Bean 生命周期完全指南:从实例化到销毁的每一步
一个 Java 对象只要 new 出来就存在了,但「存在」和「能被安全使用」是两回事:构造器跑完时,你 @Autowired 的字段可能还是 null;依赖填好了,也许还要校验配置、预热缓存、打开连接;程序退出前还得把这些资源关掉。Spring 把「从诞生到能用、再到退休」这整段路标准化成了八个阶段,并在每个阶段留了口子让你插自己的代码——这就是 Bean 生命周期。
先给五个词一句话解释(后面全文都用得上):
- Bean:不是 Spring 自己造的对象,而是「交给容器创建和管理的那个对象」,通常就是你写的一个普通 Java 类实例
- 容器:那个帮你 new 对象、传依赖、管生老病死的「大管家」对象(
ApplicationContext),你用getBean找它要东西 - 反射:程序在运行时「看着类名去调构造器和方法」的技术——容器没写死你的类,它是靠反射把你的对象造出来的
- 钩子:框架在自己的流程里预留的「插槽」,你把方法放进去,容器走到那一步就回调你
@PostConstruct:一个注解,意思是「依赖都填好了,请先执行我这个方法再对外提供服务」
把一个 Bean 想成一个人的一生:出生(实例化,构造函数那一刻)、长身体(属性填充,把爸妈给的营养吸收进来)、上户口领身份证(Aware 回调,知道「我叫什么、住在哪个容器」)、入学前的体检(初始化,配置对不对、能不能上岗)、毕业找工作时被包装了一层简历与工牌(AOP 代理,别人见到的其实是「工牌版」的你)、工作几十年(就绪入池,反复被调用)、最后退休清算(销毁回调,把占用的资源还回去)。

上图从左到右就是一个单例 Bean 真实的「成长时间线」:越靠左,能读到的注入依赖越少。第三节会把它变成可运行的日志。
学完这一篇,你应该能回答三个问题:
@PostConstruct、afterPropertiesSet、initMethod到底谁先谁后?为什么是这个顺序?- 为什么原型(prototype)Bean 的
@PreDestroy从来不打印?容器不是答应帮我管理吗? - AOP 代理是在哪一步生成的?为什么它决定了「构造器里能不能用
@Transactional」?
先问一个"为什么":为什么需要生命周期回调?因为一个对象被"创建"出来,和它"可以安全地被使用",是两回事。构造器结束时对象只是有了内存,依赖还没注入;依赖注入完又可能需要校验配置、预热缓存、打开连接;使用完毕还需释放资源。Spring 把这些"插缝"的时机标准化成了八个人生阶段。

八个阶段依次是:实例化 → 属性填充 → Aware 回调 → 初始化前置处理 → 初始化 → 初始化后置处理 → 就绪入池 → 销毁回调。注意这是"环形"而非"直线"——单例对象走完一圈就常驻容器,直到容器关闭才走最后的销毁阶段。
本文聚焦单例的生命周期。原型(prototype)Bean 只走到"就绪"就交给调用方,容器不再负责它的销毁——这一点会在第五节展开。

