深入解析Java DI依赖注入:实战案例与多种实现方式
📖 目录导读
-
什么是依赖注入(DI)?为什么需要它?

-
Java DI的三种核心注入方式(含代码案例)
-
主流框架实现对比:Spring、Guice、CDI
-
手写一个简易DI容器(模拟注入原理)
-
常见问题问答(Q&A)
-
总结与最佳实践
什么是依赖注入(DI)?为什么需要它?
依赖注入(Dependency Injection,简称DI)是控制反转(IoC)的一种实现方式,核心思想是:将对象的依赖关系从“内部创建”转移到“外部注入”,简单说,就是对象不再自己new依赖类,而是由容器负责创建并传给对象。
为什么需要DI?
- 解耦:类不再直接依赖具体实现,而是依赖接口。
- 可测试性:方便Mock替换真实依赖。
- 灵活性:运行时可以切换不同实现。
💡 举个反例:传统代码中
UserService直接new UserDao(),耦合性强;引入DI后,UserService只需声明需要UserDao接口,具体实现由容器注入。
Java DI的三种核心注入方式(含代码案例)
1 构造器注入(推荐)
public class UserService {
private final UserDao userDao;
// 通过构造器注入依赖
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
// 使用(模拟容器)
UserDao dao = new UserDaoImpl();
UserService service = new UserService(dao);
2 Setter注入
public class OrderService {
private PaymentService paymentService;
@Autowired // Spring中通过注解注入
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
3 接口注入(较少用)
public interface ServiceInjector {
void inject(OrderService service);
}
public class OrderService {
public void init(ServiceInjector injector) {
injector.inject(this);
}
}
对比表格: | 注入方式 | 优点 | 缺点 | 推荐场景 | |---------|------|------|----------| | 构造器 | 不可变、明确依赖 | 参数过多时冗长 | 必须依赖 | | Setter | 可选依赖、灵活 | 可变性、易漏注入 | 非必须依赖 | | 接口 | 标准化 | 侵入性强 | 框架底层 |
主流框架实现对比:Spring、Guice、CDI
Spring DI(最常用)
@Component
public class NotificationService {
@Autowired
private EmailSender emailSender; // Spring自动注入
// 或构造器注入(推荐)
public NotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
配置:使用@Configuration+@Bean或XML。
Google Guice
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
}
}
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
CDI(Java EE标准)
@Inject private UserRepository userRepo; // 除Spring外另一套规范
手写一个简易DI容器(模拟注入原理)
为深入理解,我们实现一个简单DI框架:
public class SimpleContainer {
private Map<Class<?>, Object> instances = new HashMap<>();
public <T> void register(Class<T> clazz, T instance) {
instances.put(clazz, instance);
}
public <T> T resolve(Class<T> clazz) {
// 1. 获取类所有构造器
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
if (constructor.getParameterCount() == 0) continue;
// 2. 递归解析参数依赖
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] params = Arrays.stream(paramTypes)
.map(type -> instances.get(type) != null ? instances.get(type) : resolve(type))
.toArray();
try {
return clazz.cast(constructor.newInstance(params));
} catch (Exception e) { throw new RuntimeException(e); }
}
// 3. 无参构造直接创建
return clazz.cast(clazz.getDeclaredConstructor().newInstance());
}
}
// 使用示例
SimpleContainer container = new SimpleContainer();
container.register(UserDao.class, new UserDaoImpl());
UserService userService = container.resolve(UserService.class); // 自动注入
这个例子展示了:容器如何自动查找依赖、递归创建对象,与Spring的核心逻辑类似。
常见问题问答(Q&A)
Q1:构造器注入和字段注入(@Autowired直接写在字段上)哪个好? A:构造器注入更推荐,因为:
- 保证依赖不可变(final)
- 便于测试(无需反射)
- 明确依赖关系,避免循环引用隐患
Q2:Spring中@Autowired和@Resource区别? A:
@Autowired:按类型注入,若多个类型匹配需配合@Qualifier@Resource:按名称注入(默认取字段名),属于Java EE标准
Q3:DI容器会不会带来性能问题? A:生产环境几乎无影响,依赖注入仅在启动时解析一次,运行时性能与手动创建无差异,Spring Boot启动虽慢,但非DI本身导致。
Q4:能否不用框架实现DI? A:完全可以,例如上述手写容器,或使用工厂模式手动实例化,但规模变大后,框架管理生命周期、AOP等优势明显。
Q5:循环依赖如何解决? A:Spring通过三级缓存(早期暴露半成品对象)解决构造器注入外的循环依赖,但构造器注入的循环依赖无法自动解决,需重构代码。
总结与最佳实践
- 优先使用构造器注入,避免字段注入产生不可测试的代码。
- 面向接口编程:注入时依赖接口而非具体类。
- 合理配置作用域:Spring中单例(默认)可复用,原型(Prototype)每次获取新实例。
- 使用框架但不迷信框架:理解底层原理(如手写容器),遇到问题能快速定位。
- 注意依赖数量:当一个类有超过5个依赖时,考虑是否违反单一职责原则。
依赖注入让Java代码更优雅、可维护,掌握其核心实现方式,无论是Spring还是其他框架,都能游刃有余。