深入 Spring 08:扩展顺序与过早创建
修改定义与包装实例需要不同的时机
订单服务的默认配置需要在创建前调整,业务方法又需要在创建后增加代理拦截。这两个需求虽然都叫扩展容器,却作用于不同对象:BeanFactoryPostProcessor 修改 BeanDefinition 等工厂元数据,BeanPostProcessor 接收已经创建出来的实例。把前者写成 getBean 后修改对象,会提前触发创建,使后者可能失去机会。
本文固定 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。完整可运行代码在 examples/spring-framework-lab/src/main/java/blog/spring/Chapter08.java。实验有两个独立上下文:正常场景按阶段修改定义和包装实例;反例在工厂后处理期间提前获取业务 singleton,观察缺失的代理和未生效的属性覆盖。
refresh 中的两个入口不能交换
AbstractApplicationContext.refresh() 在工厂准备完成后调用 invokeBeanFactoryPostProcessors(),再调用 registerBeanPostProcessors(),最后才在后续阶段完成普通 singleton 的预实例化。前一个入口要运行扩展本身,后一个入口主要是发现、排序并安装实例后处理器,供随后创建 Bean 时调用。
两者都委托给 PostProcessorRegistrationDelegate,但不能因此把它们理解成一轮统一排序。工厂后处理器可能新增定义,也可能修改另一个后处理器的配置;普通业务 Bean 应尽量保持未创建。若先实例化业务对象,再执行定义修改,缓存里的旧实例不会因元数据变化而自动重建。固定版本阶段实现
Chapter08 的 Registry 扩展先注册 service 定义,FactoryPriority 再把其 value 属性改为 modified。Service 构造时默认值为 original,实际创建过程应用修改后的属性,最终取得 modified。这个断言同时证明定义存在、修改发生在实例化之前、属性填充实际使用了修改值。
Registry 扩展先改变定义集合
BeanDefinitionRegistryPostProcessor 继承 BeanFactoryPostProcessor,额外提供 registry 回调。它能够添加、删除或替换定义,因此必须先于普通工厂元数据处理。源码先处理通过 context 手工加入的 registry 扩展,再发现容器定义中的 PriorityOrdered、Ordered 和其余 registry 扩展。
处理 registry 扩展时不能只扫描一次。某个扩展运行后可能注册另一个扩展,源码通过 processedBeans 集合去重,并在后续轮次反复检查新增名称,直到没有新的 registry 扩展需要执行。所有已处理 registry 扩展的 factory 回调随后统一执行,之后才进入普通工厂后处理器。
这解释了为什么 Registry 的两个回调不能理解成“针对某个扩展连续调用完再看下一个”。定义集合必须先达到当前阶段可见的稳定状态,才适合让后续工厂回调遍历。上游 beanDefinitionRegistryPostProcessorRegisteringAnother 和 prioritizedBeanDefinitionRegistryPostProcessorRegisteringAnother 正是围绕动态新增扩展构造测试,并断言新增 factory 扩展得到调用。
实验只有一个 registry 扩展,避免把动态注册细节和核心阶段混在同一输出中。动态新增机制来自固定版本源码与上游测试阅读,本地实验的覆盖边界是已知处理器集合的排序,不把前者标成已执行案例。
PriorityOrdered 是分组,order 是组内次序
PriorityOrdered 和 Ordered 的关系容易被整数掩盖。实验让 FactoryPriority 的 order 为 100,FactoryOrdered 的 order 为 -100。如果把所有处理器平铺后仅按整数排序,应该是 -100 在前;实际输出恰好相反。
原因是自动发现流程先分组,再在每组内排序。PriorityOrdered 组整体先于 Ordered,普通未排序组最后。order 值只在适用的同一组内决定先后,不能跨越阶段,也不能让普通 BFPP 抢到 Registry 回调之前。
同一规则也用于自动发现的普通 BPP。实验中 autoPriority=100、autoOrdered=-100,仍然先执行 autoPriority。这里没有注册 MergedBeanDefinitionPostProcessor;这类内部处理器在 delegate 后段还有重新登记到链尾的步骤,因此不能把本例输出当成所有内部 BPP 的绝对位置表。
一个扩展若对另一个扩展产物有真实依赖,应明确依赖在哪个阶段得到满足,而不是不断把 order 调得更小。order 不能解决处理阶段选错的问题,也不能让尚未注册的处理器补处理已经完成的 singleton。
手工注册走的是登记顺序
实验先用 BeanFactory.addBeanPostProcessor 加入 manual100,再加入 manual-100。二者都实现 Ordered,但执行仍为 manual100 在前。手工注册直接把实例放进处理器链,容器不会在这条路径重新根据 Ordered 排序。
自动发现的 BPP 随后才加入,所以完整实例后处理部分是 manual100、manual-100、autoPriority、autoOrdered、wrap。这个顺序既体现两种注册路径的差别,也解释了第 07 篇为什么必须把手工加入的注解处理器位置写清楚。官方扩展点文档
同理,context.addBeanFactoryPostProcessor 接受已经构造的实例,也不能假定它与 BeanDefinition 中声明的 BFPP 一起进行全局整数排序。声明方式是扩展行为的一部分,复制实现类而改变注册方式,可能直接改变结果。
在裸 DefaultListableBeanFactory 中声明一个 BFPP Bean,单纯 getBean 不会自动执行其工厂扩展契约。运行这些阶段的是 ApplicationContext 的启动流程。上游 beanFactoryPostProcessorNotExecutedByBeanFactory 明确检查这个边界,避免把“实现了接口”与“某个宿主会调用接口”混为一谈。
实例包装改变公开引用
Wrapper 的 after-initialization 回调只处理名称为 service 的 Bean,用 ProxyFactory 构造接口代理并返回。正常上下文中,程序同时断言 AopUtils.isAopProxy 为 true 和 Work.value 返回 modified。前者检查包装,后者检查定义修改是否到达实际 target。
所有 BPP 都应对非目标对象返回原引用。处理器链会接触其他基础设施对象;如果毫无筛选地包装每个实例,可能破坏类型匹配、基础设施生命周期甚至处理器自身的创建。名称筛选只是本例最小边界,真实项目通常还结合类型、注解或 advisor 匹配。
这个代理没有事务或缓存语义,advice 只调用 proceed。用最少业务逻辑验证处理器时序,避免把数据库资源或异常转换引入排序问题。代理能力的具体意义应由后续专题独立验证。
过早 getBean 的反例可以成功启动
第二个上下文先注册 service 和 wrapper 定义,随后添加一个手工 BFPP。该扩展先 getBean(“service”),再把定义的 value 改为 tooLate。refresh 没有抛异常,但最终 service 仍是原始对象,value 仍为 original,wrapper 的事件列表为空。
这三个观测分别排除三个误解:启动成功不代表所有扩展都生效;定义当前值改变不代表已有实例字段改变;BPP 以后登记完成不会追溯处理之前创建的 singleton。拿到的旧实例已缓存在 singletonObjects,后续查询直接复用,不再重走创建链。
这个场景发生在 registerBeanPostProcessors 之前,不应把是否打印“not eligible for all BeanPostProcessors”警告作为唯一判据。BeanPostProcessorChecker 自身也要在相关阶段安装;错过诊断处理器的更早创建路径仍可能没有那条熟悉警告。实验直接检查对象和属性,避免依赖日志级别或警告文本。
修复应让 BFPP 只操作定义,把普通服务调用移到完整容器准备后的合适入口。BPP 自身如果通过非 static 的 @Bean 方法创建,还可能提前实例化配置类;声明 static、使用明确返回类型并减少依赖,有助于避免额外的过早创建链。但这些声明不能挽救扩展里主动调用业务 getBean 的错误时机。
反例题与改动练习
反例题:把 manual100 的 order 改为 -1000,能否使它自动排到 manual-100 前面?本例手工添加的处理器按登记先后进入链,数值不会重新排序这个列表。自动发现处理器的分组与排序属于另一条路径。
在 Chapter08 副本的过早创建场景中删除 factory.getBean(“service”),保留定义值修改为 tooLate,重跑本章命令。先预测服务会在完整 BPP 登记后创建:它会成为代理,value 返回 tooLate,包装事件包含 wrap。原先证明缺失代理、旧属性和未补处理的三个断言会失败;仅将这组期望改为预测值,再确认正常场景的顺序仍通过。这个变体检验失败根因是创建时点,而不是 tooLate 这个字符串。
运行结果对应哪些保证
在 JDK 21 的实验工程执行:
1 | |
正常场景的精确事件为:
1 | |
六个 CHECK PASS 分别验证完整顺序、定义修改、实例代理,以及反例的缺失代理、旧属性、未补处理。任一不符合即抛 AssertionError;所有上下文用 try-with-resources 关闭,最后打印 PASS Chapter08 resources closed。原始运行日志在 examples/spring-framework-lab/evidence/08/。
上游 BeanFactoryPostProcessorTests 提供手工注册、定义注册、动态新增和裸工厂边界的独立检查。本地没有运行整个上游 Gradle 测试套件。
确定扩展位置时,可以先问它要修改的是定义集合、定义属性还是实际对象,然后再决定 registry、factory 或 instance 回调。阶段选对之后,才讨论自动发现分组和组内排序。生命周期入口见 深入 Spring 07:生命周期回调与最终代理引用。

