深入 Spring 07:生命周期回调与最终代理引用
初始化回调中的 this 属于目标对象
订单服务在初始化方法里调用自己的业务方法,容器启动后外部请求又调用同一个方法。如果业务方法由代理拦截,两次调用不一定经过同一条路径。初始化回调作用于正在创建的目标对象,最终提供给调用方的引用可以在后处理阶段被替换为代理;方法名相同不意味着入口相同。
本文固定 Spring Framework 6.2.11、JDK 21,源码提交为 4c134254642d88e058aa004bdaf44168e1be7bb2。实验使用普通 GenericApplicationContext,显式注册后处理器,避免组件扫描带入无关扩展。标准生命周期注解来自 Jakarta Annotation 2.1.1。这里使用 Java 21 编译,代码中的 var 和 List.of 不能直接用于 Java 8。
完整程序是仓库 examples/spring-framework-lab/src/main/java/blog/spring/Chapter07.java。实验同时保存回调顺序、初始化时的 this、容器公开引用和拦截次数,单靠“某条日志出现了”无法证明代理边界。
创建、注入和初始化是不同阶段
AbstractAutowireCapableBeanFactory.createBean() 先给实例化前的扩展机会;没有提前返回对象时才进入 doCreateBean()。后者调用 createBeanInstance() 得到 BeanWrapper,取出目标实例,应用合并定义后处理器,再决定是否允许早期暴露,随后依次执行 populateBean() 和 initializeBean()。固定版本创建源码
构造器返回只说明实例已经分配并执行构造逻辑。属性赋值发生在 populate 阶段,初始化回调因此可以验证属性注入结果。Chapter07 的 setter 写入 repository 字符串,@PostConstruct 当场断言它已经存在。若把断言移动到无参构造器,setter 尚未执行,预期就必须改变。
把“对象已经存在”与“对象可以执行业务”分开,能够解释许多启动阶段异常。构造器适合建立不依赖后续 setter 的不变式;初始化回调可以验证容器提供的最终配置。外部资源启动、批量调用其他 Bean 和跨线程调度还涉及整个容器的就绪状态,不能由单个 Bean 的初始化成功推出。
Aware 也有不同的调用入口
initializeBean() 首先调用 invokeAwareMethods()。在 6.2.11 中,这个方法显式识别 BeanNameAware、BeanClassLoaderAware 和 BeanFactoryAware,并依次通知它们。实验只实现名称和工厂两个接口,观察到名称先于工厂,二者都晚于属性注入。
ApplicationContextAware 等接口走另一条路径:ApplicationContext 在 prepareBeanFactory 时安装 ApplicationContextAwareProcessor,由它在 before-initialization 阶段处理。这意味着不能把所有 Aware 都画成一个脱离后处理器的统一步骤。准确的调用图必须注明具体接口和处理器注册方式。Context Aware 处理器
Aware 回调只是获得上下文对象的时机。持有 BeanFactory 并不要求在回调内立即 getBean;这种主动查找会引入新的创建依赖,甚至触发循环依赖。没有真实需求时,把查找留给正常业务入口比在初始化期间扩张对象图更容易推理。
三种初始化机制如何排序
Aware 之后,容器遍历 postProcessBeforeInitialization(),再运行 invokeInitMethods(),最后遍历 postProcessAfterInitialization()。标准 @PostConstruct 由 CommonAnnotationBeanPostProcessor 继承的注解生命周期逻辑执行,所以它本身位于 before 处理器链之中,并非链外一个额外的阶段。
invokeInitMethods() 先检查 InitializingBean,调用 afterPropertiesSet,再执行 BeanDefinition 指定的自定义 initMethod。源码还检查 externally managed 标记及方法名,避免同一生命周期方法因重复配置而被不必要地重复调用。因此实验为注解、接口、自定义方法使用三个不同的方法名,才能清楚观察三次独立回调。
实验把观察处理器以编程方式先注册,再加入 CommonAnnotationBeanPostProcessor,所以得到以下精确顺序:
1 | |
这里的 before 先于 annotation 是这个注册顺序的结果;把观察处理器放到注解处理器之后,二者顺序也会改变。可以稳定迁移的结论是注解回调所在阶段以及接口、自定义 initMethod 的相对顺序,而不是所有项目都会打印完全相同的列表。生命周期约定
包装后的引用不等于初始化时的 this
实验的 after 处理器使用 ProxyFactory,为 Work 接口创建 JDK 动态代理。处理器把目标对象作为代理 target,返回代理作为当前链的结果。容器随后缓存并公开这个返回值,所以 getBean("service") 重复查询得到同一个代理。
初始化时保存的 initThis 与公开引用使用 == 比较为 false。初始化注解方法内直接调用 work(),接收者仍是目标对象自身,拦截计数为零。容器刷新后通过公开 Work 引用调用 work,拦截计数才变为一。实验同时检查业务结果仍为 repository,避免把“进入了 advice”误判成业务执行正确。
这不是 JDK 动态代理的偶然限制。代理与目标分离时,目标方法内部的 this 调用不会自动重新进入外部代理。类代理的细节将在代理专题展开,但不能用“换成 CGLIB”代替对调用入口的分析。初始化阶段也不应依赖自身事务、缓存或异步注解已经通过代理生效。
普通 after 处理器可以返回任意兼容对象,故生命周期图中的“包装”只是一个常见用途。标准自动代理器还会参与早期引用,处理循环依赖时需要保持同一代理身份,第 09 篇会检查这条分支;当前实验不包含循环对象图。
初始化失败不会发布半个成功 Bean
反例 Broken.afterPropertiesSet 直接抛出 IllegalStateException。initializeBean 捕获并封装为 BeanCreationException,GenericApplicationContext.refresh 失败。程序检查最深层原因仍是原来的异常、context 不再 active,以及 broken 没有进入 singleton 成品缓存。
异常所在阶段决定后果。目标构造成功并不保证会执行 after 处理器;初始化失败时,正常的最终包装和发布路径不会继续。其他已经创建完成的 singleton 会随 refresh 的失败清理被销毁,但失败对象自身不能假定已经登记所有销毁回调。
如果初始化方法先打开文件或连接,再因配置校验失败抛异常,应在该方法内部用 try/finally 或 try-with-resources 处理尚未移交的资源。依靠“容器总会调用 destroy”补救部分初始化,是错误的资源所有权假设。这里的反例不分配外部资源,只验证对象发布状态。
销毁处理保存的是目标的生命周期契约
正常上下文关闭后,实验记录 preDestroy → destroy → customDestroy。三个回调都属于 Service 目标对象,代理只负责对外调用入口。doCreateBean 将原始 bean 与定义交给销毁登记逻辑,DisposableBeanAdapter 结合 DestructionAwareBeanPostProcessor、DisposableBean 和自定义 destroyMethod 调用清理。销毁适配器源码
销毁并不等于把所有 Java 引用清零。外部代码仍可能持有代理,但它所指向的资源已经关闭;容器生命周期和垃圾回收生命周期不是同一个概念。所有实验上下文使用 try-with-resources,正常和异常路径都会离开资源作用域。
上游 CommonAnnotationBeanPostProcessorTests 的 postConstructAndPreDestroy、postConstructAndPreDestroyWithPostProcessor 覆盖注解初始化与销毁,带 ApplicationContext 的测试还组合多个后处理器。这里阅读了这些测试的配置和断言,实际执行的是本工程 Chapter07,不把上游测试阅读声称为上游测试运行。
反例题与改动练习
反例题:初始化方法里调用 work 没有经过 advice,是否说明最终代理失效?不是。该调用接收者是目标的 this,而容器公开的是另一个代理引用;刷新后经公开引用调用仍会进入 advice。判断依据是两个引用的身份与对应的拦截计数。
在 Chapter07 副本中删除 definition.setInitMethodName(“customInit”),保留注解和 InitializingBean 回调,重跑本章命令。先预测 EVENTS 中只少 custom,代理身份、业务结果和销毁路径仍保持。原顺序断言会失败;将期望列表中的 custom 删除后再运行,可检验“删除一种回调配置不会取消其他初始化契约”。不要同时移除其他回调或包装处理器。
运行并检查对象、顺序和失败路径
在实验工程目录使用 JDK 21 执行:
1 | |
程序必须打印完整 EVENTS、所有 CHECK PASS 和 PASS Chapter07 resources closed,同时退出码为零。任一顺序、身份、拦截次数、异常原因或缓存状态不符都会抛出 AssertionError。原始运行结果保存在 examples/spring-framework-lab/evidence/07/。
排查生命周期问题时,先标出目标创建、注入、回调和公开引用四个节点,再检查回调是谁调用的、当时哪些处理器已经登记。这样可以把初始化数据不完整、代理未进入和资源未释放分成不同的可验证问题,而不用依靠一张包含几十个方法名却没有状态变化的总流程图。
系列起点见 深入 Spring(00):从手动组装到可验证的容器实验,创建策略见 深入 Spring 06:构造器、工厂与延迟创建。


