ElIauk

Spring Bean 生命周期与循环依赖

一个 Bean 从创建到销毁要过几道关 很多人对 Spring Bean 的理解还停留在“容器启动时 new 一下”,实际上一段完整的生命周期要经过十几个扩展点。以单例 Bean 为例,顺序大致是:实例化、属性填充、Aware 回调、BeanPostProcessor 前置处理、初始化方法(@Pos

评论
Spring Bean 生命周期与循环依赖

一个 Bean 从创建到销毁要过几道关

很多人对 Spring Bean 的理解还停留在“容器启动时 new 一下”,实际上一段完整的生命周期要经过十几个扩展点。以单例 Bean 为例,顺序大致是:实例化、属性填充、Aware 回调、BeanPostProcessor 前置处理、初始化方法(@PostConstruct、afterPropertiesSet、init-method)、BeanPostProcessor 后置处理、使用、销毁(@PreDestroy、destroy-method)。Spring Boot 3.x 的 @PostConstruct 来自 jakarta.annotation,升级时如果还 import javax.annotation,编译会直接失败。

把这条链路记住,很多“顺序不对”的问题就有了解释:构造函数里拿不到注入的字段,是因为属性还没填充;@PostConstruct 里做远程调用会拖慢启动;原型 Bean 的销毁方法不执行,是因为容器不管理它的完整生命周期。

循环依赖靠什么兜底

Spring 只解决单例 Bean 的字段注入或 Setter 注入循环,靠的是三级缓存:singletonObjects 放成品,earlySingletonObjects 放提前暴露的半成品,singletonFactories 放能生成半成品的工厂。A 依赖 B、B 依赖 A 时,A 先实例化并把工厂放进第三级缓存,再去填充 B;B 回头取 A 时拿到提前引用,等两边都填充完再进入初始化。

构造器注入的循环解决不了,因为对象还没实例化完,没有“半成品”可以暴露。Spring Boot 2.6 起 spring.main.allow-circular-references 默认是 false,遇到循环依赖会直接抛 BeanCurrentlyInCreationException,这个默认值在 3.x 延续。

代码:观察初始化与销毁

@Component
public class LifecycleProbe implements InitializingBean, DisposableBean {

    private static final Logger log = LoggerFactory.getLogger(LifecycleProbe.class);

    private final OrderRepository orderRepository;

    public LifecycleProbe(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
        log.info("1. 构造器,依赖已就绪: {}", orderRepository.getClass().getSimpleName());
    }

    @PostConstruct
    public void postConstruct() {
        log.info("2. @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() {
        log.info("3. afterPropertiesSet");
    }

    @PreDestroy
    public void preDestroy() {
        log.info("4. @PreDestroy");
    }

    @Override
    public void destroy() {
        log.info("5. destroy");
    }
}

启动后看日志顺序,再执行 mvn spring-boot:run,按 Ctrl+C 观察销毁回调,比背文档直观。如果项目里 Bean 很多,可以在启动日志里按类名过滤,避免被其他组件的输出淹没。

循环依赖的几种处理方式

最推荐的是改结构:把双向依赖里真正需要的那部分抽成事件或独立服务,让依赖变成单向。其次是用 @Lazy 在注入点生成代理,把初始化推迟到第一次调用:

@Service
public class OrderService {

    private final PaymentService paymentService;

    public OrderService(@Lazy PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

第三才是 Setter 注入或 ObjectProvider 延迟取 Bean。后两种能跑,但往往说明两个类职责缠在一起,后面还会以别的方式出问题。至于 @DependsOn,它只能调整初始化顺序,不能打破循环,别把它当解法。

常见坑

1. 以为打开 allow-circular-references 就万事大吉

这个开关只对字段或 Setter 注入的循环有效。构造器循环即使打开也会失败,正确做法是打破依赖。

2. 循环依赖里拿到的是“裸对象”

如果 A 在初始化阶段被提前暴露,而 A 又需要被 AOP 代理(比如挂了 @Transactional、@Async),早期引用可能还没经过代理创建。表现是事务不生效、异步变同步。用 @Lazy 注入代理可以绕开,或者直接消除循环。

3. @PostConstruct 里依赖还没准备好

@PostConstruct 执行时本 Bean 的属性已经填充,但不代表它依赖的 Bean 已经完成初始化。尤其在循环依赖场景下,拿到的是半成品。初始化逻辑尽量只做本地校验,远程调用、缓存预热放到 ApplicationRunner 或 SmartLifecycle 里。

4. 原型 Bean 的销毁方法不执行

@PreDestroy 和 destroy-method 只对容器管理的单例生效。原型 Bean 用完就丢,需要自己用 try-with-resources 或显式关闭。

5. 销毁阶段还在用数据源

@PreDestroy 的执行顺序和 Bean 依赖有关,关闭连接池、线程池时如果还有任务在跑,容易看到“数据源已关闭”的错误。停机顺序建议是:先让注册中心摘流量,再停 Web 容器,最后关闭线程池和连接池;反过来,连接池先关,正在执行的任务会立刻报错。

小结

Bean 生命周期的价值不在于背顺序,而在于知道每个扩展点适合做什么、不适合做什么。循环依赖则优先当设计问题处理,@Lazy 只是止痛药,别把它当成常规方案。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者