Bean 生命周期完全指南:从实例化到销毁的每一步

bee2026-10-0862 分钟0 次阅读
一个 Bean 从生到死要经过 8 个阶段、5 类回调。逐阶段给出源码顺序、可运行的验证代码,并回答那个经典问题:AOP 代理到底在哪一步生成?
1 / 124
小节
〇、30 秒看懂
2 / 124

一个 Java 对象只要 new 出来就存在了,但「存在」和「能被安全使用」是两回事:构造器跑完时,你 @Autowired 的字段可能还是 null;依赖填好了,也许还要校验配置、预热缓存、打开连接;程序退出前还得把这些资源关掉。Spring 把「从诞生到能用、再到退休」这整段路标准化成了八个阶段,并在每个阶段留了口子让你插自己的代码——这就是 Bean 生命周期。

3 / 124

先给五个词一句话解释(后面全文都用得上):

4 / 124
  • Bean:不是 Spring 自己造的对象,而是「交给容器创建和管理的那个对象」,通常就是你写的一个普通 Java 类实例
  • 容器:那个帮你 new 对象、传依赖、管生老病死的「大管家」对象(ApplicationContext),你用 getBean 找它要东西
  • 反射:程序在运行时「看着类名去调构造器和方法」的技术——容器没写死你的类,它是靠反射把你的对象造出来的
  • 钩子:框架在自己的流程里预留的「插槽」,你把方法放进去,容器走到那一步就回调你
  • @PostConstruct:一个注解,意思是「依赖都填好了,请先执行我这个方法再对外提供服务」
5 / 124
类比

把一个 Bean 想成一个人的一生:出生(实例化,构造函数那一刻)、长身体(属性填充,把爸妈给的营养吸收进来)、上户口领身份证(Aware 回调,知道「我叫什么、住在哪个容器」)、入学前的体检(初始化,配置对不对、能不能上岗)、毕业找工作时被包装了一层简历与工牌(AOP 代理,别人见到的其实是「工牌版」的你)、工作几十年(就绪入池,反复被调用)、最后退休清算(销毁回调,把占用的资源还回去)。

6 / 124
架构图
图 · 初始化三连的顺序与出处
图 · 初始化三连的顺序与出处
7 / 124

上图从左到右就是一个单例 Bean 真实的「成长时间线」:越靠左,能读到的注入依赖越少。第三节会把它变成可运行的日志。

8 / 124

学完这一篇,你应该能回答三个问题:

9 / 124
  • @PostConstruct、afterPropertiesSet、initMethod 到底谁先谁后?为什么是这个顺序?
  • 为什么原型(prototype)Bean 的 @PreDestroy 从来不打印?容器不是答应帮我管理吗?
  • AOP 代理是在哪一步生成的?为什么它决定了「构造器里能不能用 @Transactional」?
10 / 124
小节
一、生命周期全景:一个 Bean 的八个人生阶段
11 / 124

先问一个"为什么":为什么需要生命周期回调?因为一个对象被"创建"出来,和它"可以安全地被使用",是两回事。构造器结束时对象只是有了内存,依赖还没注入;依赖注入完又可能需要校验配置、预热缓存、打开连接;使用完毕还需释放资源。Spring 把这些"插缝"的时机标准化成了八个人生阶段。

12 / 124
架构图
图 1 · Bean 生命周期环形图
图 1 · Bean 生命周期环形图
13 / 124

八个阶段依次是:实例化 → 属性填充 → Aware 回调 → 初始化前置处理 → 初始化 → 初始化后置处理 → 就绪入池 → 销毁回调。注意这是"环形"而非"直线"——单例对象走完一圈就常驻容器,直到容器关闭才走最后的销毁阶段。

14 / 124
说明

本文聚焦单例的生命周期。原型(prototype)Bean 只走到"就绪"就交给调用方,容器不再负责它的销毁——这一点会在第五节展开。

15 / 124
小节
二、每阶段的触发点与源码位置
16 / 124
原理动画
动图 · 八阶段实操演示
动图 · 八阶段实操演示
17 / 124

把"谁触发、在哪触发、用来干嘛"一次性列清楚:

18 / 124
对照表
阶段触发的接口 / 注解源码位置典型用途
实例化构造器 / 工厂方法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关闭连接、刷盘、释放线程池
19 / 124
要点