把"谁触发、在哪触发、用来干嘛"一次性列清楚:
| 阶段 | 触发的接口 / 注解 | 源码位置 | 典型用途 |
|---|---|---|---|
| 实例化 | 构造器 / 工厂方法 | AbstractAutowireCapableBeanFactory#createBeanInstance | 确定用哪个构造器,反射 newInstance |
| 属性填充 | @Autowired / @Value / XML property | #populateBean → AutowiredAnnotationBeanPostProcessor | 把依赖对象塞进字段或 setter |
| Aware 回调 | BeanNameAware / BeanFactoryAware / ApplicationContextAware | #invokeAwareMethods + ApplicationContextAwareProcessor | 让 Bean 拿到"自己是谁、身处何容器" |
| 初始化前置 | BeanPostProcessor#postProcessBeforeInitialization | #initializeBean 内第一段循环 | @PostConstruct 由 CommonAnnotationBeanPostProcessor 在此触发 |
| 初始化 | InitializingBean#afterPropertiesSet / @Bean(initMethod) | #invokeInitMethods | 校验配置、预热缓存、打开资源 |
| 初始化后置 | BeanPostProcessor#postProcessAfterInitialization | #initializeBean 内第二段循环 | AOP 代理生成(AbstractAutoProxyCreator) |
| 就绪入池 | —— | DefaultSingletonBeanRegistry#addSingleton | 放入 singletonObjects,供全局复用 |
| 销毁回调 | @PreDestroy / DisposableBean#destroy / @Bean(destroyMethod) | DisposableBeanAdapter#destroy | 关闭连接、刷盘、释放线程池 |
@PostConstruct 并不是一个独立阶段,它本质上是一个 BeanPostProcessor 在"初始化前置"阶段的回调。理解这一点,才能解释第七节那个经典坑。
表格写的是「谁触发」,而新手真正卡住的地方是「我这一格能读到什么」:构造器里能不能用 @Autowired 的字段?@PostConstruct 里能不能拿到代理?八站点一遍,每站给一句实话:
上面那张环形图回答「有哪些站」,这一台单步台回答「谁在推」。左边是 getBean → doGetBean → doCreateBean 的真实骨架,右边同步刷新变量与调用栈。连点下一步,盯两件事:第 ② 步的缓存命中,和第 ⑧ 步入池那一行的位置:
Object getBean("userService") // 你的代码只写了这一行Object cached = getSingleton(beanName); // ① 先问一级缓存:已经有了吗if (cached != null) return getObjectForBeanInstance(cached); // ② 命中就短路返回RootBeanDefinition mbd = getMergedBeanDefinition(beanName); // ③ 未命中:读档案Object obj = new UserService(); // ④ doCreateBean:实例化populateBean(obj, mbd); // ⑤ 属性填充:@Autowired 生效initializeBean(obj, beanName, mbd); // ⑥ Aware + 初始化前后 + 代理addSingleton(beanName, obj); // ⑦ 成品进一级缓存return getSingleton(beanName); // ⑧ 出池,交给调用方| beanName | userService |
| singletonObjects | {orderService=..., ...} |
| allowCircularReferences | false |
getBeandoGetBean第一次与第二次这两条路的差别,动画再放一遍。注意第 ④ 帧的「命中缓存」与第 ⑤ 帧的「原型从不入池」——它俩共同决定了第五节所有关于销毁的现象:

第一次走完八站、第二次只读缓存,这条差异值得单独画一张:

