IoC 是什么
管理 Spring Bean 的容器,IoC 即“控制反转”,依靠“依赖注入(DI)”实现。
更准确地来说:IoC 是一种思想,而 DI 是一种实现,DI 实现了 IoC。
“对象的创建”这一行动,由原本的程序员(我们)手上,转移到了 IoC 的手上。
三种配置方式和三种注入方式
第一组:XML 配置模式
特点:类是纯净的 POJO,没有任何 Spring 注解,所有逻辑都在 applicationContext.xml 中。
1. XML + 构造器注入(旧代码)
配置代码 (applicationContext.xml)
<!-- 1. 定义依赖 -->
<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/>
<!-- 2. 定义业务类,使用 constructor-arg -->
<bean id="userService" class="com.example.service.impl.UserServiceImpl">
<constructor-arg ref="userDao"/>
</bean>
UserServiceImpl 类代码
package com.example.service.impl;
import com.example.dao.UserDao;
public class UserServiceImpl implements UserService {
private final UserDao userDao;
// 必须提供带参构造器
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void findAll() { userDao.query(); }
}
2. XML + Setter 注入
配置代码 (applicationContext.xml)
<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.example.service.impl.UserServiceImpl">
<!-- 使用 property 标签,name 对应 set 方法后的属性名 -->
<property name="userDao" ref="userDao"/>
</bean>
UserServiceImpl 类代码
package com.example.service.impl;
import com.example.dao.UserDao;
public class UserServiceImpl implements UserService {
private UserDao userDao;
// 必须有 Setter 方法
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void findAll() { userDao.query(); }
}
3. XML + 注解注入
注意:XML 配置不支持注解注入。XML 容器无法通过反射直接赋值,所以这种组合不存在。
第二组:Java 配置模式 (Java Config)
特点:使用 @Configuration 和 @Bean,类通常是普通的(也可以加注解,但这里演示纯 Java 配置写法)。
4. Java Config + 构造器注入(一般推荐)
配置代码 (AppConfig.java)
@Configuration
public class AppConfig {
@Bean
public UserDao userDao() {
return new UserDaoImpl();
}
// 在 @Bean 方法的参数中声明依赖,Spring 会自动注入
@Bean
public UserService userService(UserDao userDao) {
// 显式调用构造器
return new UserServiceImpl(userDao);
}
}
UserServiceImpl 类代码
package com.example.service.impl;
import com.example.dao.UserDao;
// 这是一个普通类,不需要 @Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void findAll() { userDao.query(); }
}
5. Java Config + Setter 注入
配置代码 (AppConfig.java)
@Configuration
public class AppConfig {
@Bean
public UserDao userDao() { return new UserDaoImpl(); }
@Bean
public UserService userService() {
UserServiceImpl service = new UserServiceImpl();
// 手动调用 Setter 方法注入
service.setUserDao(userDao());
return service;
}
}
UserServiceImpl 类代码
package com.example.service.impl;
import com.example.dao.UserDao;
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void findAll() { userDao.query(); }
}
6. Java Config + 注解注入
注意:在纯 Java Config 模式下,我们通常不通过注解注入来管理 Bean,而是通过方法参数或返回值。
如果非要这么干,必须先在类上加 @Autowired 注解(这就变成了混合模式),纯 Java Config 模式不支持字段注入。
第三组:注解配置模式 (Annotation)
特点:类上加 @Service,配合 @Autowired,Spring 自动扫描。这是 Spring Boot 的主流。
7. 注解 + 构造器注入(官方推荐)
配置代码
无需显式配置,Spring Boot 启动类扫描包即可。
UserServiceImpl 类代码
@Service // 1. 注册 Bean
public class UserServiceImpl implements UserService {
private final UserDao userDao;
// 2. 构造器注入
// 如果只有一个构造器,@Autowired 可以省略
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void findAll() { userDao.query(); }
}
8. 注解 + Setter 注入
配置代码
无需显式配置。
UserServiceImpl 类代码
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
// @Autowired 写在 Setter 方法上面
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void findAll() { userDao.query(); }
}
9. 注解 + 字段注入(最常用但不推荐)
配置代码
无需显式配置。
UserServiceImpl 类代码
@Service
public class UserServiceImpl implements UserService {
// @Autowired 直接写在字段上
@Autowired
private UserDao userDao;
public void findAll() { userDao.query(); }
}
核心难点:如何处理多个实现类?
当 UserDao 有两个实现类 UserDaoImpl 和 AdminUserDaoImpl 时,Spring 不知道该选谁,会报错 NoUniqueBeanDefinitionException。
1. XML 配置下的解决方案
XML 依靠 ref 属性天然就是 byName 的,即对应了Bean的id,所以非常清晰。
<!-- 定义两个实现类 -->
<bean id="userDao" class="...UserDaoImpl"/>
<bean id="adminUserDao" class="...AdminUserDaoImpl"/>
<!-- 在注入时,直接修改 ref 指向你想要的 ID -->
<bean id="userService" class="...UserServiceImpl">
<property name="userDao" ref="adminUserDao"/>
</bean>
2. Java Config 下的解决方案
使用 @Qualifier 或者方法名匹配。
@Bean
public UserService userService(UserDao adminUserDao) { // 参数名必须匹配 Bean 的名字
return new UserServiceImpl(adminUserDao);
}
// 或者
@Bean
public UserService userService(@Qualifier("adminUserDao") UserDao userDao) {
return new UserServiceImpl(userDao);
}
3. 注解配置下的解决方案
必须使用 @Qualifier 注解来指定 Bean 的名字(默认类名首字母小写)。
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Autowired
public UserServiceImpl(@Qualifier("adminUserDao") UserDao userDao) {
this.userDao = userDao;
}
}
对照表
| 组合模式 | 配置位置 | 注入代码特征 | 类代码特征 | 推荐度 |
|---|---|---|---|---|
| XML + 构造器 | <constructor-arg> | 调用带参构造器 | 纯 POJO,有构造器 | (老项目) |
| XML + Setter | <property> | 调用 set 方法 | 纯 POJO,有 Setter | |
| Java + 构造器 | @Bean 方法参数 | new Obj(param) | 纯 POJO,有构造器 | (第三方库) |
| Java + Setter | 方法内调用 set | setXxx(param) | 纯 POJO,有 Setter | |
| 注解 + 构造器 | @Service | (自动注入) | @Service,有构造器 | (首选) |
| 注解 + Setter | @Autowired | (自动注入) | @Service,有 Setter | |
| 注解 + 字段 | @Autowired | (自动注入) | @Service,私有字段 | (最常用) |
三种注入方式评估
setter 注入
缺点:
- 空指针风险,在注入前必须先构造,存在为空的半结构化风险。
- 无法使用final修饰,对象为动态,是不稳定的。
- 无法发现循环依赖问题,默认通过。
- Java Config 配置时需要手动调用 setter 方法
优点:
- 灵活性高。
Java Config 注入
缺点:
- 修改配置需重新编译、重新部署。
- 没有注解+构造的模式方便。
优点:
- 在java文件中写,类型安全,有编译期检查。
- 逻辑能力强。
- 允许 @ComponentScan 注解进行扫包,将外部库注册为 Bean
构造器注入
缺点:
- 构造函数参数过多,违反了单一职责原则,需要拆分。
优点:
- 暴露循环依赖,会直接报错,强制你进行解决,而不是等待某个业务出问题才报错,提高了稳定性。
- final 支持不可变性。
- 类不依赖 Spring 容器,完全是一个普通的 Java 对象,意味着没有 Spring 容器时也能运行。
综上,构造器注入为一般情况下最优的依赖注入,通常配合注解配置使用
若需求第三方库时,无法通过添加注解直接对代码更改,需要使用 构造器注入 + Java Config配置
AOP 下的循环依赖问题
Aop会使对象产生代理对象,Spring 在三级缓存的工厂里会生成代理对象,而不是原始对象。
对于两个代理对象的互相注入,会出现问题。
对于两个类:class A 和 class B
当A中进行B的依赖注入,同时B中也进行A的依赖注入,Spring 会无法操作从而出现错误
解决方案:三级缓存
注意:三级缓存也无法解决构造器注入的循环以来问题
三级缓存:为了解决Aop代理问题
第三级缓存: 存放注册Bean的逻辑(即工厂,ObjectFactory,用来生成某个对象的 Bean ),还未实例化
第二级缓存: 存放半实例化的对象,此时为半成品
第一级缓存: 存放实例化的对象(若存在Aop则为代理后的对象ProxyA,并非原始对象),此时为完整品
三级缓存的行为模拟
- 当 A 进行注册时,Spring 将 “ A 的工厂 ” (ObjectFactory) 放入 第三级缓存。
- 发现 A 的实例化需要注入 B 。
- 在所有缓存中寻找 B,发现没找到实例化的 B 。
- 进行B的注册,Spring 将 “ B 的工厂 ” 放入第三级缓存。
- 发现 B 的实例化需要注入 A 。
- 在缓存中寻找 A,发现没找到实例化的 A 。
- B 去三级缓存找 A 的工厂,工厂发现 A 需要 AOP,于是提前生成 ProxyA,并将 ProxyA 放入二级缓存。
- 此时 A 为半实例化, B 将 ProxyA 进行注入后,生成 B 的代理对象ProxyB,转移到第一级缓存中,并且删除第三级缓存的 ProxyB 。
- B 实例化完毕,第二级缓存的 ProxyA 将 ProxyB 注入, ProxyA 从第二级缓存转移到第一级缓存中。并删除第二级缓存的 ProxyA。
- 此时 ProxyA 和ProxyB 都在第一级缓存中,完毕。
构造器注入的具体解决方法:@Lazy
两个方法:
- 使用@Lazy懒加载,注入一个延迟代理。
- 重构代码,拆分出第三个类 C , A 和 B 都依赖 C。
本文结束
如果这篇文章帮你理清了思路,我也感到很开心。
这也同时是我的学习笔记,能帮到别人我不胜感谢,若哪里出错希望指出,同样不胜感激!
以上,@Aroaku