@PostConstruct 并不是一个独立阶段,它本质上是一个 BeanPostProcessor 在"初始化前置"阶段的回调。理解这一点,才能解释第七节那个经典坑。

20 / 124

表格写的是「谁触发」,而新手真正卡住的地方是「我这一格能读到什么」:构造器里能不能用 @Autowired 的字段?@PostConstruct 里能不能拿到代理?八站点一遍,每站给一句实话:

21 / 124
交互图解
回路八个阶段,每一站你能读到什么1 / 8
一格一格点,重点在第 ① 站和第 ⑥ 站——一个字段还是 null,一个代理还没套上
→
→
→
→
→
→
→
↻
Bean 一生
① 实例化
反射调完构造器,对象有内存了,但 @Autowired 字段全是 null。在这里读 this.dao 就是第七节那条 NPE 的现场。唯一例外:构造器注入的参数已经就位,因为它就是入参。
全部看懂了三句实话:第 ① 站别碰依赖,第 ⑤ 站只管自己,第 ⑥ 站之后才有代理。
22 / 124
小节
2.1 八个阶段是被谁推进的
23 / 124

上面那张环形图回答「有哪些站」,这一台单步台回答「谁在推」。左边是 getBean → doGetBean → doCreateBean 的真实骨架,右边同步刷新变量与调用栈。连点下一步,盯两件事:第 ② 步的缓存命中,和第 ⑧ 步入池那一行的位置:

24 / 124
单步调试台
单步台doGetBean 与 getSingleton:一次取用的两条路1 / 9
点满九步:第 ② 步决定快慢,第 ⑧ 步决定它以后是不是单例
被调试的代码
1Object getBean("userService") // 你的代码只写了这一行
2Object cached = getSingleton(beanName); // ① 先问一级缓存:已经有了吗
3if (cached != null) return getObjectForBeanInstance(cached); // ② 命中就短路返回
4RootBeanDefinition mbd = getMergedBeanDefinition(beanName); // ③ 未命中:读档案
5Object obj = new UserService(); // ④ doCreateBean:实例化
6populateBean(obj, mbd); // ⑤ 属性填充:@Autowired 生效
7initializeBean(obj, beanName, mbd); // ⑥ Aware + 初始化前后 + 代理
8addSingleton(beanName, obj); // ⑦ 成品进一级缓存
9return getSingleton(beanName); // ⑧ 出池,交给调用方
此刻的变量
beanNameuserService
singletonObjects{orderService=..., ...}
allowCircularReferencesfalse
调用栈
1getBean
2doGetBean
1第一行就是快慢的分水岭:容器先去那张叫 singletonObjects 的表里查(第 ⑦ 步入池的成果)。这一格也是本篇全部内容的坐标——所谓生命周期,就是从第 ④ 步走到第 ⑦ 步的那段路。
25 / 124

第一次与第二次这两条路的差别,动画再放一遍。注意第 ④ 帧的「命中缓存」与第 ⑤ 帧的「原型从不入池」——它俩共同决定了第五节所有关于销毁的现象:

26 / 124
原理动画
动图 · 第一次走完八站,第二次只查缓存
动图 · 第一次走完八站,第二次只查缓存
27 / 124

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

28 / 124
原理动画
动图 · 第一次走完八站,第二次只查缓存
动图 · 第一次走完八站,第二次只查缓存
29 / 124
小节
三、可运行验证:亲眼看着它走完一生
30 / 124

空谈源码不如跑一遍。下面这个 Bean 实现了三种回调接口,并叠加注解回调:

31 / 124
java
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)");    }}
32 / 124

配置类与启动代码:

33 / 124
java
@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("[*] 容器已关闭");    }}
34 / 124

控制台输出与阶段的对应关系:

