一文讲透 Spring IoC 与 Bean:依赖注入、容器区别及单例线程安全
Spring 最核心的能力是 IoC 容器:对象不再由业务代码主动创建,而是交给容器统一实例化、装配和管理。理解 IoC、DI、Bean 容器和单例线程安全,是掌握 Spring 的基础。
一、什么是 IoC 和 DI
IoC(控制反转)是一种设计思想。传统代码由对象主动 new 出依赖,而在 Spring 中,对象的创建权和依赖管理权交给容器,控制权由业务对象反转到了 Spring。
DI(依赖注入)是实现 IoC 的主要手段。Bean 只需要声明自己依赖什么,Spring 在创建 Bean 时查找合适对象并注入。
例如,OrderService 依赖 OrderRepository。传统写法由 OrderService 自己创建 Repository;使用 DI 后,Repository 由容器提供:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
因此,两者的关系可以概括为:
IoC 是目标和思想,DI 是 Spring 实现 IoC 的方式。
二、Spring 的依赖注入方式
1. 构造器注入
Spring 创建对象时,通过构造方法传入依赖:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
只有一个构造器时,可以省略 @Autowired。构造器注入适合必须存在的依赖,也是 Spring 官方推荐的主要方式。
2. Setter 注入
Spring 先创建对象,再调用 Setter 方法完成注入:
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
Setter 注入更适合可选依赖,或者确实需要在创建后重新配置依赖的场景。
3. 字段注入
@Autowired
private UserRepository userRepository;
字段注入写法简短,但依赖被隐藏,无法声明为 final,脱离 Spring 容器进行单元测试时也不方便,因此一般不建议在核心业务代码中使用。
4. 普通方法或工厂方法参数注入
Spring 也可以给配置方法、@Bean 方法或其他注入方法的参数提供依赖:
@Bean
public OrderService orderService(OrderRepository repository) {
return new OrderService(repository);
}
从官方定义看,依赖注入主要分为构造器注入与 Setter 注入;字段和普通方法注入是注解驱动开发中的常见形式。
三、构造器注入与 Setter 注入的区别
| 对比项 | 构造器注入 | Setter 注入 |
|---|---|---|
| 适用依赖 | 必需依赖 | 可选依赖 |
| 完整性 | 创建后立即完整可用 | 可能出现未注入状态 |
| 不可变性 | 支持 final 字段 | 通常不能使用 final |
| 单元测试 | 可直接传入 Mock 对象 | 需要调用 Setter |
| 循环依赖 | 构造器循环依赖通常无法解决 | 某些单例循环依赖可能被处理 |
| 后续修改 | 创建后不宜替换 | 可以重新设置 |
实际开发中,应优先使用构造器注入:它能明确暴露必需依赖,使对象处于完整状态,并有利于构建不可变组件。如果构造器参数过多,通常说明类承担了太多职责,应考虑拆分,而不是改用字段注入隐藏问题。
四、BeanFactory 与 ApplicationContext
BeanFactory 是 Spring IoC 容器的基础接口,提供 Bean 的创建、查找、依赖注入和作用域管理等核心能力。
ApplicationContext 是更高层的容器接口,继承了 BeanFactory 的能力,并面向实际应用提供更多功能。
| 能力 | BeanFactory | ApplicationContext |
| Bean 创建和依赖注入 | 支持 | 支持 |
| 自动注册后置处理器 | 通常需手动处理 | 支持 |
国际化 MessageSource | 不直接提供 | 支持 |
| 应用事件发布 | 不直接提供 | 支持 |
| 资源访问和环境配置 | 基础能力 | 更完整 |
| 常见使用场景 | 框架底层、特殊容器控制 | 普通 Spring 应用 |
常见的 AnnotationConfigApplicationContext、Web 应用中的 WebApplicationContext 都属于 ApplicationContext 体系。实际项目通常直接使用 ApplicationContext,只有需要精细控制容器启动过程时才直接操作 BeanFactory。
还有一个常见说法是“BeanFactory 懒加载,ApplicationContext 饿加载”。这并不严谨:普通单例 Bean 在 ApplicationContext 中默认会预实例化,但可以使用 @Lazy 改为延迟创建;是否懒加载最终取决于容器启动方式和 Bean 配置。
五、Spring 单例 Bean 线程安全吗
Spring 的 singleton 只保证同一个 Bean 定义在同一个容器中共享一个实例,并不自动保证该对象线程安全。当多个请求同时调用单例 Bean 时,它们访问的是同一个对象。
1. 无状态 Bean 通常是线程安全的
如果 Bean 不保存会随请求变化的共享数据,方法只使用参数和局部变量,多个线程之间就不会互相影响:
@Service
public class PriceService {
public int calculate(int price, int count) {
int total = price * count;
return total;
}
}
total 存放在线程各自的方法栈中,因此不会产生共享竞争。
需要注意,有成员变量不一定代表有状态。例如下面的 final 依赖引用也是成员变量,但它不用于保存某个请求的临时数据,Service 通常仍被视为无状态:
private final UserRepository userRepository;
判断关键不是“有没有成员变量”,而是“有没有被多个线程共同读写的可变状态”。
2. 有状态 Bean 可能不安全
@Service
public class CounterService {
private int count = 0;
public void increase() {
count++;
}
}
count++ 包含读取、加一和写回多个步骤。多个线程并发执行时可能相互覆盖,出现更新丢失,因此该单例 Bean 不是线程安全的。
六、有状态 Bean 如何保证线程安全
最优方案通常不是给所有方法加锁,而是尽量消除共享可变状态:
-
改用局部变量:请求相关数据放在方法参数或局部变量中。
-
使用不可变对象:成员变量使用
final,对象创建后不再修改。 -
使用线程安全工具:简单计数可使用
AtomicInteger,共享集合可使用ConcurrentHashMap。 -
使用锁:复合操作需要整体原子性时,可使用
synchronized或Lock,但要评估锁竞争。 -
调整 Bean 作用域:请求状态可使用
request,会话状态可使用session;确需每次获取新实例时可考虑prototype。 -
谨慎使用 ThreadLocal:可用于保存线程独享数据,但在线程池中必须在
finally中调用remove(),否则可能数据串用或内存泄漏。 -
状态下沉到专业组件:将共享数据交给数据库、Redis 等具备并发控制能力的存储系统。
Spring 官方通常建议:无状态 Bean 使用 singleton,真正有会话状态的 Bean 考虑 prototype 或合适的 Web 作用域。但改变作用域不等于自动解决所有业务并发问题,还要结合对象的使用方式判断。
更多推荐
所有评论(0)