空谈源码不如跑一遍。下面这个 Bean 实现了三种回调接口,并叠加注解回调:
package com.example.lifecycle;import org.springframework.beans.factory.BeanNameAware;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.InitializingBean;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;public class LifecycleDemoBean implements BeanNameAware, InitializingBean, DisposableBean { private String beanName; public LifecycleDemoBean() { System.out.println("[1] 构造器:实例化"); } @Override public void setBeanName(String name) { this.beanName = name; System.out.println("[2] BeanNameAware#setBeanName -> " + name); } @PostConstruct public void postConstruct() { System.out.println("[3] @PostConstruct"); } @Override public void afterPropertiesSet() { System.out.println("[4] InitializingBean#afterPropertiesSet"); } public void customInit() { System.out.println("[5] initMethod (customInit)"); } @PreDestroy public void preDestroy() { System.out.println("[6] @PreDestroy"); } @Override public void destroy() { System.out.println("[7] DisposableBean#destroy"); } public void customDestroy() { System.out.println("[8] destroyMethod (customDestroy)"); }}配置类与启动代码:
@Configurationpublic class LifecycleConfig { @Bean(initMethod = "customInit", destroyMethod = "customDestroy") public LifecycleDemoBean demoBean() { return new LifecycleDemoBean(); }}public class Main { public static void main(String[] args) { // try-with-resources 会在块结束自动 close(),从而触发销毁 try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(LifecycleConfig.class)) { System.out.println("[*] 容器就绪,可以取用了"); } System.out.println("[*] 容器已关闭"); }}控制台输出与阶段的对应关系:
[1] 构造器:实例化 ← 阶段一:实例化[2] BeanNameAware#setBeanName -> demoBean ← 阶段三:Aware 回调[3] @PostConstruct ← 阶段五:初始化(BeanPostProcessor 触发)[4] InitializingBean#afterPropertiesSet ← 阶段五:初始化(顺序在 @PostConstruct 之后)[5] initMethod (customInit) ← 阶段五:初始化(最后执行)[*] 容器就绪,可以取用了[6] @PreDestroy ← 阶段八:销毁(先执行)[7] DisposableBean#destroy ← 阶段八:销毁[8] destroyMethod (customDestroy) ← 阶段八:销毁(最后执行)[*] 容器已关闭- 初始化回调的固定顺序:
@PostConstruct→afterPropertiesSet→initMethod - 销毁回调的固定顺序:
@PreDestroy→destroy→destroyMethod - 注意
[2]出现在[3]之前:Aware 回调早于任何初始化回调
把上面这九行日志倒回去对照动图看一遍——每一帧正好对应一行输出,[1] 落在第一帧、[6][7][8] 落在最后一帧。第二遍读代码卡住时,用这张图定位「我读到第几行了」:

如果把 Bean 的 scope 改成 prototype,你会发现 [6][7][8] 三行再也不会打印。这不是 Bug,而是下一节要讲的销毁对称性。
这是面试最爱追问的一题。答案明确:在"初始化后置处理"阶段,由 AbstractAutoProxyCreator#postProcessAfterInitialization 生成。
// AbstractAutoProxyCreator(AbstractAdvisorAutoProxyCreator 的父类)@Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); // 需要就包装成代理 } } return bean;}wrapIfNecessary会判断当前 Bean 是否匹配某个通知(@Transactional、@Cacheable等),匹配就让ProxyFactory生成 JDK 或 CGLIB 代理- 代理是在原对象初始化完成后才"套"上去的,所以代理内部的
target指向的是那个完整的、已经注入好依赖的对象 - 结果就是:你注入到别处的、从容器取出的,都是代理,而不是你
new出来的那个原始对象
坑:这解释了一个新手常见困惑——"我用 getClass() 打印出来的类是 UserService$$EnhancerBySpringCGLIB,我明明写的是 UserService"。因为代理对象就是被"掉包"后放进单例池的那一个。
销毁阶段处处体现"对称",也处处是坑。四条现象摆在面前,成因却只有一个(容器有没有持有这个引用)——先把这局配完,再看下面那三条结论会轻松得多:
- 为什么原型不销毁?因为容器创建完原型后立刻撒手——它不保存引用,自然无从销毁。需要释放资源就自己
try-with-resources - 为什么单例逆序销毁?
DefaultSingletonBeanRegistry用disposableBeans记录顺序,destroySingletons()反向遍历 @PreDestroy没执行,按"是否close()→ 是否单例 → 是否被创建"三步排查
逆序这件事光看文字很难有实感,动画把「谁先建、谁先拆」放了一遍,注意第 ③④ 两帧的顺序:

在 Spring Boot 里,@PreDestroy 依赖 JVM 正常退出时触发 close()。如果进程被 kill -9 强制杀掉,销毁回调不会执行——关键数据必须靠 try/finally 或外部兜底,别只押在 @PreDestroy 上。
把「单例 vs 原型」这条最容易记反的分歧单独画成一张对照图——左边是走一遍就退休入池的单例,右边是每次取用都重跑全流程、容器拿完就撒手的原型:

单例像公司里的公共打印机——装机时配好一次(走完 ①~⑦),此后所有人共用它,公司(容器)负责它的保养和报废;原型像外卖一次性餐盒——每点一份发一个新的,商家不会上门回收,你随手扔了也没人管。所以「一次性餐盒要不要冲干净再扔」(原型的资源释放)永远是你自己的事。
想找出"启动慢的头号嫌疑人"?一个自定义 BeanPostProcessor 就够了——它正好卡在生命周期的两端:
package com.example.lifecycle;import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;import org.springframework.stereotype.Component;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;@Componentpublic class TimingBeanPostProcessor implements InstantiationAwareBeanPostProcessor { private final Map<String, Long> startTimes = new ConcurrentHashMap<>(); // 生命周期起点:实例化之前 @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) { startTimes.put(beanName, System.nanoTime()); return null; // 返回 null = 不接管创建,继续走标准流程 } // 生命周期终点:初始化之后 @Override public Object postProcessAfterInitialization(Object bean, String beanName) { Long start = startTimes.remove(beanName); if (start != null) { long costMs = (System.nanoTime() - start) / 1_000_000; if (costMs > 50) { System.out.println("[slow] " + beanName + " 创建耗时 " + costMs + "ms"); } } return bean; }}postProcessBeforeInstantiation返回null表示"我不插手,你按正常流程创建"——这是它区别于"返回代理直接短路"的关键postProcessAfterInitialization是生命周期最后一环,此刻bean已是最终形态(可能已是代理)- 只打印超过 50ms 的 Bean,避免刷屏;把它跑一遍,启动优化清单就出来了
说明:这类"横切"逻辑正是 AOP 的雏形——BeanPostProcessor 是 Spring 留给你的、插手每个 Bean 生命周期的官方手段。
| 问题 | 答案 |
|---|---|
@PostConstruct 与 afterPropertiesSet 谁先? | @PostConstruct 先。前者由 CommonAnnotationBeanPostProcessor(一个 BPP)在 before-init 触发,后者由 invokeInitMethods 在更晚触发 |
| 构造器里能拿到注入的依赖吗? | 字段/setter 注入不行——构造时依赖还没填;构造器注入可以,因为它就是靠构造参数传进来的 |
@PostConstruct先于afterPropertiesSet,是因为"BPP 回调"整体早于"InitializingBean回调"- 想在构造器里做依赖相关的事,唯一正确姿势是构造器注入:
public UserService(UserDao dao),dao在构造器执行前已就位 - 若是
@Autowired字段注入,构造器里访问它只会得到null
在 @PostConstruct 里调用一个带 @Transactional 的同类方法(自调用),事务不会生效。原因很直接:@PostConstruct 运行在"初始化前置",而 AOP 代理直到"初始化后置"才生成(见第四节)。此刻方法调用走的是原始对象,this.xxx() 绕过了代理,通知链根本没机会介入。同理,@Async、@Cacheable 在 @PostConstruct 中的自调用也会失效。
需要"启动时做一次带事务/异步的处理",正确做法是把它挪到第 12 步的 ContextRefreshedEvent 监听里,或注入自身的代理(ObjectProvider<MyService> / @Lazy),让调用经过代理。
下面这个演示把八阶段做成了可交互的时间线,切换「原型」选项还能直观看到销毁阶段的差异:
再切到 destroy 参数单独看销毁三连(@PreDestroy → destroy → destroyMethod)的打印顺序,确认它和初始化严格对称。
光知道「八个阶段」还不够——得知道这八个阶段是被谁推进的。答案在上一篇:refresh() 的第 11 步 finishBeanFactoryInitialization 会遍历所有非懒加载单例,逐个把它们推完整条流水线。下面这个实验把十二步摊开,盯住第 11 步看 Bean 是怎么一批批冒出来的:
第八节说「AOP 代理诞生于初始化后置」。那个「后置」究竟是谁在做包装?就是自动代理创建器(AutoProxyCreator——一种专门负责「要不要把这个 Bean 包成代理」的 BeanPostProcessor)。下面的实验把它拦路判断的过程摊开:先看候选判定,再看它在后置处理里怎么拦截:
最后回到「属性填充」这一步本身。依赖是靠哪种方式注入的,直接决定了对象在第 ① 步之后是不是「半成品」——构造器注入出生即完整,字段注入则要等到第 ② 步才补齐:
第五节那条「原型不销毁」的分歧,本质是作用域问题。这个实验把四种作用域的实例计数和谁来销毁放在一起对照,切到「数一数有几个」你会看到计数器随每次取用的变化,切到「谁来销毁」就能看到销毁日志只出现一次:
还有一个「什么时候才真的出生」的特例:FactoryBean 的定义登记的是工厂,容器取出的却是产品——那么产品是哪一刻被造出来的?切到「什么时候真正产出」,对照上面单步台第 ⑨ 步那句 getObjectForBeanInstance:
这台控制台连的是浏览器里那个真容器。它的 beans / di / query 都由内核当场算,所以下面每一条命令都是在验证本篇一句结论:
lab beanscope destroy 和 lab beanscope probe 要连着敲才有对比——前者告诉你「销毁日志少了一整段」,后者告诉你「实例数量一直在涨」。第五节那张配对题的答案,全部出自这两条命令的输出。
新手最容易问的两句话是:「为什么我的应用启动要 40 秒?」和「为什么加了 @Scope("prototype") 之后 @PreDestroy 不打印了?」这两个问题的答案都不在代码里,而在作用域与初始化时机的选择上。下面这个沙盘把「作用域 + 是否懒加载」合成一个三档开关,切一档就能同屏看到启动耗时、取用耗时与销毁日志三处变化:
启动耗时:38s# 第 11 步 preInstantiateSingletons 一口气 new 了 1240 个单例[slow] dataSource 创建耗时 2100ms ← 罪魁祸首之一getBean(userService) 耗时:0.01ms(命中 singletonObjects)容器关闭:@PreDestroy ✔ 全部执行
沙盘里的数字是示意,但结论是真的——单例的创建成本集中在启动那一刻,原型的成本摊在每一次取用。选错方向的两种典型症状正好相反:一个是「启动慢得要死」,一个是「跑着跑着内存涨、销毁日志一行没有」。
先来一道热身题,考的就是第五节那张对称表:
再来一道综合题,把第三、四、七、八节串起来:
Bean 的一生只有八个阶段、五类回调,记住三条主线即可——顺序:实例化 → 填充 → Aware → 初始化前后 → 入池 → 销毁;先后:@PostConstruct 先于 afterPropertiesSet 再先于 initMethod,销毁侧同理;时机:AOP 代理诞生于"初始化后置",所以任何依赖代理的逻辑都不能放在 @PostConstruct。把这三条刻进脑子,生命周期相关的题就不再是背诵,而是推理。
下面每一行的「报错原文」都可以整段复制去搜索,别意译、别缩写。新手在这三个地方最容易卡住:注解没生效、构造器里拿到 null、原型不销毁。
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
@PostConstruct 写了却一行日志都不打(也没有报错) | Spring Boot 3 / JDK 9+ 之后包名从 javax.annotation 变成 jakarta.annotation,你 import 错了包,容器根本认不得这个注解 | 检查 import:Boot 3 用 import jakarta.annotation.PostConstruct;,Boot 2 用 javax.annotation.PostConstruct。IDEA 里按 Alt+Enter 重导包即可 | 本篇第二节 · #16 Hello Spring Boot |
java.lang.NoClassDefFoundError: javax/annotation/PostConstruct | 纯 Java SE 项目或 Spring 5 + JDK 11 的精简镜像里没有那个注解类——注解类型本身不在 classpath 上 | Boot 2 加 org.apache.tomcat:annotations-api,Boot 3 加 jakarta.annotation:jakarta.annotation-api(一般已被 starter 传递带入) | 第二档练习 · #36 打包部署 |
NullPointerException: Cannot invoke "com.example.UserDao.findById(Long)" because "this.dao" is null | 你在无参构造器里用了字段注入的依赖。构造器属于第 ① 步,属性填充是第 ② 步,此刻 dao 还没被塞进来 | 改成构造器注入:public UserService(UserDao dao) { this.dao = dao; },或把逻辑挪到 @PostConstruct | 本篇第七节 · #6 IoC 与 DI |
加了 @Scope("prototype") 之后 @PreDestroy / destroy() 再也不打印 | 容器创建完原型就撒手,不保存引用,也就无从回调——这是设计如此,不是 Bug | 自己收口:让原型实现 AutoCloseable 并用 try-with-resources 取用,或改回单例 + 无状态设计 | 本篇第五节 · 上方沙盘 |
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService': Invocation of init method failed; nested exception is java.lang.IllegalStateException: ... | 你的初始化回调(@PostConstruct / afterPropertiesSet / initMethod)里抛了异常,容器把它包成创建失败 | 只看 nested exception 后面那一句——真凶在你的校验逻辑里,而不是 Spring。启动期校验失败本来就该让进程起不来 | 本篇第三节 · #8 refresh() 十二步 |
APPLICATION FAILED TO START + The dependencies of some of the beans in the application context form a cycle | 两个 Bean 在初始化阶段互相等待(循环依赖),而构造器注入无法提供早期引用 | 顺着日志里的循环链条拆开其中一条依赖,或改用 @Lazy 注入其中一方 | #10 循环依赖 · #6 IoC 与 DI |
这一节的六条里有五条都不会以「红字异常」的形式出现,而是静默不执行(不打日志、不回调)。排查生命周期问题时,先把 logging.level.org.springframework.beans=DEBUG 打开,看容器到底有没有为这个 Bean 走到对应阶段——大多数「注解失效」其实是「压根没注册成功」。
表格里第五条是唯一会大声报错的那一条,而它的真凶恰好藏在生命周期的一格子里。先别看答案,点出凶手帧:
加了个「启动时校验配置」的 @PostConstruct,本地跑得好好的,测试环境一启动就崩;报错很长,同事第一反应是「数据库连不上」。
目标:用一个能直接跑的最小工程,把「构造器 → Aware → @PostConstruct → afterPropertiesSet → initMethod → 关闭三连」全打出来,并亲眼看到原型少掉最后三行。
第一步,建一个 Maven 工程 pom.xml(只要 spring-context 与 jakarta 注解):
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.8</version> </dependency> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>3.0.0</version> </dependency></dependencies>第二步,写这个 Bean(src/main/java/com/example/demo/LifeBean.java):
package com.example.demo;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.springframework.beans.factory.BeanNameAware;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Value;public class LifeBean implements BeanNameAware, InitializingBean, DisposableBean { @Value("${app.tag:untagged}") private String tag; // 字段注入:构造器里读不到它 public LifeBean() { System.out.println("1 构造器 | tag = " + tag); } @Override public void setBeanName(String name) { System.out.println("2 BeanNameAware | name = " + name); } @PostConstruct public void initAnnotation() { System.out.println("3 @PostConstruct | tag = " + tag); } @Override public void afterPropertiesSet() { System.out.println("4 afterPropertiesSet"); } public void customInit() { System.out.println("5 initMethod"); } @PreDestroy public void preDestroy() { System.out.println("6 @PreDestroy"); } @Override public void destroy() { System.out.println("7 DisposableBean#destroy"); } public void customDestroy() { System.out.println("8 destroyMethod"); }}第三步,配置类与启动器(同一个包下新建 Main.java):
package com.example.demo;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration@PropertySource(value = "classpath:app.properties", ignoreResourceNotFound = true)public class Main { @Bean(initMethod = "customInit", destroyMethod = "customDestroy") public LifeBean singletonLife() { return new LifeBean(); } public static void main(String[] args) { try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Main.class)) { System.out.println("---- 容器就绪 ----"); } System.out.println("---- 容器已关闭 ----"); }}运行 main,预期输出(顺序必须逐行对上,否则说明你的 import 用错了包):
1 构造器 | tag = null2 BeanNameAware | name = singletonLife3 @PostConstruct | tag = untagged4 afterPropertiesSet5 initMethod---- 容器就绪 ----6 @PreDestroy7 DisposableBean#destroy8 destroyMethod---- 容器已关闭 ----验收清单:① 你能指出第 1 行为什么打印 null 而第 3 行有值;② 把 @PostConstruct 的 import 换成一个不存在的包,程序仍然启动成功但少打一行——你就亲手复现了「静默失效」;③ 说得出 6/7/8 三行分别由谁调用。
只改一个地方,观察结论完全不同:
- 给
singletonLife()加上@Scope("prototype")(记得import org.springframework.context.annotation.Scope;)。你会观察到:1~5照常打印,6/7/8一行都没有;再调两次ctx.getBean(LifeBean.class),1~5会完整重复两遍。这就是第五节和对照图讲的「容器拿完就撒手」。 - 把
@PostConstruct方法里加一行System.out.println(this.hashCode()),同时把该 Bean 换成带@Transactional的类并在后置处理里比较两者地址。你会观察到:@PostConstruct里的对象地址与最终getBean拿到的不同——代理是在它之后才被套上的。 - 把
customInit()里改成throw new IllegalStateException("配置校验失败")。你会观察到:整个启动直接失败,异常被包成BeanCreationException: Error creating bean with name 'singletonLife': Invocation of init method failed,而且6/7/8依然会执行——这正是上一篇讲的「不留半成品」。
提示:做完第 1 条再回头看第十一节的沙盘,把开关切到 prototype,两边结论应当完全对得上。
给自己写一个「生命周期探针」组件,以后任何一个项目的启动慢、资源泄漏都能用它量出来。
需求:
- 一个
LifecycleProbe实现InstantiationAwareBeanPostProcessor,记录每个 Bean 的「实例化时刻」与「初始化完成时刻」 - 额外区分三类信息:是否走了
@PostConstruct(可用CommonAnnotationBeanPostProcessor的存在与否推断)、是否被代理过(比较postProcessAfterInitialization返回对象的getClass()与入参是否相同) - 容器关闭时(配合
SmartLifecycle或ContextClosedEvent监听)打印一张汇总表:Top 10 耗时 Bean、被代理的 Bean 数量、从未被创建的懒加载 Bean 名单 - 全部逻辑不许修改业务 Bean 一行代码
验收清单:① 在一个空 Boot 工程跑,输出的 Top 10 至少包含 dataSource 之类的自动配置 Bean;② 故意把一个 Bean 标成 prototype 并使用三次,表格里它的创建次数应为 3;③ 给某个 Bean 加 @Transactional 后,它出现在「被代理」计数里;④ 探针自身不会拖慢启动超过 5%(用第一次的总耗时对比验证)。
不看上文,按顺序说出八个阶段的名字,并指出「属性填充」和「初始化前置」之间隔着哪一步回调。
@PostConstruct 到底是谁调用的?为什么它算「初始化前置」而不是一个独立阶段?
为什么构造器里读字段注入的值一定是 null,而构造器注入就不是?两种写法各自的「出生即完整」程度差别在哪?
原型 Bean 的销毁为什么不执行?如果确实需要释放资源,你有哪两条正规出路?
AOP 代理诞生于哪一步?这一步的时机如何同时解释了「@PostConstruct 里自调用 @Transactional 失效」和「getClass() 打印出 CGLIB 类名」两件事?
先进货(实例化)、再摆货(填充)、挂牌营业前必体检(初始化)、发工牌在最后(代理)——原型不发退休证。