跳到主要内容
约 7 分钟4 张卡

BeanDefinition

BeanDefinition 保存 Spring 创建 Bean 所需的配置元数据,也就是类型、依赖和初始化方式等信息。本文从定义注册入手,说明两类后处理器的执行时机,再介绍单例预实例化、FactoryBean 和配置类方法调用。区分定义与实例这两个阶段,才能判断扩展点何时生效,以及对象为何被提前创建。

1. 面试核心​

先记住这几条,再读正文
  • 定义与实例。 BeanDefinition 描述 Bean 的类型、作用域、依赖和生命周期配置,容器据此创建对象。
  • 两类后处理器。 BeanFactoryPostProcessor 在普通 Bean 创建前处理定义,BeanPostProcessor 处理已创建的实例。
  • 应用上下文。 ApplicationContext 在 BeanFactory 基础上自动发现后处理器,并集成事件、资源和环境配置。卡 · BeanFactoryPostProcessor
  • 定义注册时机。 BeanDefinitionRegistryPostProcessor 可在普通工厂后处理前注册定义。卡 · 注册表后处理器
  • FactoryBean 产品。 getBean("name") 获取 FactoryBean 产品,getBean("&name") 获取工厂。卡 · FactoryBean 与 BeanFactory

2. BeanDefinition 注册​

容器先把不同配置转换为 BeanDefinition,后续才能用统一方式创建对象。定义主要来自以下四类配置:

  • Java 配置通过 @Bean 方法声明组件。
  • 组件扫描发现 @Component 及其派生注解标记的类。
  • XML、程序化注册和框架导入提供额外定义。
  • Spring Boot 自动配置通过配置类解析形成定义。

每份定义记录创建和管理对象所需的信息,包括以下三组设置:

  • Bean 类或工厂方法,以及构造参数和属性值,决定怎样创建并填充对象。
  • 作用域和懒加载标记,决定何时创建对象,以及哪些调用共享实例。
  • 初始化与销毁方法,决定对象创建后和容器关闭时执行哪些回调。

定义注册完成后,容器才在后续阶段创建相应对象。因此,注册成功时仍可能尚未执行构造器。

3. BeanFactoryPostProcessor​

BeanFactoryPostProcessor 用于在普通 Bean 创建前修改定义。例如,容器可在这个阶段解析属性占位符,或调整某个 Bean 的配置值:

public final class ClientTimeoutProcessor
implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory factory) {
BeanDefinition definition =
factory.getBeanDefinition("paymentClient");
definition.getPropertyValues().add("timeout", Duration.ofSeconds(1));
}
}

示例把 paymentClient 定义中的 timeout 属性设为 1 秒。容器随后创建对象时,才把这个值写入目标属性,所以目标类型需要提供可写的 timeout 属性。

如果在此阶段调用 getBean(),容器会提前创建对应对象及其依赖。这时,部分 BeanPostProcessor 尚未注册,对象就会错过预期的处理。比如自动代理创建器还未就绪,业务 Bean 就会缺少 AOP 代理。排查时应沿提前创建的依赖链查找入口。

4. BeanPostProcessor​

BeanPostProcessor 要先注册,才能参与普通 Bean 的创建。它们的具体实现可处理依赖注入、生命周期注解,也可在初始化后返回代理。容器按下面的顺序安排定义处理与实例创建:

注册初始定义
→ 注册表后处理器扩展定义
→ BeanFactoryPostProcessor 处理定义
→ 注册 BeanPostProcessor
→ 创建非懒加载单例
→ 完成刷新

处理器自身及其直接依赖会提前创建,所以处理器应只依赖必要的基础组件。若它提前依赖业务 Bean,日志可能提示该对象 not eligible for auto-proxying。这表示对象未能经过完整的自动代理处理。

实例处理过程见 Bean 生命周期。

5. BeanFactory 与 ApplicationContext​

选择容器时,要区分核心对象管理与应用启动所需的配套能力。BeanFactory 提供定义管理、对象创建与依赖解析。ApplicationContext 在此基础上自动发现并注册后处理器,还集成环境、事件、资源加载、国际化和生命周期管理。

直接使用 DefaultListableBeanFactory 时,调用者需要自行执行和注册相应处理器。仅仅注册业务 Bean 的定义,还不足以启用注解注入和 AOP。应用开发使用 ApplicationContext,就能复用这些启动步骤。

6. 单例预实例化​

单例预实例化让容器在启动时尽早发现配置问题。ApplicationContext 刷新时会创建非懒加载单例,因此缺失依赖、构造异常和配置错误会直接导致启动失败。@Lazy 将创建推迟到首次获取,创建成本和错误也随之延后。

若非懒加载单例直接依赖 lazy Bean,依赖解析仍会创建后者。注入点的 @Lazy 代理可继续推迟实际获取。

排查启动慢时,应分别测量以下几类耗时:

  • 类路径扫描与定义解析花了多久。
  • 后处理器执行和 Bean 构造花了多久。
  • 外部连接、数据库迁移脚本和初始化任务分别等待了多久。

日志最后出现的 Bean 只说明程序执行到了哪里。结合各阶段耗时,才能判断实际卡在哪一步。

7. FactoryBean 与 BeanFactory​

