Spring Boot 在容器的初始化中使用 @Configuration 注解代替了 applicationContext.xml 文件, 省略了之前在 xml 文件中配置 bean,配置 bean 扫描等过程。Spring Boot 是如何使用 @Configuration 注解实现这些功能的呢?本文从这个问题出发,探讨以下三点内容。
- 配置类(Configuration 注解的类)是什么时候生效的?
- 配置类中可以配置些什么?解析流程是怎样的?
- Spring Boot 默认的配置是什么样的?
以下所有的分析基于 Spring Boot 2.4。
BeanFactoryPostProcessor 是什么
想要了解配置类是什么时候生效的,首先要了解 BeanFactoryPostProcessor。我们看下 Spring 中 Bean 的生成过程。简单来讲,Spring 会先生成 BeanFactory(也就是 IOC 容器),再由 BeanFactory 生成 Bean。BeanFactory 是个 Factory,也就是 IOC 容器或对象工厂。它的职责包括:实例化、定位、配置 Bean 对象及建立这些对象间的依赖。在 Spring 中,所有的 Bean 都是由 BeanFactory 来进行管理的。
BeanFactory 首先会从 xml 文件或注解配置中读取 Bean 的各种信息,Spring 将这些信息称为 BeanDefinition。一般而言,我们通过 BeanDefinition 就可以直接生成 Bean 了。但是 Spring 饶了这么大一个圈子肯定不是为了简单的生成一个 Bean。Spring 会读取 BeanDefinition 的信息,区分出特殊的 Bean(实现 BeanFactoryPostProcessor接口的 Bean)。这些特殊的 Bean 会被优先处理。
BeanFactoryPostProcessor ---> 普通 Bean 构造方法。所以,BeanFactoryPostProcessor 用于扩展或是修改当前已经加载好的 BeanDefinition。这样就可以在真正初始化Bean 之前对 Bean 做一些处理操作,这也是 Spring 一个很重要的扩展点。
@FunctionalInterface
public interface BeanFactoryPostProcessor {
/**
* Modify the application context's internal bean factory after its standard
* initialization. All bean definitions will have been loaded, but no beans
* will have been instantiated yet. This allows for overriding or adding
* properties even to eager-initializing beans.
* @param beanFactory the bean factory used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}
为什么会有 BeanDefinitionRegistryPostProcessor
Spring Boot 中大部分 BeanFactoryPostProcessor 是在初始化 context 实例的时候加入的,也有一部分是以 ApplicationContextInitializer 的形式加入容器中的,详见Spring Boot 扩展 ApplicationContextInitializer 的方式。简单来讲,SPring Boot 扩展了初始化器,这些初始化器会生成一些 BeanFactoryPostProcessor 的实现类,加入到容器中。 我们暂且不用关心到底是哪些 BeanFactoryPostProcessor。
BeanFactoryPostProcessor 有一个比较麻烦的地方,在于它可以随意的增加 BeanFactory 中的 BeanDefinition。假设我们现在有两个 Bean 以及两个 BeanFactoryPostProcessor,一个 BeanFactoryPostProcessor 是用来增加 Bean 的,一个 BeanFactoryPostProcessor 是为所有的 Bean 增加一个功能。那很明显,用来增加 Bean 的 BeanFactoryPostProcessor 应该优先执行,才能保证后一个 BeanFactoryPostProcessor 的功能可以覆盖所有的 Bean。为了实现这种效果,Spring 中设定了一个 BeanFactoryPostProcessor 的子接口 BeanDefinitionRegistryPostProcessor。
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
/**
* Modify the application context's internal bean definition registry after its
* standard initialization. All regular bean definitions will have been loaded,
* but no beans will have been instantiated yet. This allows for adding further
* bean definitions before the next post-processing phase kicks in.
* @param registry the bean definition registry used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}
如果一个 BeanFactoryPostProcessor 是用来为容器增加 BeanDefinition 的,那就继承 BeanDefinitionRegistryPostProcessor。Spring 通过这种方式区分 BeanFactoryPostProcessor,也用来保证 BeanFactoryPostProcessor 的执行顺序。现在,我们的执行顺序变成了这样。
BeanDefinitionRegistryPostProcessor --> BeanFactoryPostProcessor --> 普通 Bean 构造方法
需要注意的是,BeanDefinitionRegistryPostProcessor 注册的 BeanDefinition 可能也是一个 BeanDefinitionRegistryPostProcessor,所以在注册结束后,还需要重新判断是否加入了新的 BeanDefinitionRegistryPostProcessor,如果是,那就要执行新加入的 BeanDefinitionRegistryPostProcessor 的注册方法,直到数量不再增加,才能认为所有的 BeanDefinition 都加载到了容器中。如下方法截取自 PostProcessorRegistrationDelegate 类 invokeBeanFactoryPostProcessors 方法。
boolean reiterate = true;
while (reiterate) {
reiterate = false;
// 获得当前容器中的 BeanDefinitionRegistryPostProcessor 实现类
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
// 判断这个类是否是新加入的
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
// 排序
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 执行 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
}
invokeBeanDefinitionRegistryPostProcessors 最终执行 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法,完成 BeanDefinition 的注册。
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {
for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process")
.tag("postProcessor", postProcessor::toString);
postProcessor.postProcessBeanDefinitionRegistry(registry);
postProcessBeanDefRegistry.end();
}
}
ConfigurationClassPostProcessor 加入容器的流程
了解了 BeanFactoryPostProcessor 后,我们再回过头来看配置类的解析方式。Configuration 主要是用于向容器中注入新的 Bean 的,肯定是由某个 BeanDefinitionRegistryPostProcessor 处理。这个 BeanDefinitionRegistryPostProcessor 其实就是 ConfigurationClassPostProcessor,看名字也知道是用来处理配置类的,那 ConfigurationClassPostProcessor 是如何加入容器的呢?
Spring Boot 初始化的时候,会执行 createApplicationContext 方法,用于创建上下文实例,默认的类型是 AnnotationConfigServletWebServerApplicationContext,这个类在实例化的过程中会调用如下代码,这段代码会在容器中注册一些 BeanDefinition。其中有一个就是 ConfigurationClassPostProcessor。
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, @Nullable Object source) {
...
Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);
if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
...
return beanDefs;
}
这就接上了上面的流程,ConfigurationClassPostProcessor 作为一个 BeanDefinitionRegistryPostProcessor,会在上下文初始化完毕后,向容器中添加的额外的 BeanDefinition。
读取 @SpringBootApplication 配置
Spring Boot 在 context 初始化的时候,除了在 registerAnnotationConfigProcessors 方法中默认加入的 BeanDefinition 外。还有一个就是 Spring Boot 的启动类,也就是被 @SpringBootApplication 修饰的类。请注意,由于此时的 Spring 并不知道扫描策略,也就不会从项目下加载 Bean。可以认为此时的 Spring 容器只有启动类以及初始化过程默认加入的类。
那整个项目是如何启动起来的呢?我们不要忽略 @SpringBootApplication 注解,@SpringBootApplication 注解是一个组合注解,本身就包含了 @Configuration 在内的多个注解,用于快捷配置启动类。@SpringBootApplication 中包含了当前容器加载 Bean 的默认配置,这也是我们基本不用自己写配置的原因。
在 BeanDefinitionRegistryPostProcessor 执行阶段中,ConfigurationClassPostProcessor 会遍历当前所有的 BeanDefination,判断该类是否是配置类,如果是的话,会被打上一个标记,这个类会被认为是配置类,然后进行后续的处理。
这里有两个不同的标记,CONFIGURATION_CLASS_FULL 和 CONFIGURATION_CLASS_LITE。CONFIGURATION_CLASS_FULL 很好理解,就是带有 @Configuration 注解的。CONFIGURATION_CLASS_LITE 是什么时候用呢?
并不是只有 @Configuration 才能增加新的 BeanDefination,普通的用 @component 注解的类,如果其中的某个方法,使用 @Bean 注解,其实也能生成一个新的 BeanDefination,或者使用@ComponentScan,@Import,@ImportResource 注解的类也会生成新的类,这样类就用 CONFIGURATION_CLASS_LITE 标记,可以认为它也是一种配置类。
如下代码用于判断一个类是否是配置类。
...
Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
// 带有 @Configuration 注解的,会被标记为 CONFIGURATION_CLASS_FULL
// proxyBeanMethods 是 @Configuration 参数,默认为 true
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}
// 只要有 @Component,@ComponentScan,@Import,@ImportResource 这几个注解之一
// 或是有包含 @Bean 的方法,标记为 CONFIGURATION_CLASS_LITE
else if (config != null || isConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
else {
return false;
}
如下是对 CONFIGURATION_CLASS_LITE 类型的配置类的判断代码。从中可以看出 并不是只有 @Configuration 注解的类才是配置类,只要某个类有 @Component,@ComponentScan,@Import,@ImportResource 这几个注解之一,或是包含 @Bean 注解的方法,就是一个配置类
如下是判断 CONFIGURATION_CLASS_LITE 类型的配置类的代码。
static {
candidateIndicators.add(Component.class.getName());
candidateIndicators.add(ComponentScan.class.getName());
candidateIndicators.add(Import.class.getName());
candidateIndicators.add(ImportResource.class.getName());
}
public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
if (metadata.isInterface()) {
return false;
}
for (String indicator : candidateIndicators) {
if (metadata.isAnnotated(indicator)) {
return true;
}
}
// let's look for @Bean methods...
try {
return metadata.hasAnnotatedMethods(Bean.class.getName());
}
catch (Throwable ex) {
return false;
}
}
在达成配置这个目的上看,@Component 可以实现和 @Configuration 一样的效果。当然,这两者在实现上(生成具体对象的方式不同)还是有一些区别的。
总之,代码启动后,@SpringBootApplication 注解的类会被认为是配置类,然后读取默认配置,进行类文件的扫描和加载,判断扫描到的类是否是配置类,读取配置,这样不断循环,直到扫描的类数目不再发生变化。
配置类解析流程
ConfigurationClassPostProcessor 解析配置类的流程比较复杂。
1. 处理内部类
配置类如果包含内部类的话,首先处理内部类。内部类也可能是一个配置类,判断方式也是上面判断内部类的方式,这是个递归处理的过程。
2. 处理 @PropertySource 注解
PropertySource 注解,目的是加载指定的配置文件,如下就是加载 resources 文件夹下的 demo.properties 文件。将其作为一个配置源。Spring Boot 核心接口之 Envirnoment 文章中我详细讲过配置源,就不赘述了。
@PropertySource(value = {"classpath:demo.properties"})
这种方式加入的配置源优先级很低,低于 application.properties 配置文件的优先级。
3. 处理 @ComponentScan 注解
@ComponentScan 就是根据配置的扫描路径,把符合扫描规则的类装配到容器中。该注解默认会扫描该类所在的包下所有的配置类。
如果需要指定扫描的包,那就使用 @ComponentScan 的 valule 属性来配置。使用 excludeFilters 来按照规则排除某些包的扫描。使用 includeFilters 来按照规则只包含某些包的扫描。具体的逻辑后面单独看。
4. 处理 @Import 注解
@Import 有三种方式,最简单的就是直接 Import 一个类,这个类会被加载到 Spring 容器中。
@Import({ Cat.class})
class InnerConfiguration {}
第一种方式是比较死板的,所以 Spring 还提供了 ImportSelector 接口,我们只要继承了这个接口,就可以动态的指定类名称。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.shenmax.xiang.configuration.Cat"};
}
}
此时,只需把 @Import 的参数换成 ImportSelector 的实现类即可。
@Import({ MyImportSelector.class})
class InnerConfiguration {}
第三种方式和第二种比较相似,需要实现 ImportBeanDefinitionRegistrar 接口,这种用法自定义注册 BeanDefination,很少使用。
整体代码流程很清晰,首先判断导入的类是否实现了 ImportSelector,接着判断 ImportBeanDefinitionRegistrar,都不是的话,直接导入。
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
...
if (selector instanceof DeferredImportSelector) {
...
}
...
}
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
...
}
else {
...
}
}
5. 处理 @ImportResource 注解
@Import 的对象是一个或多个类,@ImportResource 的对象就是一个或多个配置文件路径。本质是读取 xml 文件,将配置文件中的 Bean 配置项加载到容器中。
@ImportResource(value = "xxx.xml")
6. 处理 @Bean 方法
之前的注解都是放在类上的,@Bean 是放在方法上的。ConfigurationClassPostProcessor 会扫描配置类的方法,只要发现了 @Bean 注解,就将该方法的返回值加载容器中。
@Configuration
public class MyConfiguration {
@Bean
public Persion persion() {
return new Persion();
}
}
7. 处理父类
获得当前类的父类,然后将上面的流程再走一遍。当然,不管是处理父类还是子类,都是需要判断是否已经处理过,否则就死循环了。
Spring Boot 默认配置
@SpringBootApplication 注解中,涉及到配置类的只定义了 @ComponentScan,我们只要搞清楚这个配置的含义就可以了。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
@ComponentScan 注解由 ComponentScanAnnotationParser 中的 parse 方法处理。如下,我们可以看出,@ComponentScan 支持配置 basePackages 或 basePackageClasses,用于指定扫描的根路径,如果都没有配置的话,则默认根路径是 ClassUtils.getPackageName(declaringClass),也就是当前 @ComponentScan 直接类所在的包路径。
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
有了这个路径后,就需要从这个路径下读取文件,如下是读取文件的相关代码。
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
最终 packageSearchPath 的值是 classpath*:xxx/xxx/xxx/**/*.class。表示扫描当前路径下的任意层级的 .class 后缀的文件。这并没有结束,后续还需要对读取到的类做过滤。
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
...
}
根据我们配置的 excludeFilters 和 includeFilters。需要注意的是,includeFilters 是有默认值的,我们如果没有申明 useDefaultFilters = false,那么这里使用的就是默认的 includeFilters。默认 Component 和 ManagedBean 都在扫描范围内。@Controller, @Service 和 @Repository 都继承了 @Component,所以也会是会被扫描到的。
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
综上, Spring Boot 默认会扫描入口文件所在包下的任意层级的 .class 文件,该类需要被 @Component 或 @ManagedBean 注解,且不能是 TypeExcludeFilter 或 AutoConfigurationExcludeFilter。
总结
-
Spring 提供了 BeanFactoryPostProcessor 拓展点, 用于在 BeadDefination 加载到容器后,依然能修改 BeadDefination。
-
BeanDefinitionRegistryPostProcessor 是特殊的 BeanFactoryPostProcessor,用于注册新的 BeadDefination 到容器中。
-
ConfigurationClassPostProcessor 就是一个 BeanDefinitionRegistryPostProcessor,用于解析配置类,根据配置类的配置加载类到容器中。
-
配置类分为 full 和 lite 两类。
-
并不是只有 @Configuration 注解的类才是配置类,只要某个类有 @Component,@ComponentScan,@Import,@ImportResource 这几个注解之一,或是包含 @Bean 注解的方法,就是一个配置类
-
配置类的解析流程是这样的:内部类 -> @PropertySource -> @ComponentScan -> @Import -> @ImportResource -> @Bean -> 父类。
-
@SpringBootApplication 注解是一个组合注解,其中包含了 @Configuration 和 @ComponentScan 等多个注解。@ComponentScan 的默认路径是该配置类所在包路径。
如果您觉得有所收获,就请点个赞吧!