Spring Boot 的注解是如何生效的
在 Spring 中,@Configuration
、@ComponentScan
、@Bean
、@Import
等注解的扫描、解析和 BeanDefinition
注册是一个分层处理的过程。下面我们以 @Configuration
类为例,结合代码流程详细说明其从扫描到注册的完整逻辑。
1. 整体流程概览
以下是核心步骤的流程图:
1. 扫描候选配置类 → 2. 解析注解元数据 → 3. 注册 BeanDefinition
具体分为以下阶段:
- 扫描阶段:通过
BeanDefinitionRegistry
获取所有候选配置类。 - 解析阶段:使用
ConfigurationClassParser
解析注解(如@ComponentScan
、@Bean
、@Import
)。 - 注册阶段:通过
ConfigurationClassBeanDefinitionReader
将解析结果注册为BeanDefinition
。
2. 详细步骤解析
2.1 扫描阶段:识别候选配置类
触发入口:
ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
逻辑:
- 从
BeanDefinitionRegistry
获取所有已注册的BeanDefinition
名称:String[] beanNames = registry.getBeanDefinitionNames();
- 遍历这些名称,检查对应的
BeanDefinition
是否是候选配置类:- 条件:类上有
@Configuration
、@Component
、@ComponentScan
、@Import
、@ImportResource
,或类中有@Bean
方法。 - 判断逻辑:
if (isFullConfigurationCandidate(beanDef) || isLiteConfigurationCandidate(beanDef)) {configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); }
isFullConfigurationCandidate(beanDef)
:检查是否有@Configuration
注解。isLiteConfigurationCandidate(beanDef)
:检查是否有其他相关注解(如@Component
、@Bean
方法)。
- 条件:类上有
关键点:
- 扫描的输入是已注册的
BeanDefinition
(可能来自 XML、Java Config 或自动扫描)。 - 此时尚未解析注解内容,仅识别出需要进一步处理的候选类。
2.2 解析阶段:处理注解元数据
核心类:ConfigurationClassParser
入口方法:parse()
逻辑:递归解析每个候选配置类的注解。
(1) 解析 @ComponentScan
- 作用:扫描指定包路径下的
@Component
类(如@Service
、@Repository
)。 - 流程:
- 获取
@ComponentScan
注解的basePackages
或basePackageClasses
。 - 使用
ClassPathBeanDefinitionScanner
扫描类路径:scanner.scan(basePackages);
- 扫描到的类会被注册为新的
BeanDefinition
(类型为ScannedGenericBeanDefinition
)。
- 获取
- 关键点:
- 扫描时使用
ASM
或反射读取类注解,避免提前加载类到 JVM。 - 新注册的
BeanDefinition
可能也会被后续解析(如果它们也是配置类)。
- 扫描时使用
(2) 解析 @Bean
方法
- 作用:将配置类中的
@Bean
方法转换为BeanDefinition
。 - 流程:
- 遍历配置类中的所有方法,筛选带
@Bean
注解的方法。 - 为每个
@Bean
方法生成一个BeanDefinition
:- 类型:
ConfigurationClassBeanDefinition
。 - 工厂方法:设置为
@Bean
方法(通过factoryMethodName
和factoryBeanName
指定)。 - 依赖:解析
@Bean
方法的参数(按类型或@Qualifier
注入)。
- 类型:
- 遍历配置类中的所有方法,筛选带
- 示例:
@Configuration public class AppConfig {@Beanpublic DataSource dataSource() {return new HikariDataSource();} }
- 生成的
BeanDefinition
会记录:factoryBeanName=appConfig
,factoryMethodName=dataSource
。
- 生成的
(3) 解析 @Import
- 作用:动态导入其他配置类或
BeanDefinition
。 - 三种处理方式:
- 普通类:直接注册为
BeanDefinition
。@Import(OtherConfig.class)
ImportSelector
:通过编程方式选择要导入的类。@Import(MyImportSelector.class)
MyImportSelector
实现selectImports()
方法,返回要导入的类名数组。
ImportBeanDefinitionRegistrar
:直接注册BeanDefinition
。@Import(MyRegistrar.class)
MyRegistrar
实现registerBeanDefinitions()
方法,手动操作BeanDefinitionRegistry
。
- 普通类:直接注册为
(4) 处理父类与接口
- 递归检查配置类的父类和接口,确保不遗漏任何
@Bean
方法或元注解。
2.3 注册阶段:加载 BeanDefinition
核心类:ConfigurationClassBeanDefinitionReader
入口方法:loadBeanDefinitions()
逻辑:将解析结果(ConfigurationClass
对象)转换为 BeanDefinition
并注册到容器。
(1) 注册 @Import
的类
- 普通类:通过
registry.registerBeanDefinition()
直接注册。 ImportBeanDefinitionRegistrar
:调用其registerBeanDefinitions()
方法。
(2) 注册 @Bean
方法
- 为每个
@Bean
方法生成BeanDefinition
并注册:for (BeanMethod beanMethod : configClass.getBeanMethods()) {loadBeanDefinitionsForBeanMethod(beanMethod); }
(3) 处理嵌套配置类
- 如果配置类内部有
@Configuration
静态嵌套类,递归处理。
3. 关键设计点
(1) 延迟加载与递归处理
- 延迟加载:
@ComponentScan
扫描到的类可能也是配置类,需要递归解析。 - 循环依赖处理:Spring 通过提前暴露
BeanDefinition
解决配置类之间的循环引用。
(2) 元数据存储
ConfigurationClass
对象存储解析后的中间结果(如@Bean
方法、@Import
类等)。BeanDefinition
的attribute
字段存储配置类的元信息(如@Lazy
、@Primary
)。
(3) 性能优化
- ASM 字节码分析:在扫描阶段避免加载类到 JVM。
- 缓存:解析结果缓存到
ConfigurationClass
中,避免重复处理。
4. 示例全流程
场景
@Configuration
@ComponentScan("com.example.service")
@Import(OtherConfig.class)
public class AppConfig {@Beanpublic DataSource dataSource() {return new HikariDataSource();}
}
步骤
- 扫描阶段:
- 发现
AppConfig
是候选配置类(有@Configuration
)。
- 发现
- 解析阶段:
- 解析
@ComponentScan
:扫描com.example.service
包,注册@Service
类。 - 解析
@Import(OtherConfig.class)
:递归处理OtherConfig
。 - 解析
@Bean dataSource()
:生成工厂方法BeanDefinition
。
- 解析
- 注册阶段:
- 注册
OtherConfig
及其@Bean
方法。 - 注册
dataSource
的BeanDefinition
。
- 注册
5. 总结
Spring 对配置类注解的处理是一个分层递归的过程:
- 扫描:通过
BeanDefinitionRegistry
筛选候选类。 - 解析:
ConfigurationClassParser
解析注解并生成中间结果(ConfigurationClass
)。 - 注册:
ConfigurationClassBeanDefinitionReader
将解析结果转换为BeanDefinition
。
这种设计将注解元数据解析与 BeanDefinition
注册分离,确保了灵活性和扩展性(如支持动态 ImportSelector
)。同时,递归处理和缓存机制解决了复杂依赖和性能问题。