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 的能力,并面向实际应用提供更多功能。

能力BeanFactoryApplicationContext
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 如何保证线程安全

最优方案通常不是给所有方法加锁,而是尽量消除共享可变状态:

  1. 改用局部变量:请求相关数据放在方法参数或局部变量中。

  2. 使用不可变对象:成员变量使用 final,对象创建后不再修改。

  3. 使用线程安全工具:简单计数可使用 AtomicInteger,共享集合可使用 ConcurrentHashMap

  4. 使用锁:复合操作需要整体原子性时,可使用 synchronizedLock,但要评估锁竞争。

  5. 调整 Bean 作用域:请求状态可使用 request,会话状态可使用 session;确需每次获取新实例时可考虑 prototype

  6. 谨慎使用 ThreadLocal:可用于保存线程独享数据,但在线程池中必须在 finally 中调用 remove(),否则可能数据串用或内存泄漏。

  7. 状态下沉到专业组件:将共享数据交给数据库、Redis 等具备并发控制能力的存储系统。

Spring 官方通常建议:无状态 Bean 使用 singleton,真正有会话状态的 Bean 考虑 prototype 或合适的 Web 作用域。但改变作用域不等于自动解决所有业务并发问题,还要结合对象的使用方式判断。

参考资料:Spring 依赖注入BeanFactory APISpring Bean 作用域

更多推荐