依赖注入详解:Spring IoC 的实现方式
一、什么是依赖注入?
依赖注入(Dependency Injection,DI)是 Spring 实现控制反转(IoC)的具体方式。它的核心思想是:对象不再自己创建依赖,而是由外部容器在运行时将依赖注入到对象中。
java
// 没有依赖注入
public class UserService {
private UserDao userDao = new UserDaoImpl(); // 自己创建依赖
}
// 使用依赖注入
@Service
public class UserService {
@Autowired
private UserDao userDao; // 由容器注入
}
依赖注入把"如何获取依赖"这件事从代码中移除了。UserService 不需要知道 UserDao 的具体实现是什么,也不需要知道它怎么创建。它只声明"我需要一个 UserDao",剩下的由容器负责。
二、三种注入方式
2.1 构造器注入
依赖通过构造方法参数传入。
java
@Service
public class UserService {
private final UserDao userDao;
private final EmailService emailService;
public UserService(UserDao userDao, EmailService emailService) {
this.userDao = userDao;
this.emailService = emailService;
}
}
Spring 在创建 UserService 时,会从容器中找到 UserDao 和 EmailService 类型的 Bean,作为参数调用构造方法。
如果类只有一个构造方法,Spring 4.3 之后可以省略 @Autowired:
java
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
如果类有多个构造方法,需要显式标注 @Autowired 指定使用哪一个。
优点:
- 依赖不可变(
final修饰),对象创建后依赖不可更改 - 依赖在对象创建时就完全就绪,避免空指针
- 便于单元测试,直接传入 Mock 对象即可
- 能明确暴露类的依赖关系,依赖过多时能及时发现设计问题
缺点:
- 依赖较多时构造方法参数列表很长
- 构造器注入无法解决循环依赖问题
2.2 Setter 注入
依赖通过 Setter 方法注入。
java
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
Spring 先通过无参构造方法创建对象,再调用 Setter 方法注入依赖。
优点:
- 可以在对象创建后动态替换依赖
- 依赖是可选时比较灵活
缺点:
- 依赖不是不可变的,可能被意外修改
- 对象创建后依赖可能仍为 null,存在空指针风险
2.3 字段注入
依赖直接注入到字段上。
java
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private EmailService emailService;
}
Spring 通过反射直接将依赖赋值给字段,不需要 Setter 方法。
优点:
- 代码最简洁,写起来最方便
- 适合快速开发
缺点:
- 无法使用
final修饰字段 - 单元测试时无法直接通过构造方法注入 Mock 对象,需要借助
ReflectionTestUtils或 Spring 测试上下文 - 依赖关系不够清晰,容易注入过多依赖
- 可能隐藏设计问题(类承担了过多职责)
三、三种方式对比
| 对比维度 | 构造器注入 | Setter 注入 | 字段注入 |
|---|---|---|---|
| 依赖可变性 | 不可变 | 可变 | 可变 |
| 空指针风险 | 无 | 有 | 有 |
| 单元测试便利性 | 高 | 中 | 低 |
| 循环依赖支持 | 不支持 | 支持 | 支持 |
| 依赖过多时提示 | 构造方法参数过长,容易发现 | 不明显 | 不明显 |
| Spring 官方推荐 | ✅ 推荐 | ⚠️ 可选 | ❌ 不推荐 |
四、底层原理
依赖注入的核心处理类是 AutowiredAnnotationBeanPostProcessor,它实现了 BeanPostProcessor 接口。在 Bean 实例化并填充属性阶段,该处理器会扫描所有标注了 @Autowired、@Value、@Inject 的字段和方法,解析依赖并注入。
简化后的流程:
scss
1. Spring 实例化 Bean(通过构造方法反射创建对象)
2. 调用 populateBean() 填充属性
3. AutowiredAnnotationBeanPostProcessor.postProcessProperties()
├── 扫描字段/方法上的 @Autowired
├── 解析依赖类型(DefaultListableBeanFactory.resolveDependency)
│ ├── 按类型查找 Bean
│ ├── 找到多个时按名称匹配
│ ├── 有 @Primary 时优先
│ └── 有 @Qualifier 时按名称
└── 通过反射设置字段值或调用 Setter 方法
对于构造器注入,处理时机更早------在实例化阶段,ConstructorResolver 会解析构造方法的参数类型,从容器中查找匹配的 Bean,再调用构造方法。
五、@Autowired 与 @Resource 的区别
| 对比 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 注解 | Java 标准注解(JSR-250) |
| 匹配方式 | 先按类型,再按名称 | 先按名称,再按类型 |
| 是否支持 required | 支持 required = false |
不支持 |
| 是否支持 @Primary | 支持 | 不支持 |
| 应用位置 | 字段、构造器、方法、参数 | 字段、方法 |
java
// @Autowired 按类型
@Autowired
private UserDao userDao;
// @Resource 按名称
@Resource(name = "userDaoImpl")
private UserDao userDao;
六、循环依赖问题
6.1 什么是循环依赖?
两个 Bean 互相依赖对方:
java
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
6.2 构造器注入的循环依赖
java
@Service
public class AService {
private final BService bService;
public AService(BService bService) { this.bService = bService; }
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) { this.aService = aService; }
}
这种情况会直接抛出 BeanCurrentlyInCreationException。因为创建 A 需要 B,创建 B 需要 A,两个构造方法互相等待,无法完成。
6.3 字段注入的循环依赖
字段注入(或 Setter 注入)可以解决循环依赖。Spring 通过三级缓存提前暴露对象的早期引用。
css
创建 AService:
① 实例化 A(此时 bService 为 null)
② 将 A 的早期引用放入三级缓存 singletonFactories
③ 填充属性,发现需要 BService
创建 BService:
① 实例化 B(aService 为 null)
② 将 B 的早期引用放入三级缓存
③ 填充属性,发现需要 AService
从三级缓存拿到 A 的早期引用,注入
④ B 创建完成,放入一级缓存
④ 将 B 注入 A
⑤ A 创建完成
三级缓存分别是:
singletonObjects(一级缓存):完整的单例 BeanearlySingletonObjects(二级缓存):提前暴露的 Bean,尚未完成属性注入singletonFactories(三级缓存):Bean 工厂,用于生成早期引用
Spring 通过 ObjectFactory 和 AOP 代理的延迟创建,解决了循环依赖。但字段注入的循环依赖虽然能解决,本质上是一种"设计上不健康"的表现。更好的做法是重构代码,消除循环依赖。
七、常见问题
注入失败:NoSuchBeanDefinitionException
找不到匹配的 Bean。检查是否加了 @Component、@Service 等注解,是否在扫描范围内,类型是否匹配。
注入失败:NoUniqueBeanDefinitionException
找到多个同类型 Bean。使用 @Qualifier("beanName") 指定具体名称,或使用 @Primary 标记首选 Bean。
注入失败:字段为 null
检查类是否被 Spring 管理。如果 new 出来的对象,依赖注入不会生效,因为对象不受容器管理。
静态字段无法注入
@Autowired 不能直接注入静态字段。可以在 Setter 方法上标注 @Autowired,赋值给静态变量:
java
@Component
public class SpringContextHolder {
private static ApplicationContext context;
@Autowired
public void setContext(ApplicationContext context) {
SpringContextHolder.context = context;
}
}
八、最佳实践
优先使用构造器注入。 Spring 官方推荐构造器注入,它保证了依赖的不可变性和完整性。依赖过多时,构造方法参数过长本身就是设计需要拆分的一个信号。
避免字段注入。 字段注入虽然写起来方便,但不利于测试,也容易积累过多依赖而不自知。
使用 @Qualifier 处理多实现。 当一个接口有多个实现时,用 @Qualifier 明确指定名称,避免歧义。
避免循环依赖。 循环依赖通常意味着两个类的职责边界不清晰。通过提取公共服务、调整职责划分来消除循环依赖,而不是依赖三级缓存来"兜底"。
用 final 修饰构造器注入的字段。 保证依赖不可变,也向调用方传达这个依赖是必须的。
依赖注入是 Spring 最核心的能力之一。它把对象从"自己管理依赖"中解放出来,让代码只关注业务逻辑本身。理解三种注入方式的区别和底层原理,是写出高质量 Spring 代码的基础。