这两个接口名称相近,但承担的职责不同。BeanFactory 是管理 Bean 的容器接口,FactoryBean<T> 则是容器管理的特殊 Bean,用来生产其他对象。

假设工厂名为 client,getBean("client") 返回它通过 getObject() 生产的对象。名称加上 & 后,getBean("&client") 才返回工厂本身。按类型查找时,容器还会读取 getObjectType() 提供的产品类型。

isSingleton() 声明工厂是否提供共享产品。工厂自身为 singleton 且该值为 true 时,容器缓存产品。工厂定义的作用域与产品共享语义需分别判断。

容器管理 FactoryBean 自身的销毁。产品资源需由工厂或其他明确所有者负责释放。

8. 注册表后处理器​

配置类解析会发现新的组件,因此容器需要一个能够继续注册定义的扩展点。BeanDefinitionRegistryPostProcessor 扩展了工厂后处理器接口。容器先调用它的 postProcessBeanDefinitionRegistry() 注册额外定义,再调用 postProcessBeanFactory() 处理工厂元数据。

ConfigurationClassPostProcessor 属于注册表后处理器。它解析 @Configuration、@Bean、扫描和导入关系。定义集合因此在处理过程中继续扩展。

通过 @Bean 声明这类处理器时,方法宜为 static。这样,容器调用工厂方法时就无需提前创建所属配置类。需要控制处理器顺序时,可实现 PriorityOrdered 或 Ordered。

9. 配置类的 Bean 方法调用​

配置方法互相调用时,需要判断取得的是受管 Bean,还是再次执行方法体创建的对象。@Configuration 默认使用 proxyBeanMethods=true。容器会增强配置类,拦截其中 @Bean 方法的相互调用,让被调用方法通过容器获取对应 Bean。

proxyBeanMethods=false 时,配置类保留普通 Java 调用语义。直接调用另一个 @Bean 方法,会再次执行方法体。若方法体执行 new,就会得到额外对象。

@Configuration(proxyBeanMethods = false)
class ClientConfiguration {
@Bean
Client client() {
return new Client();
}

@Bean
ClientService clientService(Client client) {
return new ClientService(client);
}
}

示例通过方法参数声明依赖。容器先解析 Client,再传给工厂方法。即使关闭配置类代理,也能使用受管依赖。

普通 @Component 中的 @Bean 方法也采用这种轻量模式。Bean 的注册与作用域仍由容器管理。变化的是配置方法之间的直接调用行为。

10. 常见问题​

10.1 定义与实例的创建时机​

定义注册只说明容器已经知道怎样创建对象。非懒加载 singleton 会随上下文刷新创建,lazy 单例可推迟到首次获取。prototype 则在每次向容器获取时新建实例。

10.2 两类后处理器的处理对象​

11 · 面试题卡片​

11.1 BeanFactory 与 ApplicationContext 有什么区别​

出现公司:美团、交通银行

考察重点:说明对象管理与后处理器自动注册的区别,并解释预实例化为何还受 lazy 设置影响。

相关内容:BeanFactoryPostProcessor至单例预实例化。

参考回答

BeanFactory 提供 Bean 管理的核心能力,ApplicationContext 则增加应用所需的容器启动与配套能力。

前者负责定义管理、创建对象、解析依赖和管理作用域。后者自动发现并注册后处理器,还集成环境、事件、资源加载、国际化和生命周期管理。

单例是否预创建,取决于刷新过程与 lazy 设置。该行为只是一项差异,完整引导能力才是两者的主要分工。

11.2 配置类解析为什么使用 BeanDefinitionRegistryPostProcessor?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释配置类解析为何会增加定义,以及注册表后处理器为何要早于普通工厂后处理器执行。

相关内容:注册表后处理器。

参考回答

配置类解析需要在普通工厂后处理前注册新定义,因此使用注册表后处理器。

ConfigurationClassPostProcessor 解析 @Bean、组件扫描和导入关系,把发现的组件加入定义集合。这样,后续处理器就能看到这些定义。

这个阶段应操作元数据。若提前获取业务 Bean,它就会错过尚未注册的实例后处理器。

11.3 FactoryBean 与它的产品怎样获取,产品一定是单例吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:说明普通名称与 & 前缀各取得什么对象,并判断产品何时被缓存、由谁清理。

相关内容:FactoryBean 与 BeanFactory。

参考回答

getBean("client") 获取产品,getBean("&client") 获取工厂,产品是否共享由工厂的约定决定。

工厂自身为 singleton 且 isSingleton() 为 true 时,容器会缓存产品。也就是说,工厂作用域和产品共享方式需要分别判断。

容器负责工厂自身的销毁,产品资源则需由工厂或其他明确的所有者清理。

11.4 proxyBeanMethods=false 后,直接调用另一个 Bean 方法会怎样?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释关闭配置类代理后的方法调用行为,并说明参数注入怎样取得受管依赖。

相关内容:配置类的 Bean 方法调用。

参考回答

关闭配置类代理后,直接调用另一个 @Bean 方法会按普通 Java 语义执行方法体。

如果方法体执行 new,就会创建额外对象。

将依赖写为 @Bean 方法参数后,容器会先解析依赖,再传入方法。这样,配置类即使没有代理,仍能取得容器管理的那份 singleton。

12. 参考资料​