35 / 124
代码对照
代码text
[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 回调早于任何初始化回调
36 / 124

把上面这九行日志倒回去对照动图看一遍——每一帧正好对应一行输出,[1] 落在第一帧、[6][7][8] 落在最后一帧。第二遍读代码卡住时,用这张图定位「我读到第几行了」:

37 / 124
原理动画
动图 · 八阶段与日志逐行对照
动图 · 八阶段与日志逐行对照
38 / 124
提示

如果把 Bean 的 scope 改成 prototype,你会发现 [6][7][8] 三行再也不会打印。这不是 Bug,而是下一节要讲的销毁对称性。

39 / 124
小节
四、AOP 代理到底在哪一步生成?
40 / 124

这是面试最爱追问的一题。答案明确:在"初始化后置处理"阶段,由 AbstractAutoProxyCreator#postProcessAfterInitialization 生成。

41 / 124
代码对照
代码java
// 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"。因为代理对象就是被"掉包"后放进单例池的那一个。

42 / 124
小节
五、销毁阶段的对称性:为什么它有,它没有
43 / 124

销毁阶段处处体现"对称",也处处是坑。四条现象摆在面前,成因却只有一个(容器有没有持有这个引用)——先把这局配完,再看下面那三条结论会轻松得多:

44 / 124
配对闯关
闯关销毁现象配成因已配对 0/6 · 配错 0
左边是你在控制台看到的实况,右边是它真正的原因
先点左边一个
45 / 124
  • 为什么原型不销毁?因为容器创建完原型后立刻撒手——它不保存引用,自然无从销毁。需要释放资源就自己 try-with-resources
  • 为什么单例逆序销毁?DefaultSingletonBeanRegistry 用 disposableBeans 记录顺序,destroySingletons() 反向遍历
  • @PreDestroy 没执行,按"是否 close() → 是否单例 → 是否被创建"三步排查
46 / 124

逆序这件事光看文字很难有实感,动画把「谁先建、谁先拆」放了一遍,注意第 ③④ 两帧的顺序:

47 / 124
原理动画
动图 · 销毁为什么必须逆序
动图 · 销毁为什么必须逆序
48 / 124
警告

在 Spring Boot 里,@PreDestroy 依赖 JVM 正常退出时触发 close()。如果进程被 kill -9 强制杀掉,销毁回调不会执行——关键数据必须靠 try/finally 或外部兜底,别只押在 @PreDestroy 上。

49 / 124

把「单例 vs 原型」这条最容易记反的分歧单独画成一张对照图——左边是走一遍就退休入池的单例,右边是每次取用都重跑全流程、容器拿完就撒手的原型:

50 / 124
架构图
图 · 单例 vs 原型:一生走几次
图 · 单例 vs 原型:一生走几次
51 / 124
类比

单例像公司里的公共打印机——装机时配好一次(走完 ①~⑦),此后所有人共用它,公司(容器)负责它的保养和报废;原型像外卖一次性餐盒——每点一份发一个新的,商家不会上门回收,你随手扔了也没人管。所以「一次性餐盒要不要冲干净再扔」(原型的资源释放)永远是你自己的事。

52 / 124
小节
六、实战:用 BeanPostProcessor 统计创建耗时
53 / 124

想找出"启动慢的头号嫌疑人"?一个自定义 BeanPostProcessor 就够了——它正好卡在生命周期的两端:

54 / 124
代码对照
代码java
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 生命周期的官方手段。

55 / 124
小节
七、面试连环问:两个高频对比
56 / 124
对照表
问题答案
@PostConstruct 与 afterPropertiesSet 谁先?@PostConstruct 先。前者由 CommonAnnotationBeanPostProcessor(一个 BPP)在 before-init 触发,后者由 invokeInitMethods 在更晚触发
构造器里能拿到注入的依赖吗?字段/setter 注入不行——构造时依赖还没填;构造器注入可以,因为它就是靠构造参数传进来的
57 / 124
  • @PostConstruct 先于 afterPropertiesSet,是因为"BPP 回调"整体早于"InitializingBean 回调"
  • 想在构造器里做依赖相关的事,唯一正确姿势是构造器注入:public UserService(UserDao dao),dao 在构造器执行前已就位
  • 若是 @Autowired 字段注入,构造器里访问它只会得到 null
58 / 124
小节
八、坑:在 @PostConstruct 里依赖代理能力会失效
59 / 124
坑

在 @PostConstruct 里调用一个带 @Transactional 的同类方法(自调用),事务不会生效。原因很直接:@PostConstruct 运行在"初始化前置",而 AOP 代理直到"初始化后置"才生成(见第四节)。此刻方法调用走的是原始对象,this.xxx() 绕过了代理,通知链根本没机会介入。同理,@Async、@Cacheable 在 @PostConstruct 中的自调用也会失效。

60 / 124
提示

需要"启动时做一次带事务/异步的处理",正确做法是把它挪到第 12 步的 ContextRefreshedEvent 监听里,或注入自身的代理(ObjectProvider<MyService> / @Lazy),让调用经过代理。

61 / 124
小节
九、上手体验:亲手跑一遍 Bean 的一生
62 / 124

下面这个演示把八阶段做成了可交互的时间线,切换「原型」选项还能直观看到销毁阶段的差异:

63 / 124
内核实验
TeaVM亲手跑一遍 Bean 的一生未启动
切换「原型」选项,观察销毁阶段的差异
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
64 / 124

再切到 destroy 参数单独看销毁三连(@PreDestroy → destroy → destroyMethod)的打印顺序,确认它和初始化严格对称。

65 / 124

光知道「八个阶段」还不够——得知道这八个阶段是被谁推进的。答案在上一篇:refresh() 的第 11 步 finishBeanFactoryInitialization 会遍历所有非懒加载单例,逐个把它们推完整条流水线。下面这个实验把十二步摊开,盯住第 11 步看 Bean 是怎么一批批冒出来的:

66 / 124
内核实验
TeaVM生命周期挂在 refresh 的哪一步上未启动
选「只盯关键四步」,看 finishBeanFactoryInitialization 如何把所有单例推完八阶段;再切「某一步失败会怎样」看半成品怎么回收
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
67 / 124

第八节说「AOP 代理诞生于初始化后置」。那个「后置」究竟是谁在做包装?就是自动代理创建器(AutoProxyCreator——一种专门负责「要不要把这个 Bean 包成代理」的 BeanPostProcessor)。下面的实验把它拦路判断的过程摊开:先看候选判定,再看它在后置处理里怎么拦截:

68 / 124
内核实验
TeaVM代理是在哪一秒套上去的未启动
先选「候选判定」看清哪些 Bean 会被包装,再选「在后置处理里拦截」对应本文第八节的时机
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
69 / 124

最后回到「属性填充」这一步本身。依赖是靠哪种方式注入的,直接决定了对象在第 ① 步之后是不是「半成品」——构造器注入出生即完整,字段注入则要等到第 ② 步才补齐:

70 / 124
内核实验
TeaVM三种注入的装配时序未启动
依次切 constructor / setter / field,对照第三节日志看 [1] 与 [2] 之间发生了什么
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
71 / 124

第五节那条「原型不销毁」的分歧,本质是作用域问题。这个实验把四种作用域的实例计数和谁来销毁放在一起对照,切到「数一数有几个」你会看到计数器随每次取用的变化,切到「谁来销毁」就能看到销毁日志只出现一次:

72 / 124
内核实验
TeaVM四种作用域:几个实例、谁负责收尾未启动
先用计数看 singleton 与 prototype 的差别,再切「谁来销毁」验证第五节那条规则
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
73 / 124

还有一个「什么时候才真的出生」的特例:FactoryBean 的定义登记的是工厂,容器取出的却是产品——那么产品是哪一刻被造出来的?切到「什么时候真正产出」,对照上面单步台第 ⑨ 步那句 getObjectForBeanInstance:

74 / 124
内核实验
TeaVMFactoryBean 的产品到底在哪一刻被创建未启动
注意产出发生在第一次按名字取产品时,不是容器 refresh 阶段;再切「加 & 拿工厂」看两种取法的区别
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
75 / 124
小节
9.1 换成命令行:把生命周期敲出来
76 / 124

这台控制台连的是浏览器里那个真容器。它的 beans / di / query 都由内核当场算,所以下面每一条命令都是在验证本篇一句结论:

77 / 124
内核控制台
78 / 124
提示

lab beanscope destroy 和 lab beanscope probe 要连着敲才有对比——前者告诉你「销毁日志少了一整段」,后者告诉你「实例数量一直在涨」。第五节那张配对题的答案,全部出自这两条命令的输出。

79 / 124
小节
十一、沙盘:生命周期到底慢在哪
80 / 124

新手最容易问的两句话是:「为什么我的应用启动要 40 秒?」和「为什么加了 @Scope("prototype") 之后 @PreDestroy 不打印了?」这两个问题的答案都不在代码里,而在作用域与初始化时机的选择上。下面这个沙盘把「作用域 + 是否懒加载」合成一个三档开关,切一档就能同屏看到启动耗时、取用耗时与销毁日志三处变化:

81 / 124
沙盘
沙盘作用域 × 初始化时机沙盘
运行结果
启动耗时:38s
# 第 11 步 preInstantiateSingletons 一口气 new 了 1240 个单例
[slow] dataSource 创建耗时 2100ms ← 罪魁祸首之一
getBean(userService) 耗时:0.01ms(命中 singletonObjects)
容器关闭:@PreDestroy ✔ 全部执行
默认姿势:启动时代价换运行期速度。想提速就把重对象改成 @Lazy。
82 / 124
说明

沙盘里的数字是示意,但结论是真的——单例的创建成本集中在启动那一刻,原型的成本摊在每一次取用。选错方向的两种典型症状正好相反:一个是「启动慢得要死」,一个是「跑着跑着内存涨、销毁日志一行没有」。

83 / 124
小节
十二、随堂自测
84 / 124

先来一道热身题,考的就是第五节那张对称表:

85 / 124
随堂自测
随堂自测某个 Bean 打了 @PreDestroy 方法,但关闭应用时控制台一行都没输出。下列哪项**不可能**是原因?
先自己选一个,选中立刻告诉你对不对
86 / 124

再来一道综合题,把第三、四、七、八节串起来:

87 / 124
随堂自测
随堂自测一个 Bean 里同时有无参构造器、@Autowired 字段 dao、@PostConstruct init()、InitializingBean#afterPropertiesSet()、@Transactional 注解。下列说法正确的是?
先自己选一个,选中立刻告诉你对不对
88 / 124
小节
十、这段逻辑该放哪个生命周期回调?
89 / 124
决策
决策你有一段逻辑,需要**读取注入进来的配置项做校验**,校验不通过就抛异常阻止应用启动。应该放在哪里?
90 / 124
总结

Bean 的一生只有八个阶段、五类回调,记住三条主线即可——顺序:实例化 → 填充 → Aware → 初始化前后 → 入池 → 销毁;先后:@PostConstruct 先于 afterPropertiesSet 再先于 initMethod,销毁侧同理;时机:AOP 代理诞生于"初始化后置",所以任何依赖代理的逻辑都不能放在 @PostConstruct。把这三条刻进脑子,生命周期相关的题就不再是背诵,而是推理。

91 / 124
小节
十三、常见报错速查
92 / 124

下面每一行的「报错原文」都可以整段复制去搜索,别意译、别缩写。新手在这三个地方最容易卡住:注解没生效、构造器里拿到 null、原型不销毁。

93 / 124
对照表
报错原文(片段)真实原因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
94 / 124
提示

这一节的六条里有五条都不会以「红字异常」的形式出现,而是静默不执行(不打日志、不回调)。排查生命周期问题时,先把 logging.level.org.springframework.beans=DEBUG 打开,看容器到底有没有为这个 Bean 走到对应阶段——大多数「注解失效」其实是「压根没注册成功」。

95 / 124

表格里第五条是唯一会大声报错的那一条,而它的真凶恰好藏在生命周期的一格子里。先别看答案,点出凶手帧:

96 / 124
报错急救
报错急救BeanCreationException: Invocation of init method failed

加了个「启动时校验配置」的 @PostConstruct,本地跑得好好的,测试环境一启动就崩;报错很长,同事第一反应是「数据库连不上」。

APPLICATION FAILED TO START
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'reportExporter' defined in class path resource [com/example/AppConfig.class]: Invocation of init method failed; nested exception is java.lang.NullPointerException: Cannot invoke "com.example.ConfigSnapshot.storageBucket()" because "this.snapshot" is null
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at com.example.report.ReportExporter.init(ReportExporter.java:47)
Caused by: java.lang.NullPointerException: Cannot invoke "com.example.ConfigSnapshot.storageBucket()" because "this.snapshot" is null
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
97 / 124
小节
十四、动手练习
98 / 124
小节
第一档 · 照做
99 / 124

目标:用一个能直接跑的最小工程,把「构造器 → Aware → @PostConstruct → afterPropertiesSet → initMethod → 关闭三连」全打出来,并亲眼看到原型少掉最后三行。

100 / 124

第一步,建一个 Maven 工程 pom.xml(只要 spring-context 与 jakarta 注解):

101 / 124
xml
<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>
102 / 124

第二步,写这个 Bean(src/main/java/com/example/demo/LifeBean.java):

103 / 124
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");    }}
104 / 124

第三步,配置类与启动器(同一个包下新建 Main.java):

105 / 124
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("---- 容器已关闭 ----");    }}
106 / 124

