Spring Boot 注解方式如何扫描并注册 BeanDefinition?
1. 引言
在 Spring Boot 时代,「零 XML」几乎成了标配------只写几个注解,容器就能自动把 Bean 注册进来。
可你有没有好奇过:Spring 到底是怎么把散落在各个包里的类,变成内存里的 BeanDefinition 的?
2. 全景概览(一张图先记住)
less
@SpringBootApplication
└─ @ComponentScan ------ 1. 指定扫描范围
↓
ConfigurationClassPostProcessor ------ 2. 解析所有配置类
↓
ConfigurationClassParser#doProcessConfigurationClass
↓
ComponentScanAnnotationParser → ClassPathBeanDefinitionScanner
↓
ASM 读取 *.class 字节码 ------ 3. 无需加载类即可拿到注解元数据
↓
过滤 → 生成 ScannedGenericBeanDefinition ------ 4. 封装成 BeanDefinition
↓
DefaultListableBeanFactory.registerBeanDefinition() ------ 5. 塞进 ConcurrentHashMap
下面分步展开。
3. 启动入口:@SpringBootApplication
java
@SpringBootApplication // 复合注解
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication =
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan // ← 今天的主角
默认扫描 当前主类所在包及其子包 ,可通过 basePackages / basePackageClasses 调整。
4. 触发时机:ConfigurationClassPostProcessor
- 容器启动
refresh()→invokeBeanFactoryPostProcessors() - 优先级最高的
BeanDefinitionRegistryPostProcessor就是
ConfigurationClassPostProcessor(简称 CCPP)。 - CCPP 会解析所有标了
@Configuration的类(包括主类),找出@ComponentScan并真正执行扫描。
org.springframework.context.support.AbstractApplicationContext#refresh
java
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
5. 核心源码链路
| 阶段 | 关键类 / 方法 | 作用 |
|---|---|---|
| 解析配置类 | ConfigurationClassParser#doProcessConfigurationClass |
发现 @ComponentScan |
| 转给扫描器 | ComponentScanAnnotationParser#parse |
封装扫描参数 |
| 执行扫描 | ClassPathBeanDefinitionScanner#doScan |
遍历包路径 |
| 类元数据 | ASM MetadataReader |
不加载类就能读注解 |
| 条件过滤 | ConditionEvaluator |
处理 @ConditionalOnClass 等 |
| 注册定义 | DefaultListableBeanFactory#registerBeanDefinition |
放入 ConcurrentHashMap |
6. 细节拆解
6.1 包路径如何变成 *.class 文件?
ResourcePatternResolver 把 com.example.demo 解析成
ruby
classpath*:com/example/demo/**/*.class
支持 jar、file、nested jar 等多种协议。
6.2 为什么不用反射?------ ASM 提速
Spring 使用 org.springframework.asm.ClassReader 直接解析字节码,拿到:
- 类名、修饰符
- 注解信息(
@Component、@Scope、@Lazy...)
好处:
- 不触发类加载,快
- 避免
ClassNotFoundException陷阱
6.3 过滤规则一览
| 注解 | 默认是否包含 | 说明 |
|---|---|---|
@Component |
✔️ | 元注解,派生 @Service/@Repository/@Controller |
@Conditional... |
✔️ | 不满足条件直接跳过 |
includeFilters / excludeFilters |
自定义 | 可扩展自己的 TypeFilter |
6.4 BeanDefinition 长什么样?
java
ScannedGenericBeanDefinition bd = new ScannedGenericBeanDefinition(metadata);
bd.setBeanClassName("com.example.demo.service.UserService");
bd.setScope("singleton");
bd.setLazyInit(false);
bd.setPrimary(false);
...
registry.registerBeanDefinition("userService", bd);
7. @Import & @EnableAutoConfiguration 补充
@Import引入的普通类会递归解析;
如果是ImportSelector,会再批量导入一批配置类。@EnableAutoConfiguration通过spring.factories/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports加载 AutoConfiguration 类 ,同样走条件过滤 → 注册BeanDefinition。
自动配置类也是靠同一套
ConfigurationClassParser机制,只是入口不同。
8. 调试技巧速查表
| 目标 | 操作 |
|---|---|
| 想看扫到哪些类 | logging.level.org.springframework.context.annotation=DEBUG |
| 想看条件评估 | logging.level.org.springframework.boot.autoconfigure=DEBUG |
| 断点调试 | ConfigurationClassParser#doProcessConfigurationClass |
| 加速冷启动 | 引入 spring-context-indexer 并编译时生成 META-INF/spring.components |
9. 小结一句话
Spring Boot 的"魔法"并不神秘:启动时用一个后置处理器(CCPP)解析
@ComponentScan,借助 ASM 把类变成BeanDefinition塞进容器,后面getBean时直接实例化即可。
搞懂这一条链路,阅读 Spring Boot 源码或写自定义 starter 都会事半功倍。