Skip to content

Spring Bean 生命周期:扩展点在哪一层,拿到的是哪个对象 ​

背下十几个回调的顺序用处不大。更有用的是分清三件事:哪些扩展点改的是定义、哪些改的是实例;代理在哪一步产生;以及容器最后交出去的,已经不是构造函数里的那个 this。

一个缓存服务在 @PostConstruct 里调用了自己的 @Transactional 方法做预热,日志显示没有事务。同一个方法在启动之后调用,事务正常开启。原因不在事务配置:@PostConstruct 执行时,这个对象还是原始实例,代理要在后面的步骤里才生成。

本文用 Spring Framework 7.0.9 的真实容器记录一个单例从定义到销毁的每一个回调,再验证几个常见的坑:容器交出去的是代理、JDK 代理下按实现类型取不到 Bean、prototype 不会被销毁、后处理器过早依赖普通 Bean 让它错过代理。

一、先说结论 ​

  • 扩展点分两层:BeanFactoryPostProcessor 改的是定义(配方),在任何普通 Bean 创建之前执行;BeanPostProcessor 处理的是实例(成品),代理就在它的 after 阶段产生。
  • 容器交出去的是代理:实测 getBean 拿到的对象与构造出的实例不是同一个;@PostConstruct 里通过 this 调用 @Transactional 方法,事务不生效。
  • 初始化回调的顺序是固定的:@PostConstruct → afterPropertiesSet → initMethod,但不要依赖三种机制叠加,选一种即可。
  • prototype 不归容器销毁:实测取 2 次 prototype Bean,初始化回调执行 2 次,销毁回调 0 次。
  • 后处理器不要依赖普通 Bean:被后处理器提前创建的 Bean 会错过代理,实测它的 @Transactional 方法没有事务;容器只打一条警告。
  • 类是不是代理影响按类型查找:纯 Spring Framework 下,实现了接口的 Bean 默认是 JDK 动态代理,按实现类型 getBean 会找不到;Spring Boot 默认用类代理。

二、一个单例的完整时间线 ​

实验里的 PriceCache 实现了几乎所有生命周期接口,每个回调记一笔:

text
BeanFactoryPostProcessor 修改 priceCache 的定义:timeoutMs=500
→ Clock 构造
→ PriceCache 构造(注入 Clock)
→ 属性填充 timeoutMs=500
→ BeanNameAware.setBeanName → BeanFactoryAware.setBeanFactory → ApplicationContextAware.setApplicationContext
→ BeanPostProcessor.before
→ @PostConstruct → InitializingBean.afterPropertiesSet → @Bean(initMethod)
→ BeanPostProcessor.after(拿到的是代理)
→ SmartInitializingSingleton.afterSingletonsInstantiated
→ SmartLifecycle.start
→ ContextRefreshedEvent

关闭容器时:

text
ContextClosedEvent → SmartLifecycle.stop → @PreDestroy → DisposableBean.destroy → @Bean(destroyMethod)

把它按「改什么」分成几段更容易记住:

改定义BeanFactoryPostProcessor实例化与注入构造器字段 / setter初始化@PostConstructafterPropertiesSet · init包装BeanPostProcessorafter:产生代理全部就绪SmartInitializing…Lifecycle.start · 事件这里的 this 还是原始对象@Transactional、@Async 通过 this 调用都不生效容器交出去的是代理事务、异步、切面在这里生效关闭:ContextClosedEvent → Lifecycle.stop → @PreDestroy → destroy → destroyMethod;prototype 不会被销毁
图 1 · BeanFactoryPostProcessor 改的是定义,在任何普通 Bean 创建之前;初始化回调之后,BeanPostProcessor 的 after 阶段产生代理;所有单例就绪之后才适合做依赖外部系统的启动动作
阶段回调适合做什么
改定义BeanFactoryPostProcessor修改属性值、替换占位符、注册额外的定义;此时还没有任何普通 Bean
实例化与注入构造器、字段和 setter 注入只做赋值,不要调用依赖的方法
感知容器*Aware拿到 Bean 名称、工厂、上下文;新代码通常直接注入需要的对象
初始化@PostConstruct、afterPropertiesSet、initMethod轻量的校验与准备,不要做慢速远程调用
包装BeanPostProcessor.after代理在这里产生(事务、@Async、AOP 切面)
全部就绪SmartInitializingSingleton、SmartLifecycle.start、ContextRefreshedEvent预热缓存、启动消费者、开始接受流量
关闭SmartLifecycle.stop、@PreDestroy、destroy、destroyMethod停止接收新任务、释放资源,回调要幂等

BeanFactoryPostProcessor 设置的 timeoutMs=500 在属性填充阶段生效,证明它改的是定义:它执行时 PriceCache 还没有被创建。

三、容器交出去的是代理 ​

PriceCache.refresh() 带有 @Transactional,所以它最终会被包上一层代理:

text
getBean 返回 PriceCache$$SpringCGLIB$$<n>,与构造出的实例相同=false;
@PostConstruct 里调用 @Transactional 方法:事务活跃=false;
启动后经容器取得的对象调用:事务活跃=true