运行 main,预期输出(顺序必须逐行对上,否则说明你的 import 用错了包):

107 / 124
text
1 构造器          | tag = null2 BeanNameAware   | name = singletonLife3 @PostConstruct  | tag = untagged4 afterPropertiesSet5 initMethod---- 容器就绪 ----6 @PreDestroy7 DisposableBean#destroy8 destroyMethod---- 容器已关闭 ----
108 / 124

验收清单:① 你能指出第 1 行为什么打印 null 而第 3 行有值;② 把 @PostConstruct 的 import 换成一个不存在的包,程序仍然启动成功但少打一行——你就亲手复现了「静默失效」;③ 说得出 6/7/8 三行分别由谁调用。

109 / 124
小节
第二档 · 变体
110 / 124

只改一个地方,观察结论完全不同:

111 / 124
  1. 给 singletonLife() 加上 @Scope("prototype")(记得 import org.springframework.context.annotation.Scope;)。你会观察到:1~5 照常打印,6/7/8 一行都没有;再调两次 ctx.getBean(LifeBean.class),1~5 会完整重复两遍。这就是第五节和对照图讲的「容器拿完就撒手」。
  2. 把 @PostConstruct 方法里加一行 System.out.println(this.hashCode()),同时把该 Bean 换成带 @Transactional 的类并在后置处理里比较两者地址。你会观察到:@PostConstruct 里的对象地址与最终 getBean 拿到的不同——代理是在它之后才被套上的。
  3. 把 customInit() 里改成 throw new IllegalStateException("配置校验失败")。你会观察到:整个启动直接失败,异常被包成 BeanCreationException: Error creating bean with name 'singletonLife': Invocation of init method failed,而且 6/7/8 依然会执行——这正是上一篇讲的「不留半成品」。
