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 实现了几乎所有生命周期接口,每个回调记一笔:
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关闭容器时:
ContextClosedEvent → SmartLifecycle.stop → @PreDestroy → DisposableBean.destroy → @Bean(destroyMethod)把它按「改什么」分成几段更容易记住:
| 阶段 | 回调 | 适合做什么 |
|---|---|---|
| 改定义 | 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,所以它最终会被包上一层代理:
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 的子类。
priceCache 是 JDK 动态代理=true,按类型 getBean(PriceCache.class):NoSuchBeanDefinitionException同一个 Bean 按名称能取到,按实现类型取不到,注入 PriceCache 类型的字段也会失败。Spring Boot 默认 spring.aop.proxy-target-class=true,使用类代理,所以在 Boot 应用里不常遇到;但在纯 Framework 项目、或者有人把这个配置改掉之后,就会出现「明明有这个 Bean 却注入失败」。
四、prototype 只管生,不管死
取 2 次 prototype Bean 后关闭容器:@PostConstruct 2 次,@PreDestroy 0 次容器创建 prototype Bean 并完成初始化之后,就把它交给调用方,不再跟踪。持有连接、文件、线程的 prototype Bean,需要调用方自己释放。如果一个资源需要容器管理生命周期,它更适合作为单例,或者用作用域代理包装。
五、后处理器过早依赖普通 Bean
后处理器本身也是 Bean,它们比普通 Bean 更早创建。一个后处理器如果通过构造器依赖了普通 Bean,那个普通 Bean 就会在「后处理器还没全部注册」的时候被提前创建:
public static class AuditingPostProcessor implements BeanPostProcessor, PriorityOrdered {
public AuditingPostProcessor(AuditService audit) { ... } // AuditService 被提前创建
}被后处理器提前创建的 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,就值得认真看一眼。
六、几个工程建议
- 初始化回调只选一种:优先
@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 自动配置。
配套实验
- codesphere-labs/spring/bean-lifecycle:单例启动与关闭的回调顺序、容器交出的代理与
@PostConstruct里的事务、JDK 代理与按类型取 Bean、prototype 的销毁、提前创建的 Bean 错过代理(验证记录)
参考资料