BeanDefinition、组件扫描与配置类
一个 @Bean 方法可以返回客户端对象,一个 @Component 类可以提供业务服务。Spring 在创建这些对象前,先把创建方式登记成 BeanDefinition。注册表中的名称连接了配置、依赖查找和最终实例;同一个类可以登记多份定义,也可以完全不进入容器。
定义保存哪些创建信息
BeanDefinition
├── 创建入口:beanClass / factoryBeanName + factoryMethodName / supplier
├── 依赖:构造参数、属性、自动注入、dependsOn
├── 复用与时机:scope、lazyInit
├── 候选资格:autowireCandidate、primary、fallback
├── 初始化与销毁:initMethodName、destroyMethodName
└── 描述:role、resourceDescription、sourceBeanDefinitionRegistry 按名称保存定义。DefaultListableBeanFactory 同时实现注册表和对象工厂;它取得定义后还要解析构造参数、创建依赖、执行处理器。登记一个定义不会立即调用它的构造器。BeanDefinition
工厂方法也是创建入口。静态工厂不需要工厂实例;实例工厂需要先获取 factoryBeanName 指向的 Bean,再调用方法。@Bean 方法通常属于后一种。Supplier 可以提供直接创建逻辑,但返回的实际类型和对外声明类型应一致,否则按类型查找及提前预测会受影响。
名称用于查找,别名只增加入口,不复制对象。父子定义则是元数据继承:子定义可以继承属性并覆盖部分值,创建前得到合并后的 RootBeanDefinition。它与父子 ApplicationContext 的委托查询不同,也不要求 Java 类继承同一个父类。定义继承
用真实工厂注册一份定义
源码包包含两个 Java 程序。Linux 宿主账号需要 Docker 和 unzip,并有 Docker 操作权限;实验只创建进程内对象,无端口、数据库或外部业务调用。Spring 为 7.0.9,Maven 镜像 3.9.12-eclipse-temurin-25,编译目标 Java 17。
mkdir spring-registration-lab
unzip spring-framework-bean-definition-registration-lab.zip -d spring-registration-lab
cd spring-registration-lab
mkdir -p .m2
IMAGE=maven:3.9.12-eclipse-temurin-25
docker pull "$IMAGE"src/main/java/example/FirstDefinition.java 的完整内容:
package example;
import org.springframework.beans.factory.support.*;
public class FirstDefinition {
public record Client(String endpoint) {}
public static void main(String[] args) {
var factory = new DefaultListableBeanFactory();
var definition = new RootBeanDefinition(Client.class);
definition.getConstructorArgumentValues().addIndexedArgumentValue(0, "sandbox");
definition.setResourceDescription("programmatic:client");
factory.registerBeanDefinition("client", definition);
factory.registerAlias("client", "legacyClient");
System.out.println("beforeLookup=" + factory.containsSingleton("client"));
var client = factory.getBean("client", Client.class);
System.out.println("endpoint=" + client.endpoint());
System.out.println("aliasSame=" + (client == factory.getBean("legacyClient")));
factory.destroySingletons();
}
}addIndexedArgumentValue(0, "sandbox") 指定构造器第一个参数;setResourceDescription 给程序注册留下可定位来源;registerAlias 把旧名称映射到同一定义。裸 BeanFactory 没有为业务对象自动配置注解处理器,这个程序使用显式构造参数,足以观察定义与实例的差别。
项目的 pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>registration-lab</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>7.0.9</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><parameters>true</parameters></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version></plugin>
</plugins></build>
</project>docker run --rm --user "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/maven \
-v "$PWD:/work" -v "$PWD/.m2:/cache" -w /work "$IMAGE" \
sh -ec 'mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/cache \
clean package dependency:build-classpath -Dmdep.outputFile=target/classpath.txt
java -cp "target/classes:$(cat target/classpath.txt)" example.FirstDefinition'beforeLookup=false
endpoint=sandbox
aliasSame=true查询前没有单例实例,第一次 getBean 依据定义创建 Client,别名查询复用同一实例。程序结束调用 destroySingletons()。Context 会组织相应关闭动作;直接使用工厂时要自己管理。
挂载目录和 .m2 必须对宿主账号可写,--user 约束的是构建容器身份,不改变 Docker daemon 身份。Maven 仓库不通时,可挂载企业批准的 settings 并用 -s 指定;离线复跑需先具备镜像和全部依赖,再加 -o。不要把镜像换源和 Java 编译错误混在一起处理。Maven 配置
从 Java 元数据形成定义集合
扫描找到什么
@ComponentScan 按基础包查找候选;@Component 及其元注解派生类型如 @Service、@Repository 是常见入口。扫描器借助 class 文件元数据尽量不加载候选类,应用包含/排除规则、名称生成和 scope 配置后,才注册定义。某些过滤器或条件需要加载类型,因此“扫描永不加载类”并不成立。组件扫描
基础包应指向业务包,推荐用标记类避免字符串包名在重构后失效:
@Configuration(proxyBeanMethods = false)
@ComponentScan(basePackageClasses = OrdersPackage.class)
class OrdersConfiguration {}这里 OrdersPackage 是放在期望扫描根包中的空标记类型。根包过宽可能把测试配置、第三方扩展和不应加载的组件一起纳入;同名冲突先看扫描来源,而不是立刻允许覆盖。
配置类也要先进入容器。一个类写了 @Configuration,但既没有注册、导入,也没有被扫描到,其中的 @Bean 方法就不会生效。
Import 与配置类解析
显式 register / XML reader / scanner / registrar
↓
BeanDefinitionRegistry
↑
ConfigurationClassPostProcessor
→ 解析配置类和条件
→ @ComponentScan 发现新配置
→ @Import 导入配置、Selector 或 Registrar
→ 登记 @Bean 工厂方法定义定义可以从不同入口进入注册表。XML reader 和程序注册直接形成定义;配置类处理器负责解析配置类语义。
ImportSelector 返回需要导入的类名,DeferredImportSelector 把选择推迟到配置类初步解析之后;ImportBeanDefinitionRegistrar 能直接操作注册表。选择器适合选择配置集合,Registrar 适合按元数据生成定义。它们仍运行在对象创建之前,不应为决定是否登记一个客户端而连接外部服务。组合配置
ConfigurationClassPostProcessor 作为注册表后处理器解析新配置并登记 Bean 方法,再在工厂后处理阶段完成必要的配置类增强。递归扫描和 Import 可能产生更多配置,因此它会跟踪已处理候选,直到本轮定义集合稳定。7.0.9 配置类处理器
Profile 与 Condition 决定定义是否存在
@Profile 是基于 Environment 的条件。活动 profile 应在 refresh() 之前设置;默认 profile 只在没有活动 profile 时参与。类级条件不成立会连带排除其中的 Bean 方法。
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.RegistrationLab profile'sandboxBean=true
inactiveBean=false同一份 Config 在激活 sandbox 的 Context 中注册 endpoint,在未激活的 Context 中没有该定义。这里验证的是确定的配置输入;远端服务瞬时可用性应通过运行期健康处理,不用它决定每个实例注册哪套 Bean。Profile
配置方法互调与定义处理时机
full 与 lite 的对象身份
完整 @Configuration 默认允许配置类增强。外部或同类方法调用一个 @Bean 方法时,增强逻辑可以转成容器查找,维持 singleton、scope 等语义。proxyBeanMethods=false 或普通组件中的 Bean 方法按普通 Java 调用执行,方法体中的 new 每次仍然创建对象。Bean 方法语义
扩展程序中的三种配置只改变依赖的取得方式:
// 完整配置:token() 互调由增强配置类转交容器。
@Configuration
class Full {
@Bean Token token() { return new Token(); }
@Bean Pair pair() { return new Pair(token()); }
}
// 无增强配置:方法参数由容器解析,不直接互调。
@Configuration(proxyBeanMethods = false)
class Parameters {
@Bean Token token() { return new Token(); }
@Bean Pair pair(Token token) { return new Pair(token); }
}RegistrationLab 还包含直接互调的 lite 负例:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.RegistrationLab configuration'输出 fullSame=true liteSame=false parameterSame=true。lite 配置中 Pair 持有的 Token 是方法直接创建的容器外对象,与独立 token Bean 不同;参数注入恢复同一引用。这个差异对连接池、定时器等资源尤其重要:意外创建第二个对象,可能既重复启动又没有正确销毁。
需要增强的配置类和非 static Bean 方法必须满足可继承/可覆盖条件。修改 proxyBeanMethods 之前,检查是否存在方法互调;改为参数依赖后,再用引用比较或资源实例数确认行为。
定义处理器与实例处理器
BeanDefinitionRegistryPostProcessor 可增加定义;BeanFactoryPostProcessor 修改定义或工厂;BeanPostProcessor 处理实例。这些阶段的输入不同,不能因为都叫“后处理器”而混用。
BDRPP 注册/修改定义
→ BFPP 修改工厂和定义
→ 注册 BPP
→ 普通 Bean 创建、注入、初始化早期调用 getBean() 会立即创建普通对象,后续 BPP 可能来不及处理它。读取属性可使用 Environment,调整参数直接修改 BeanDefinition;返回 BFPP/BDRPP 的 @Bean 方法通常采用 static,避免为了创建处理器而提前实例化配置类。容器扩展点
在 registry 实验里,BFPP 把 Value 的 text 属性由 before 改为 after,普通对象随后按照新定义创建:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.RegistrationLab registry'duplicateRejected=true
postProcessedValue=after同一程序先显式 setAllowBeanDefinitionOverriding(false),再次登记同名定义时验证覆盖被拒绝。纯 Framework 工厂和 Boot 应用的默认覆盖政策不同,实验不依赖默认值。开启覆盖会让后注册定义替换前者;应在明确的兼容扩展点处理,不能用全局覆盖掩盖扫描错误。
定义异常、启动诊断与 AOT
查看最终配方和来源
对指定 Bean 名检查元数据即可,无需为了排障实例化所有 Bean:
var factory = context.getBeanFactory();
var raw = factory.getBeanDefinition("client");
var merged = factory.getMergedBeanDefinition("client");
System.out.println("source=" + raw.getResourceDescription());
System.out.println("class=" + merged.getBeanClassName()
+ " factory=" + merged.getFactoryBeanName()
+ " method=" + merged.getFactoryMethodName()
+ " lazy=" + merged.isLazyInit());仅在 containsBeanDefinition(name) 为 true 时执行。source 是定位信息而非必有字段,程序注册最好主动填写。工厂方法定义可能没有直接 beanClass,判断应同时查看 factory 字段。
定义失败常见三类。缺 Bean 时先检查配置类是否被注册、扫描范围和条件输入;同名冲突看异常里的两份定义来源,缩小扫描或改明确名称后重建 Context;实例化阶段出现构造参数错误时看合并定义和工厂方法签名,不能只重复增加 @Component。若 class 文件无法读取或加载,还应检查实际依赖版本:
docker run --rm --user "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/maven \
-v "$PWD:/work" -v "$PWD/.m2:/cache" -w /work "$IMAGE" \
mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/cache dependency:tree关注是否混入不同 Spring 发行线或不同 Jakarta API,修正依赖管理后从 clean 构建重新运行相同反例。
冻结与 AOT 的含义
freezeConfiguration() 告诉 BeanFactory 配置可视作稳定,从而更积极地缓存类型和合并元数据。它不是 Java 集合意义的不可修改锁;实现仍有注册和缓存重置逻辑。但运行中并发修改活跃定义并非受支持的常规装配方式,需要动态模块时优先使用有明确关闭过程的子 Context。工厂 API
AOT 会把 Bean 定义处理结果和部分创建逻辑提前生成。任意运行时注册、无法预测的工厂返回类型和反射访问因此需要重新审视;RuntimeHints 用于描述反射、资源、代理等需求。应以最终构建方式测试 profile、条件、类型推断和资源读取,不能只确认 IDE 启动正常。AOT
实验对象都在进程结束前释放。保留源码和 POM 可比较升级差异;清理时只移除当前独立目录中的 target 与依赖缓存,不操作其他项目。
权威资料与规范地址
可按接口、注解和实现类查阅详细契约。
