ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring源码解析:applyMergedBeanDefinitionPostProcessors与注入元数据缓存机制

Spring源码解析:applyMergedBeanDefinitionPostProcessors与注入元数据缓存机制

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 创建的核心方法,主干步骤可以简化为:

  1. 调用createBeanInstance得到 Bean 实例(此时只是裸对象,属性全是默认值)。
  2. 调用applyMergedBeanDefinitionPostProcessors处理合并后的 BeanDefinition。
  3. 处理循环依赖时提前暴露ObjectFactory(如果允许)。
  4. 调用populateBean进行属性填充。
  5. 调用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的核心逻辑是:

  1. 调用findMergedAnnotation查找@Autowired和@Value注解。
  2. 调用buildAutowiringMetadata构建InjectionMetadata。
  3. 把结果放入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 的完整生命周期里,后处理器的调用顺序大致是:

  1. InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation(实例化前)
  2. MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition(实例化后,属性填充前)
  3. InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation(属性填充前)
  4. InstantiationAwareBeanPostProcessor#postProcessProperties(属性填充)
  5. BeanPostProcessor#postProcessBeforeInitialization(初始化前)
  6. BeanPostProcessor#postProcessAfterInitialization(初始化后)

这个顺序是固定的,由doCreateBean的代码结构决定。如果你的后处理器同时实现了多个接口,要清楚每个方法在哪个阶段被调用,避免在错误的阶段做事情。

7.3 一个后处理器实现多个接口的实践

AutowiredAnnotationBeanPostProcessor同时实现了MergedBeanDefinitionPostProcessor、InstantiationAwareBeanPostProcessor、SmartInstantiationAwareBeanPostProcessor、PriorityOrdered。这种“一个类多个角色”的设计在 Spring 内部很常见,好处是相关逻辑集中在一个类里,便于维护。

自己写扩展时,如果注入元数据的准备和使用逻辑紧密相关,也可以采用这种模式。但如果逻辑差异较大,建议拆成两个独立的类,通过缓存或者共享的Map来传递数据,这样职责更清晰。

8. 源码阅读的方法论小结

读applyMergedBeanDefinitionPostProcessors这类方法,我的习惯是先看它在主流程中的位置,再看它的参数和返回值,最后看有哪些实现类。位置决定了它的职责边界,参数决定了它能拿到什么信息,实现类决定了它的实际用途。

这个方法的价值在于它揭示了一个设计模式:在对象创建和属性填充之间插入一个“元数据准备”阶段。这个模式在很多框架里都有类似体现,比如 ORM 框架在实体创建后、字段赋值前解析映射信息,序列化框架在对象构造后、字段读取前解析注解。理解了这个模式,再看其他框架的源码会轻松很多。

另外,读源码时不要只看主流程,要特别留意缓存和清理逻辑。injectionMetadataCache和resetBeanDefinition这种成对出现的代码,往往是排查线上问题的关键线索。很多“偶发”的注入失败、类型错乱问题,根源都在缓存没有正确清理。

最后分享一个我自己的习惯:读到一个关键方法时,会尝试自己写一个最小实现,然后打断点观察调用栈。光看代码容易漏掉细节,跑一遍才能真正理解参数是怎么传进来的、缓存是什么时候被读写的。这个方法虽然笨,但效果最好。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进