截止目前,我们已经写了四篇关于Spring源码解析的文章,但它们只关注了Spring和SpringBoot的启动流程,并没有回答我们日常使用中的核心问题——自动配置的实现原理,本文进行探索
在前文“Spring源码剖析 - BeanDefinition”中扫描Bean定义一节中,我们漏掉了一个关键方法调用,其调用路径如下
1 | org.springframework.context.annotation.AnnotationConfigApplicationContext |
即,在AnnotationConfigApplicationContext
执行构造方法时,调用AnnotationConfigUtils.registerAnnotationConfigProcessors()
,注册了一系列注解处理器,具体来说,有
ConfigurationClassPostProcessor
用于处理配置相关的各种注解
AutowiredAnnotationBeanPostProcessor
用于处理自动注入相关的注解,如
@Autowired
、@Value
、@Inject
CommonAnnotationBeanPostProcessor
用于处理一些通用注解,比如
@Lazy
、@Primary
、PreDestroy
等PersistenceAnnotationBeanPostProcessor
用于处理JPA相关注解
EventListenerMethodProcessor
用于处理
@EventListener
,即把方法注册成监听器DefaultEventListenerFactory
结合
EventListenerMethodProcessor
使用,该工厂输入方法,可创建出一个ApplicationListener
类
本文我们重点关注配置相关的注解
概览
ConfigurationClassPostProcessor
继承结构如上,属于容器构建阶段的处理器,我们再复习一下它的两个父接口的作用
BeanFactoryPostProcessor
:在容器初始化之后,对容器做一些自定义操作。具体时机是:BeanDefinition
加载完成之后,Bean
实例化之前。BeanDefinitionRegistryPostProcessor
:在容器初始化完成之后,BeanFactoryPostProcessor
执行之前对容器做一些自定义操作。
而ConfigurationClassPostProcessor
在这两个时机分别做了两件事
容器初始化完成后,
BeanFactoryPostProcessor
调用前,从@Configuration
注解的类中加载Bean定义,然后向容器注册。这一点是本文的重点 ,逻辑位于
org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
,进一步的确切逻辑位于注解解析:
->org.springframework.context.annotation.ConfigurationClassParser#parse(java.util.Set<org.springframework.beans.factory.config.BeanDefinitionHolder>)
->
org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass
BeanDefinition
注册->
org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitions
。其它逻辑无助于理解,一概忽略
容器初始化完成后,对所有
@Configuration
注解标识的BeanDefinition
进行增强这一点不是重点,大概描述一下:使用的是CGLib的Enhancer API。添加了如下几个增强回调
BeanMethodInterceptor
:拦截所有@Bean
注解的方法BeanFactoryAwareMethodInterceptor
:拦截所有实现了BeanFactoryAware
的@Configuration
类的setBeanFactory()
方法,目的是将容器注入
注解解析
我们从ConfigurationClassParser.doProcessConfigurationClass
的解析逻辑中,分析如下几个注解的工作原理。
@Conditional
该注解用于条件配置,即满足条件时才生效,否则被忽略,它的实现由ConditionEvaluator
完成。使用的地方包括
ConfigurationClassParser.processConfigurationClass()
,对配置类有效。ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod
,加载Bean时有效,即作用在@Bean
注解的方法上
逻辑落地在ConditionEvaluator.shouldSkip()
- 如果没有被
@Conditional
注解,则不应被跳过 - 获取
@Conditional
注解的属性值,即其指定的Condition
类,可能有多个 - 依次调用这些
Condition
类的matches()
方法,只要有一个匹配,则判定为应该被跳过,可见,多条件是与的关系
SpringBoot还定义了ConditionalOnClass
、ConditionalOnMissingBean
之类的注解,其实看一眼他们的定义就知道。
1 |
|
比如上面这个注解,它是@Conditional
注解加上OnClassCondition
这个条件构成的。而在OnClassCondition
的匹配方法的逻辑如下(有继承关系,matches()
方法在父类,这里只给出核心逻辑):类加载器能加载出来就匹配,否则就不匹配
1 | public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { |
@PropertySource
该注解能够将指定的properties文件加载到容器的Environment中,使用参考。
通过源码解读可以看出它的逻辑
- 读取
@PropertySource
的value值,作为配置文件的位置location - 解析
location
中的占位符,这意味着location中可以存在动态字段 - 使用
resourceLoader
将location解析成Resource
对象 - 使用
PropertySourceFactory
创建PropertySource
对象,并加入Environment
这里有一个能够自定义的东西:属性源工厂,如果我们指定自己的属性源工厂,则能够按照自己的需求解析,因此我们能够自定义出解析yaml文件的逻辑。默认是DefaultPropertySourceFactory
,它创建的是一个ResourcePropertySource
。
我们大致看一下它的核心逻辑
1 | // 解析注解的属性:name、encoding、value、ignoreResourceNotFound、factory |
@ComponentScan
该注解用于扫描指定包、类所属的包下的Bean定义,其核心逻辑挂在ComponentScanAnnotationParser.parse()
上,如下
构建
ClassPathBeanDefinitionScanner
扫描器,用于执行Bean定义扫描读取
@ComponentScan
的各类属性,写入扫描器,属性包括nameGenerator
:Bean名生成器scopedProxy
:scopedProxy的方式:不代理、JDK代理、cglib代理,关于scopedProxy,可以参考这里。大致来说,就是当scope范围更宽的bean1引用scope范围较小的bean2时,由于他们的作用范围不一致,导致bean1的生存时间大于bean2的生存时间,直接引用会发生问题,此时可以将bean2用一个代理包装起来,由代理负责引用实际的Bean,每当bean2的生命结束时,由代理自动创建新的实例,对bean1做到透明。scopeResolver
:Scope解析器resourcePattern
:扫描目标文件的通配符,如*/**/class
includeFilters
:过滤器excludeFilters
:过滤器lazyInit
:延迟加载basePackages
:基包basePackageClasses
:基类
调用扫描器的扫描方法,这个在前面介绍扫描BeanDefinition时有描述过,这里不再赘述。
源码也简单,只要注意一点:如果没有指定基包或基类,就是用当前配置类所在的包作为扫描位置
1 | public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { |
@Import
该配置用于导入其它配置,定义如下
1 |
|
如注释所言,支持导入的对象有四种
被
@Configuration
注解的类,即一个普通的配置类,这是最常用的。对它的处理方式就是,再当做配置类执行一遍processConfigurationClass()
方法逻辑ImportSelector
,顾名思义,导入选择器,这是一个接口,定义了要导入的资源,导入的资源依旧是这四类,但最终会是普通的注解类。对于该类对象的处理方式,是找出其选择的导入对象,递归执行导入逻辑
ImportBeanDefinitionRegistrar
,顾名思义,用于导入BeanDefinition
注册器,其逻辑是向ConfigurationClassBeanDefinitionReader
中注入该注册器,后面在加载BeanDefinition
时会用到它。
导入选择器
1 | if (candidate.isAssignable(ImportSelector.class)) { |
所以如果导入对象是ImportSelector
,它所做的工作其实只有选择待导入的配置对象。
导入BeanDefinition集合
1 | else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { |
在读取Bean定期时是有调用的,在ConfigurationClassBeanDefinitionReader
中可以看到,它的调用在概览中有描述,在下一节也会描述
1 | // 在类ConfigurationClassBeanDefinitionReader中 |
导入普通配置
1 | else { |
小结
总的来说,@Import
原理上其实非常简单,最终目的是导入其它配置类或BeanDefinition
集合。
@ImportResource
该注解用于指定导入源,Spring会将这些源当做配置源进行读取,他们可以是xml、groovy,这是根据资源文件的后缀自动决定的。接口定义如下
1 |
|
在注解扫描阶段,对它只是进行了读取
1 | AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); |
什么时候执行呢?和前面说的BeanDefinition
集合一样,都是在加载阶段,后文详述。
@Bean
该注解用在方法上,将方法的返回值构建成一个新的Bean定义,解析阶段只是向configClass注册了待加载的方法,解析阶段后文详述。
1 | // 从类中加载出所有被Bean注解的方法 |
此外,如果配置类实现的接口的方法有被@Bean
注解,且该配置类实现了该方法,则该方法也会被加载为Bean。
BeanDefinition注册
我们从ConfigurationClassBeanDefinitionReader.loadBeanDefinitions
大致看一下加载过程中做了什么事情。
核心逻辑位于:org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass
1 | private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { |
- 如果配置类是被导入的,则先将该配置类注册成为一个Bean定义
- 将所有被
@Bean
注解的方法构建成Bean定义 - 从
@ImportResource
指定的资源中加载Bean定义 - 从
@Import
导入的BeanDefinitionRegistrar
中导入Bean定义
注册配置类本身
1 | private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) { |
如上,比较简单,不过这里有个值得说的点:处理通用注解AnnotationConfigUtils.processCommonDefinitionAnnotations()
,它主要用于处理如下注解
Lazy
Primary
DependsOn
Role
Description
都不需要怎么解释,看一下代码就能了解
1 | static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) { |
@Bean
关于Bean方法的处理,看起来一长串,其实就两个关键的逻辑
- 使用
ConfigurationClassBeanDefinition
作为Bean定义,它将被注解的方法注册成了Bean定义的工厂方法。这样在Bean创建时走的完全是正常的创建流程 - 以代理的方式创建,代理模式为CGLIB
具体参考ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod
,这里也没必要列出来源码。
@ImportResource
创建指定的BeanDefinitionReader
实例,然后读取指定的location即可。
1 | private void loadBeanDefinitionsFromImportedResources(Map<String, Class<? extends BeanDefinitionReader>> importedResources) { |
BeanDefinitionRegistrar
更简单,在第一篇Spring源码剖析的文章中我们提到过Registry
和Registrar
的区别,前者是注册器,用于接收资源的注册并持有;而后者只是一个资源集合,持有一堆资源,一般调用方式是传入Registry
,向其中倾泻自己持有的资源。
这里的BeanDefinitionRegistrar
就是如此,持有一堆BeanDefinition
集合,在本阶段向注册器中注册
1 | private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) { |
自动配置怎么实现
上一篇介绍SpringBoot启动流程的文章中,我们知道了SpringApplication
在快速启动中做了什么,这其中,最最重要的是通过META/spring.factories
加载指定类的实现的机制,即SpringFactoriesLoader.loadFactory()
方法。但上一篇文章并没有介绍自动配置的原理,因为还缺少@Import
注解的说明。
本文我们补上了这一点,于是可以探索@SpringBootApplication
这一SpringBoot的另一核心的原理,其定义如下
1 |
|
层层深入,可以发现它最终落在三个注解上
@Configuration
:表明这是一个配置类@Import(AutoConfigurationImportSelector.class)
+@Import(AutoConfigurationPackages.Registrar.class)
:导入配置@ComponentScan
:表明从当前类的包的子包下扫描
进一步,重点落在AutoConfigurationImportSelector
和AutoConfigurationPackages.Registrar
上,重点看前者,后者与自动配置无关,暂且忽略。
AutoConfigurationImportSelector
该选择器,调用了SpringFactoriesLoader.loadFactory()
,从spring.factories文件中加载键为org.springframework.boot.autoconfigure.EnableAutoConfiguration
所对应的值,核心逻辑追踪如下
1 | org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports |
再看最后这个方法的实现,显而易见了
1 | protected Class<?> getSpringFactoriesLoaderFactoryClass() { |
看看自动配置
官方的spring-boot-autoconfigure
下的META-INF/spring.factories
中,定义了非常多的自动配置类
1 | # Auto Configure |
我们选取一个比较熟的org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration
,浏览一下其代码。
1 |
|
总结
通过spring.factories
机制和@Import + ImportSelector
的形式,SpringBoot为实现自动注解提供了最基础的能力,但根据各个具体不同的场景,Spring还在上面提到的基本注解之上构建了非常多具体的注解,比如条件注解就好多。一个个看是不现实的,但我们了解了本文提到的基础知识,再去看就很好理解了。