1. 为什么单独拎出这个方法值得写一篇笔记
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName)这个方法在 Spring 的doCreateBean流程里只占一行调用,位置在createBeanInstance之后、populateBean之前。很多人读源码时一扫而过,觉得它就是个“钩子调用”,没什么好深究的。但如果你真正做过自定义注解注入、做过@Autowired和@Resource的差异排查、或者写过基于MergedBeanDefinitionPostProcessor的扩展,就会发现这个方法是 Spring 从“实例已创建”过渡到“属性填充”的关键闸门。
它解决的问题很具体:Bean 实例已经通过构造器或者工厂方法造出来了,但此时 Spring 还不知道这个实例里哪些字段、哪些方法需要被注入。applyMergedBeanDefinitionPostProcessors就是给后处理器一个机会,让它们在属性填充之前,把需要注入的元信息“缓存”到BeanDefinition里。最典型的实现就是AutowiredAnnotationBeanPostProcessor,它在这个阶段扫描@Autowired、@Value注解,把注入点信息存进InjectionMetadata缓存,等到populateBean阶段再直接取出来用。
这篇文章适合已经能跑通 Spring 基本流程、想往源码深处走一步的开发者。我会从调用位置、参数含义、核心实现、常见后处理器、实操扩展、排查技巧几个角度拆开讲,尽量把“为什么这么设计”说清楚,而不是只贴代码。读完之后,你应该能自己写一个MergedBeanDefinitionPostProcessor,并且知道它和BeanPostProcessor、InstantiationAwareBeanPostProcessor的区别在哪里。
注意:本文基于 Spring 5.x 的源码结构展开,不同小版本在方法命名和内部细节上可能有微调,但核心设计思路一致。阅读时建议对照自己项目里的实际版本。
2. 方法在 doCreateBean 中的位置与调用时机
2.1 从 doCreateBean 的主干流程说起
AbstractAutowireCapableBeanFactory#doCreateBean是单例 Bean 创建的核心方法,主干步骤可以简化为:
- 调用
createBeanInstance得到 Bean 实例(此时只是裸对象,属性全是默认值)。 - 调用
applyMergedBeanDefinitionPostProcessors处理合并后的 BeanDefinition。 - 处理循环依赖时提前暴露
ObjectFactory(如果允许)。 - 调用
populateBean进行属性填充。 - 调用
initializeBean执行初始化方法、BeanPostProcessor的前后置处理。
applyMergedBeanDefinitionPostProcessors处在第 2 步,这个位置非常讲究。它必须在实例创建之后,因为有些后处理器需要拿到beanType甚至实例本身来做判断;但它又必须在属性填充之前,因为它的产出正是属性填充要用的元数据。
如果把它放到createBeanInstance之前,beanType可能还没最终确定,尤其是涉及 FactoryBean 或者代理的场景;如果放到populateBean之后,那就完全失去意义了,因为注入已经做完了。Spring 把它卡在这个中间点,本质上是在“实例已存在”和“属性待注入”之间插入一个元数据准备阶段。
2.2 三个参数分别代表什么
方法签名是:
void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName)mbd:合并后的RootBeanDefinition。注意是“合并后”的,意味着它已经吸收了父 BeanDefinition 的属性,也经过了BeanFactoryPostProcessor的修改。后处理器往它身上挂东西,后续populateBean能直接读到。beanType:当前 Bean 的实际类型。这个类型可能和mbd.getBeanClass()不完全一样,比如经过 FactoryBean 或者某些动态代理场景后,实际类型会以beanType为准。beanName:Bean 的名称,用于日志、缓存 key、以及后处理器内部做条件判断。
这三个参数组合起来,给后处理器提供了“我是谁、我是什么类型、我的定义是什么”的完整上下文。很多后处理器就是靠beanType上的注解来决定要不要处理这个 Bean。
2.3 为什么强调“MergedBeanDefinition”
这里有个容易混淆的点:BeanDefinitionPostProcessor和MergedBeanDefinitionPostProcessor是两个不同层级的接口。前者在BeanDefinition注册阶段就可能被调用,后者明确要求处理的是“合并后”的定义。
合并后的定义有几个特点:父定义的属性已经继承过来、propertyValues已经合并、dependsOn已经解析。对于注入元数据的缓存来说,必须基于合并后的定义,否则你拿到的可能是一个不完整的、缺少父类注入信息的定义。这也是 Spring 在方法名里特意加上Merged的原因,提醒实现者注意处理时机。
实操心得:如果你自定义的后处理器需要读取父类上的注解信息,一定要用
mbd而不是原始的BeanDefinition,否则在存在继承关系的 Bean 上会漏掉父类字段的注入点。
3. 核心实现拆解:后处理器是如何被触发的
3.1 方法内部的两行关键代码
打开AbstractAutowireCapableBeanFactory,这个方法体其实很短:
protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof MergedBeanDefinitionPostProcessor) { MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp; bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName); } } }逻辑很直白:遍历所有BeanPostProcessor,筛出实现了MergedBeanDefinitionPostProcessor接口的,逐个调用postProcessMergedBeanDefinition。但这里有几个细节值得注意。
第一,遍历的是getBeanPostProcessors()返回的列表,这个列表是有序的,顺序由后处理器的注册顺序和PriorityOrdered、Ordered接口决定。如果你的后处理器依赖另一个后处理器的产出,顺序就很重要。
第二,筛选用的是instanceof,意味着一个后处理器可以同时实现多个接口,在不同阶段被调用。比如AutowiredAnnotationBeanPostProcessor既实现了MergedBeanDefinitionPostProcessor,也实现了InstantiationAwareBeanPostProcessor,它在两个阶段都有动作。
第三,方法没有返回值,所有产出都通过修改mbd或者后处理器内部的缓存来传递。这是一种“副作用式”的设计,读源码时要特别留意后处理器内部维护了哪些 Map。
3.2 MergedBeanDefinitionPostProcessor 接口定义
接口本身只有一个方法:
public interface MergedBeanDefinitionPostProcessor extends BeanPostProcessor { void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName); }它继承自BeanPostProcessor,所以实现类天然就是后处理器。Spring 5.x 里这个接口还加了一个默认方法resetBeanDefinition,用于在 BeanDefinition 被重置时清理缓存。这个方法在容器刷新或者动态注册 Bean 的场景下会被调用,如果你在postProcessMergedBeanDefinition里维护了缓存,记得在resetBeanDefinition里同步清理,否则会出现缓存和定义不一致的问题。
3.3 内置实现类一览
Spring 5.x 中实现了这个接口的内置后处理器主要有:
| 后处理器 | 作用 | 处理阶段 |
|---|---|---|
AutowiredAnnotationBeanPostProcessor | 扫描@Autowired、@Value、@Inject,缓存注入元数据 | 本方法缓存,populateBean 时注入 |
CommonAnnotationBeanPostProcessor | 扫描@Resource、@PostConstruct、@PreDestroy | 本方法缓存注入元数据,初始化阶段执行生命周期方法 |
PersistenceAnnotationBeanPostProcessor | 处理 JPA 相关注解 | 本方法缓存,后续注入 |
ApplicationListenerDetector | 检测ApplicationListener类型的 Bean | 本方法记录,初始化后注册监听器 |
这几个后处理器的共同点是:它们都需要在属性填充之前知道“这个 Bean 有哪些特殊点”,然后把信息存起来,等到合适的阶段再执行。ApplicationListenerDetector稍微特殊,它在本方法里只是判断类型并记录,真正的注册动作在postProcessAfterInitialization里。
4. AutowiredAnnotationBeanPostProcessor 的缓存机制
4.1 注入元数据的构建过程
AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition的核心逻辑是:
- 调用
findMergedAnnotation查找@Autowired和@Value注解。 - 调用
buildAutowiringMetadata构建InjectionMetadata。 - 把结果放入
injectionMetadataCache,key 是beanName。
buildAutowiringMetadata会遍历当前类及其父类,检查字段和方法上的注解。字段注入点会被封装成AutowiredFieldElement,方法注入点封装成AutowiredMethodElement。这些元素内部持有Field或Method对象,以及依赖描述符DependencyDescriptor。
这里有个性能优化点:injectionMetadataCache是一个ConcurrentHashMap,构建好的元数据会被缓存。同一个 Bean 多次创建时(比如 prototype 作用域),不会重复扫描注解。但要注意,缓存 key 是beanName,如果同一个 Bean 名称对应的类型发生变化,缓存可能失效,这也是resetBeanDefinition存在的原因。
4.2 为什么不在 populateBean 时再扫描
有人会问:既然populateBean才是真正注入的地方,为什么不直接在populateBean里扫描注解,非要提前缓存?
原因有三点。第一,扫描注解涉及反射遍历字段和方法,开销不小,缓存后 prototype Bean 可以复用。第二,populateBean阶段可能涉及循环依赖,如果此时才扫描,逻辑会变得复杂且容易出错。第三,Spring 的设计哲学是“元数据准备”和“元数据使用”分离,这样后处理器可以在准备阶段做更多判断,比如根据beanType决定是否跳过某些 Bean。
4.3 缓存与 resetBeanDefinition 的配合
resetBeanDefinition在 Spring 5.x 中的默认实现是空方法,但AutowiredAnnotationBeanPostProcessor重写了它:
@Override public void resetBeanDefinition(String beanName) { this.injectionMetadataCache.remove(beanName); }这个清理动作在容器刷新、BeanDefinition 覆盖注册等场景下很关键。我踩过一次坑:在动态注册 Bean 时,同一个 beanName 先注册了一个类型,后来又覆盖成另一个类型,如果没有清理缓存,第二次创建时会拿到第一次的注入元数据,导致注入点错乱。排查了很久才定位到是缓存没清。
注意:如果你自己实现
MergedBeanDefinitionPostProcessor并维护了缓存,务必重写resetBeanDefinition,否则在动态刷新场景下会出现难以复现的诡异问题。
5. 自己写一个 MergedBeanDefinitionPostProcessor
5.1 需求场景:自定义注解注入
假设我们有一个自定义注解@MyInject,希望它像@Autowired一样在属性填充阶段注入依赖。完整实现需要两部分:一个MergedBeanDefinitionPostProcessor负责扫描并缓存注入点,一个InstantiationAwareBeanPostProcessor负责在populateBean时执行注入。
先定义注解:
@Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyInject { boolean required() default true; }然后实现后处理器:
public class MyInjectAnnotationBeanPostProcessor implements MergedBeanDefinitionPostProcessor, InstantiationAwareBeanPostProcessor, PriorityOrdered { private final Map<String, InjectionMetadata> metadataCache = new ConcurrentHashMap<>(256); @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { InjectionMetadata metadata = findMyInjectMetadata(beanName, beanType); metadataCache.put(beanName, metadata); } private InjectionMetadata findMyInjectMetadata(String beanName, Class<?> beanType) { // 遍历字段和方法,构建 InjectionMetadata // 具体实现参考 AutowiredAnnotationBeanPostProcessor return InjectionMetadata.EMPTY; } @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = metadataCache.get(beanName); if (metadata != null && !metadata.isEmpty()) { try { metadata.inject(bean, beanName, pvs); } catch (Throwable ex) { throw new BeanCreationException(beanName, "MyInject injection failed", ex); } } return pvs; } @Override public void resetBeanDefinition(String beanName) { metadataCache.remove(beanName); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 2; } }5.2 关键实现细节说明
InjectionMetadata是 Spring 提供的工具类,构造时需要传入目标类和一组InjectedElement。每个InjectedElement需要实现inject方法,在里面通过反射设置字段值或者调用方法。
构建元数据时要注意父类字段的处理。AutowiredAnnotationBeanPostProcessor的做法是从当前类开始,逐级向上遍历父类,直到Object。每一级都检查声明的字段和方法,避免遗漏继承来的注入点。
postProcessProperties的返回值是PropertyValues,通常直接返回传入的pvs即可。如果返回null,Spring 会跳过后续的属性填充,这个行为要谨慎使用。
getOrder返回的顺序决定了后处理器在列表中的位置。AutowiredAnnotationBeanPostProcessor的顺序是LOWEST_PRECEDENCE - 2,CommonAnnotationBeanPostProcessor是LOWEST_PRECEDENCE - 3。自定义后处理器如果依赖它们的结果,顺序要排在后面;如果希望自己的注入先执行,顺序要排在前面。
5.3 注册后处理器
在配置类中注册:
@Configuration public class MyInjectConfig { @Bean public static MyInjectAnnotationBeanPostProcessor myInjectProcessor() { return new MyInjectAnnotationBeanPostProcessor(); } }注意方法要用static修饰,因为BeanPostProcessor需要在其他 Bean 创建之前实例化,非静态方法会导致配置类本身被提前创建,可能引发循环依赖或者初始化顺序问题。这是 Spring 官方文档里明确提到的注意事项,我见过不少项目在这里踩坑。
6. 常见问题与排查技巧实录
6.1 注入元数据缓存不生效
现象:自定义后处理器在postProcessMergedBeanDefinition里明明扫描到了注入点,但populateBean时取不到缓存。
排查思路:先确认beanName是否一致。有些场景下 Bean 会以别名注册,postProcessMergedBeanDefinition拿到的beanName和postProcessProperties拿到的可能不同。可以在两个方法里都打印beanName对比。
另一个可能是缓存被提前清理了。检查是否有其他地方调用了resetBeanDefinition,或者容器刷新导致缓存被清空。如果是 prototype Bean,每次创建都会走一遍postProcessMergedBeanDefinition,缓存会被覆盖,这是正常的。
6.2 后处理器顺序导致注入失败
现象:自定义注解的注入点没有被处理,或者处理时依赖的 Bean 还没创建。
排查思路:打印getBeanPostProcessors()的顺序,确认自定义后处理器的位置。如果它排在AutowiredAnnotationBeanPostProcessor之前,而它的实现又依赖@Autowired注入的字段,就会出现依赖未注入的情况。
解决办法是实现PriorityOrdered或Ordered接口,明确指定顺序。一般来说,自定义注入后处理器应该排在 Spring 内置的之后,除非你有特殊需求。
6.3 循环依赖场景下的元数据问题
现象:A 依赖 B,B 依赖 A,其中一个 Bean 的注入元数据在循环依赖处理过程中丢失。
排查思路:循环依赖时,Spring 会提前暴露ObjectFactory,此时applyMergedBeanDefinitionPostProcessors可能还没执行完。检查mbd的状态,确认元数据是否已经挂载。如果后处理器依赖实例本身做判断,在循环依赖场景下可能拿到的是未完全初始化的实例。
常见问题速查表:
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
| 注入点扫描不到 | 注解不在beanType上,或在父类但未遍历 | 检查遍历逻辑,确认父类处理 |
| 缓存取不到 | beanName 不一致或缓存被清理 | 打印 beanName,检查 reset 调用 |
| 注入顺序错乱 | 后处理器顺序不对 | 实现 Ordered,调整 getOrder |
| 循环依赖时报错 | 元数据准备与实例暴露时序冲突 | 避免在元数据阶段依赖实例状态 |
| 动态刷新后注入失效 | 缓存未清理 | 重写 resetBeanDefinition |
6.4 一个容易被忽略的细节:beanType 与 mbd.getBeanClass() 的差异
在大多数场景下,beanType和mbd.getBeanClass()是同一个类。但在 FactoryBean 场景下,beanType可能是 FactoryBean 本身,也可能是它产出的对象类型,取决于调用时机。如果你的后处理器用mbd.getBeanClass()做类型判断,可能会判断错。
我建议统一用beanType参数做类型判断,它是 Spring 经过一系列解析后确定的实际类型,更可靠。只有在需要读取 BeanDefinition 上的属性(比如propertyValues、initMethodName)时,才用mbd。
7. 与其他后处理器接口的协作关系
7.1 和 InstantiationAwareBeanPostProcessor 的分工
InstantiationAwareBeanPostProcessor有三个关键方法:postProcessBeforeInstantiation、postProcessAfterInstantiation、postProcessProperties。其中postProcessProperties是属性注入的实际执行点。
MergedBeanDefinitionPostProcessor负责“准备元数据”,InstantiationAwareBeanPostProcessor负责“使用元数据”。两者配合的典型模式就是:前者把InjectionMetadata存入缓存,后者从缓存取出并执行注入。理解这个分工,就能明白为什么 Spring 要把它们拆成两个接口,而不是合并成一个。
7.2 和 BeanPostProcessor 的调用顺序
在一个 Bean 的完整生命周期里,后处理器的调用顺序大致是:
InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation(实例化前)MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition(实例化后,属性填充前)InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation(属性填充前)InstantiationAwareBeanPostProcessor#postProcessProperties(属性填充)BeanPostProcessor#postProcessBeforeInitialization(初始化前)BeanPostProcessor#postProcessAfterInitialization(初始化后)
这个顺序是固定的,由doCreateBean的代码结构决定。如果你的后处理器同时实现了多个接口,要清楚每个方法在哪个阶段被调用,避免在错误的阶段做事情。
7.3 一个后处理器实现多个接口的实践
AutowiredAnnotationBeanPostProcessor同时实现了MergedBeanDefinitionPostProcessor、InstantiationAwareBeanPostProcessor、SmartInstantiationAwareBeanPostProcessor、PriorityOrdered。这种“一个类多个角色”的设计在 Spring 内部很常见,好处是相关逻辑集中在一个类里,便于维护。
自己写扩展时,如果注入元数据的准备和使用逻辑紧密相关,也可以采用这种模式。但如果逻辑差异较大,建议拆成两个独立的类,通过缓存或者共享的Map来传递数据,这样职责更清晰。
8. 源码阅读的方法论小结
读applyMergedBeanDefinitionPostProcessors这类方法,我的习惯是先看它在主流程中的位置,再看它的参数和返回值,最后看有哪些实现类。位置决定了它的职责边界,参数决定了它能拿到什么信息,实现类决定了它的实际用途。
这个方法的价值在于它揭示了一个设计模式:在对象创建和属性填充之间插入一个“元数据准备”阶段。这个模式在很多框架里都有类似体现,比如 ORM 框架在实体创建后、字段赋值前解析映射信息,序列化框架在对象构造后、字段读取前解析注解。理解了这个模式,再看其他框架的源码会轻松很多。
另外,读源码时不要只看主流程,要特别留意缓存和清理逻辑。injectionMetadataCache和resetBeanDefinition这种成对出现的代码,往往是排查线上问题的关键线索。很多“偶发”的注入失败、类型错乱问题,根源都在缓存没有正确清理。
最后分享一个我自己的习惯:读到一个关键方法时,会尝试自己写一个最小实现,然后打断点观察调用栈。光看代码容易漏掉细节,跑一遍才能真正理解参数是怎么传进来的、缓存是什么时候被读写的。这个方法虽然笨,但效果最好。