Spring Boot 注解方式如何扫描并注册 BeanDefinition?
1. 引言
在 Spring Boot 时代,「零 XML」几乎成了标配——只写几个注解,容器就能自动把 Bean 注册进来。
可你有没有好奇过:Spring 到底是怎么把散落在各个包里的类,变成内存里的 BeanDefinition
的?
2. 全景概览(一张图先记住)
@SpringBootApplication
└─ @ComponentScan —— 1. 指定扫描范围
↓
ConfigurationClassPostProcessor —— 2. 解析所有配置类
↓
ConfigurationClassParser#doProcessConfigurationClass
↓
ComponentScanAnnotationParser → ClassPathBeanDefinitionScanner
↓
ASM 读取 *.class 字节码 —— 3. 无需加载类即可拿到注解元数据
↓
过滤 → 生成 ScannedGenericBeanDefinition —— 4. 封装成 BeanDefinition
↓
DefaultListableBeanFactory.registerBeanDefinition() —— 5. 塞进 ConcurrentHashMap
下面分步展开。
3. 启动入口:@SpringBootApplication
@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
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
解析成
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 长什么样?
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 都会事半功倍。