@PostConstruct 执行时,代理还没有产生,即使产生了,通过 this 调用也绕过了代理。这与 Spring AOP 为什么会失效 里的自调用是同一个问题。需要在启动时执行事务性的预热,把它放到所有单例就绪之后的回调里(SmartInitializingSingleton、ApplicationReadyEvent 或 SmartLifecycle.start),并且通过注入的代理调用。

3.1 JDK 代理与按类型查找 ​

PriceCache 实现了 SmartLifecycle 等接口。在纯 Spring Framework 容器里,@EnableTransactionManagement 默认创建 JDK 动态代理:代理只实现这些接口,并不是 PriceCache 的子类。

text
priceCache 是 JDK 动态代理=true,按类型 getBean(PriceCache.class):NoSuchBeanDefinitionException

同一个 Bean 按名称能取到,按实现类型取不到,注入 PriceCache 类型的字段也会失败。Spring Boot 默认 spring.aop.proxy-target-class=true,使用类代理,所以在 Boot 应用里不常遇到;但在纯 Framework 项目、或者有人把这个配置改掉之后,就会出现「明明有这个 Bean 却注入失败」。

四、prototype 只管生,不管死 ​

text
取 2 次 prototype Bean 后关闭容器:@PostConstruct 2 次,@PreDestroy 0 次

容器创建 prototype Bean 并完成初始化之后,就把它交给调用方,不再跟踪。持有连接、文件、线程的 prototype Bean,需要调用方自己释放。如果一个资源需要容器管理生命周期,它更适合作为单例,或者用作用域代理包装。

五、后处理器过早依赖普通 Bean ​

后处理器本身也是 Bean,它们比普通 Bean 更早创建。一个后处理器如果通过构造器依赖了普通 Bean,那个普通 Bean 就会在「后处理器还没全部注册」的时候被提前创建:

java
public static class AuditingPostProcessor implements BeanPostProcessor, PriorityOrdered {
    public AuditingPostProcessor(AuditService audit) { ... }   // AuditService 被提前创建
}
text
被后处理器提前创建的 AuditService:是代理=false,调用 @Transactional 方法时事务活跃=false
WARNING: Bean 'auditService' of type [...AuditService] is not eligible for getting processed
         by all BeanPostProcessors (for example: not eligible for auto-proxying)...

AuditService 没有被代理,它的 @Transactional 静默失效。容器只打了一条警告,启动照常成功。实验里这个后处理器实现了 PriorityOrdered,比负责创建代理的后处理器更早注册;没有实现排序接口的后处理器注册得更晚,那时代理创建器已经就位,结果可能不同。无论哪种情况,都不应该让后处理器依赖业务 Bean:需要时用 ObjectProvider 延迟获取,或者把后处理器声明成 static 的 @Bean 并且不注入普通 Bean。

启动日志里出现 is not eligible for getting processed by all BeanPostProcessors,就值得认真看一眼。

AuditingPostProcessorPriorityOrdered,先注册AuditService被提前创建自动代理创建器Ordered,之后才注册构造器依赖来不及不是代理事务活跃=false日志只有一条 WARNING:… is not eligible for getting processed by all BeanPostProcessors
图 2 · 实现 PriorityOrdered 的后处理器比创建代理的后处理器更早注册;它通过构造器依赖 AuditService,AuditService 于是在代理创建器就位前被创建,最终不是代理,@Transactional 静默失效

六、几个工程建议 ​

  • 初始化回调只选一种:优先 @PostConstruct 或构造器,不要把一个 Bean 的初始化拆在三个回调里。
  • 慢操作不要放在初始化回调里:远程调用没有超时,应用就一直起不来。需要等待外部系统的动作放到 SmartLifecycle.start 或就绪事件里,并设置超时。
  • 启动时的业务操作通过代理调用:事务、异步、缓存注解都依赖代理,在 @PostConstruct 里通过 this 调用都不生效。
  • 销毁回调要幂等:优雅停机可能重试,容器关闭过程中也可能出现部分失败。
  • 看启动日志里的警告:「not eligible for auto-proxying」意味着某些注解已经悄悄失效。

七、常见误区 ​

  • 「@PostConstruct 里 Bean 已经完全可用」:此时还是原始对象,代理和其他 Bean 的就绪都不能指望。
  • 「从容器取到的就是我 new 出来的那个对象」:带事务、异步、切面的 Bean 拿到的是代理。
  • 「prototype Bean 关闭容器时会被销毁」:不会,容器不跟踪 prototype 实例。
  • 「后处理器里注入个服务很正常」:会让那个服务错过其他后处理器,包括创建代理的那个。
  • 「生命周期对所有 Bean 都一样」:FactoryBean、作用域代理、懒加载、循环依赖都会改变具体路径,见 Spring 循环依赖。

小结 ​

理解 Bean 生命周期,关键是分清「改定义」和「改实例」两层扩展点,知道代理在初始化回调之后才产生,以及容器交出去的对象可能已经不是 this。据此就能判断:启动逻辑放在哪个回调里、为什么某个注解在启动时不生效、为什么一个 Bean 明明存在却注入失败。自动配置怎样决定要不要创建这些 Bean,见 Spring Boot 自动配置。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权