112 / 124

提示:做完第 1 条再回头看第十一节的沙盘,把开关切到 prototype,两边结论应当完全对得上。

113 / 124
小节
第三档 · 造一个
114 / 124

给自己写一个「生命周期探针」组件,以后任何一个项目的启动慢、资源泄漏都能用它量出来。

115 / 124

需求:

116 / 124
  • 一个 LifecycleProbe 实现 InstantiationAwareBeanPostProcessor,记录每个 Bean 的「实例化时刻」与「初始化完成时刻」
  • 额外区分三类信息:是否走了 @PostConstruct(可用 CommonAnnotationBeanPostProcessor 的存在与否推断)、是否被代理过(比较 postProcessAfterInitialization 返回对象的 getClass() 与入参是否相同)
  • 容器关闭时(配合 SmartLifecycle 或 ContextClosedEvent 监听)打印一张汇总表:Top 10 耗时 Bean、被代理的 Bean 数量、从未被创建的懒加载 Bean 名单
  • 全部逻辑不许修改业务 Bean 一行代码
117 / 124

验收清单:① 在一个空 Boot 工程跑,输出的 Top 10 至少包含 dataSource 之类的自动配置 Bean;② 故意把一个 Bean 标成 prototype 并使用三次,表格里它的创建次数应为 3;③ 给某个 Bean 加 @Transactional 后,它出现在「被代理」计数里;④ 探针自身不会拖慢启动超过 5%(用第一次的总耗时对比验证)。

118 / 124
小节
十五、要点自查
119 / 124
自检

不看上文,按顺序说出八个阶段的名字,并指出「属性填充」和「初始化前置」之间隔着哪一步回调。

120 / 124
自检

@PostConstruct 到底是谁调用的?为什么它算「初始化前置」而不是一个独立阶段?

121 / 124
自检

为什么构造器里读字段注入的值一定是 null,而构造器注入就不是?两种写法各自的「出生即完整」程度差别在哪?

122 / 124
自检

原型 Bean 的销毁为什么不执行?如果确实需要释放资源,你有哪两条正规出路?

123 / 124
自检

AOP 代理诞生于哪一步?这一步的时机如何同时解释了「@PostConstruct 里自调用 @Transactional 失效」和「getClass() 打印出 CGLIB 类名」两件事?

124 / 124
口诀

先进货(实例化)、再摆货(填充)、挂牌营业前必体检(初始化)、发工牌在最后(代理)——原型不发退休证。