1. 前言
在早期的 Spring 项目中,使用 XML 文件进行配置。如下所示,在 applicationContext.xml
配置文件中,使用 bean
标签注册组件,使用 context:component-scan
标签批量扫描组件,使用 import
标签引入其他的 XML 文件,使用 context:property-placeholder
标签加载配置文件,等等。此外,有的标签带有前缀,需要引入相应的名称空间,操作起来异常繁琐。鉴于此,Spring 提供了一套声明式的解决方案,以配置类为核心,为我们提供了更加方便快捷、灵活多样的配置应用的方式。
xml
代码解读
复制代码
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <-- 1. 注册组件 !--> <bean id="foo" class="cn.stimd.spring.Foo" > <property name="bar" value="bar"/> </bean> <bean id="bar" class="cn.stimd.spring.Bar" /> <-- 2. 批量扫描 !--> <context:component-scan base-package="cn.stimd.spring" use-default-filters="true"/> <-- 3. 导入配置文件 !--> <import resource="other.xml"/> <-- 4. 加载属性文件 !--> <context:property-placeholder location="classpath:jdbc.properties"/> </beans>
Spring 将配置类分为 Full 模式与 Lite 模式,其中 Lite 模式表示轻量级,仅拥有配置类的部分功能,Full 模式为全量级,拥有配置类的全部功能。两者的区别如下:
- Full 模式:声明了
@Configuration
注解的类 - Lite 模式:类上声明了
@Component
、@ComponentScan
、@Import
等注解,或者方法上声明了@Bean
注解,也可认为是配置类
为了简化代码实现,我们不区分轻量级与全量级的配置类,所讨论的配置类也仅以声明了 @Configuration
注解为标准。@Configuration
注解声明了元注解 @Component
,因此配置类可以通过扫描的方式来加载。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
2. 配置类组件
2.1 继承结构
Spring 为了处理配置类,提供了三个重要的组件,此外还使用 CongigurationClass
来描述已解析的配置类。简单介绍如下:
ConfigurationClassPostProcessor
:寻找容器中已存在的配置类,并对其进行解析。ConfigurationClassParser
:负责具体的解析过程,将已解析的配置类封装成CongigurationClass
对象。ConfigurationClassBeanDefinitionReader
:对CongigurationClass
集合进行处理,将所有的组件以BeanDefinition
的方式注册到容器中。
2.2 ConfigurationClass
ConfigurationClass
用于描述一个独立的配置类,并以扁平化的方式管理所有的 BeanMethod。我们首先要明确独立和扁平化这两个概念。其一,当前配置类、内部配置类、导入配置类、组件扫描的配置类,都是独立的配置类。但配置类的父类除外,属于当前配置类的一部分。其二,扁平化是指配置类和父类可能都存在若干 BeanMethod,不管有多少继承层次,都会将所有的 BeanMethod 放在一个集合中管理。这也说明了配置类和父类是一体的。
ConfigurationClass
类定义了一些属性来描述配置类的信息,简单介绍如下:
metadata
:表示配置类的元数据importedBy
:表示配置类是由谁导入的,对于内部类来说是外部类导入的。对于导入类来说,是由声明了@Import
注解的类导入的,可能是外部类或内部类。beanMethods
:缓存了配置类及其父类所有的工厂方法(BeanMethod 相关,待实现)importBeanDefinitionRegistrars
:缓存了ImportBeanDefinitionRegistrar
接口的实现类集合(导入相关,待实现)
java
代码解读
复制代码
public class ConfigurationClass { private final AnnotationMetadata metadata; private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1); private final Set<BeanMethod> beanMethods = new LinkedHashSet<>(); private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars = new LinkedHashMap<>(); }
3. ConfigurationClassPostProcessor
3.1 概述
ConfigurationClassPostProcessor
类实现了 BeanDefinitionRegistryPostProcessor
接口,这是用于处理 BeanFactory
的后处理器。beans 模块定义了该接口,当时没有用武之地,所以没有展开来讲。ConfigurationClassPostProcessor
的作用是解析配置类,并注册 BeanDefinition
,其重要性不言而喻。
AnnotationConfigUtils
工具类注册 ConfigurationClassPostProcessor
作为默认的组件。这样一来,我们在创建 AnnotationConfigApplicationContext
实例的时候,自动拥有了处理配置类的能力。
java
代码解读
复制代码
public class AnnotationConfigUtils { public static final String CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME = "internalConfigurationAnnotationProcessor"; public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) { //注册支持@Configuration注解的组件 if(!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)){ RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class); registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME); } //其余略 } }
3.2 加载流程
从 AnnotationConfigApplicationContext
的创建到配置类的处理,中间经过了哪些流程,我们结合时序图来简单分析一下。整个流程由以下步骤组成:
- 在
AnnotationConfigApplicationContext
的构造函数中创建AnnotatedBeanDefinitionReader
组件的实例 - 在构造
AnnotatedBeanDefinitionReader
的过程中,通过工具类AnnotationConfigUtils
让容器注册一些组件 - 将解析
ConfigurationClassPostProcessor
注册到容器中 - 回到
AnnotationConfigApplicationContext
的构造函数,调用父类的refresh
方法,刷新容器 AbstractApplicationContext
在刷新的过程中,调用invokeBeanFactoryPostProcessors
方法回调BeanFactoryPostProcessor
ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry
方法完成配置类的解析流程
3.3 代码实现
postProcessBeanDefinitionRegistry
方法是处理配置类的入口,该方法定义了处理配置类的主流程,大体可以分为三步:
-
寻找容器中已存在的配置类(引导配置类)
-
解析引导配置类,加载所有的组件(包括其他配置类)
-
此时已经拿到了所有组件的元数据,包装成
BeanDefinition
注册到容器中
java
代码解读
复制代码
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws RuntimeException { //1. 寻找容器中已存在的配置类 List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] names = registry.getBeanDefinitionNames(); for (String name : names) { BeanDefinition definition = registry.getBeanDefinition(name); //检查是否声明了@Configuration等注解,如果是进一步区分full模式和lite模式 if(ConfigurationClassUtils.checkConfigurationClassCandidate(definition, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(definition, name)); } } if(configCandidates.isEmpty()){ return; } //2. 解析引导配置类,加载所有的配置类 Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); ConfigurationClassParser parser = new ConfigurationClassParser(metadataReaderFactory, environment, resourceLoader, registry); parser.parse(candidates); //3. 注册BeanDefinition if(this.reader == null){ this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.resourceLoader, this.environment); } this.reader.loadBeanDefinitions(parser.getConfigurationClasses()); } }
第二步和第三步有专门的组件来处理,逻辑比较复杂,下边单独介绍,我们先来看第一步。虽然第一步的逻辑并不复杂,关键问题是 ConfigurationClassPostProcessor
组件在 refresh
方法中的执行顺序相当靠前,几乎在 BeanFactory
准备完毕后就被调用,此时容器中不存在配置类。因此我们需要事先注册一个配置类,为了与后来加载的配置类区分开,我们把提前注册的配置类称为引导配置类。所谓引导(bootstrap)是指 Spring 以引导配置类为切入点,加载其他的配置类,从而完成对所有组件的加载。
一般情况下,引导配置类只有一个,主要通过 AnnotatedBeanDefinitionReader
来注册。这里也解决了我们一个疑惑,AnnotatedBeanDefinitionReader
只能注册一个 BeanDefinition
,并没有比手动注册 BeanDefinition
方便多少,感觉有些鸡肋。现在我们知道,AnnotatedBeanDefinitionReader
的主要作用就是注册引导配置类,实际上 SpringBoot 的启动类就是如此处理的。
4. ConfigurationClassParser
4.1 配置类解析
在拿到引导配置类的集合之后,交给 ConfigurationClassParser
组件来解析。parse
方法是解析配置类的入口,由于 BeanDefinition
有多种加载方式,需要区分不同的情况,因此需要调用不同的 parse
重载方法,但最终都会调用 processConfigurationClass
方法。我们先不考虑具体的解析流程是如何处理的,经过处理之后,configurationClasses
字段保存了所有已解析的配置类。
java
代码解读
复制代码
class ConfigurationClassParser { private final MetadataReaderFactory metadataReaderFactory; //已解析的配置类集合 private final Set<ConfigurationClass> configurationClasses = new LinkedHashSet<>(); /** * 解析引导配置类,唯一入口方法 * @param configCandidates 引导配置类集合 */ public void parse(Set<BeanDefinitionHolder> configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); //1. 通过注解声明的方式创建,根据注解元数据(AnnotationMetadata)来加载 if (bd instanceof AnnotatedBeanDefinition) { parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()); } //2. 通过编程的方式创建,比如RootBeanDefinition,根据class属性来加载 else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); } //3. 其余情况,BeanDefinition的class属性未确定,根据className来加载 else{ parse(bd.getBeanClassName(), holder.getBeanName()); } } //处理延迟导入(TODO 先略过,详见Import一节) } //注解元数据已知,可能是反射或ASM的方式 protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(metadata, beanName)); } //Class信息已知,通过反射的方式解析配置类 protected final void parse(Class<?> clazz, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(clazz, beanName)); } //类名已知,通过ASM的方式解析配置类 protected final void parse(String className, String beanName) throws IOException { MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); processConfigurationClass(new ConfigurationClass(reader.getAnnotationMetadata(), beanName)); } }
4.2 processConfigurationClass 方法
processConfigurationClass
方法起到了辅助的作用,首先判断是否应该处理配置类,这一功能与条件判定有关,暂时先跳过。然后以循环的方式处理配置类及其父类,当一个配置类处理完毕,会被加入到缓存中保存起来。在 doProcessConfigurationClass
方法的执行过程中,可能需要解析新的配置类,则以递归的方式调用 processConfigurationClass
方法,也就是说该方法可能多次执行,直到所有的配置类都被处理。
java
代码解读
复制代码
/** * 对指定的配置类进行解析 * @param configClass 表示一个配置类,有多种来源,可以是引导配置类、内部配置类、组件扫描的配置类、导入的配置类 */ protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { //根据@Conditional判断是否应该处理配置类(TODO 先略过,详见条件判定一节) //遍历配置类及其父类 SourceClass sourceClass = asSourceClass(configClass); do{ sourceClass = doProcessConfigurationClass(configClass, sourceClass); } while (sourceClass != null); //将已解析的配置类添加到集合中 this.configurationClasses.add(configClass); }
doProcessConfigurationClass
方法是解析配置类的核心方法,实现了大量功能,逻辑也比较复杂,我们将花费大量篇幅对该方法进行解读。配置类的解析主要有六种情况需要处理,如下所示:
- 内部类:解析声明了
@Configuration
注解的内部类 - 属性文件:如果配置类声明了
@PropertySource
注解,则加载指定的属性文件 - 组件扫描:如果配置类声明了
@ComponentScan
注解,则通过扫描包的方式加载 Bean - 导入:如果配置类声明了
@Import
注解,则导入相应的组件 - 工厂方法:如果方法声明了
@Bean
注解,则以工厂方法的方式加载 Bean - 父类:如果配置类继承了父类,则尝试解析父类
java
代码解读
复制代码
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { //1. 处理内部类 //2. 处理配置文件 //3. 组件扫描 //4. 处理导入 //5. 处理工厂方法 //6. 处理父类 }
4.3 SourceClass
SourceClass
是 ConfigurationClassParser
的内部类,作用是以统一地方式来描述配置类。所谓的统一有两层含义,一是来源不同,可以是当前配置类、内部配置类、配置类的父类、导入的配置类,组件扫描加载的配置类等;二是加载方式的不同,可能是基于反射或 ASM 机制加载的。
source
属性表示配置类对应的资源,从构造方法可以看出source
有两种类型,Class
表示通过反射加载的,MetadataReader
表示通过 ASM 加载的metadata
属性表示配置类的相关信息,ConfigurationClass
也有这个属性
java
代码解读
复制代码
private class SourceClass { private final Object source; private final AnnotationMetadata metadata; public SourceClass(Object source) { this.source = source; if (source instanceof Class) { this.metadata = new StandardAnnotationMetadata((Class<?>) source, true); } else { this.metadata = ((MetadataReader) source).getAnnotationMetadata(); } } }
SourceClass
还有一些很有用的方法,列举如下。可以发现,这些方法都有一个特点,即返回的类型都是 SourceClass
,这一点也印证了 SourceClass
的确是将各种形式的配置类给统一了起来。
-
Set<SourceClass> getAnnotations()
: 获取配置类上的所有注解 -
Collection<SourceClass> getAnnotationAttributes(String annType, String attribute)
:获取指定注解上的指定属性值,比如@Import
注解的value
属性 -
SourceClass getSuperClass()
:获取父类 -
Collection<SourceClass> getMemberClasses()
:获取所有的内部类
5. ConfigurationClassBeanDefinitionReader
经过对引导配置类的解析,所有组件都加载完毕,接下来以 BeanDefinition
的形式注册到容器中,这一工作是由 ConfigurationClassBeanDefinitionReader
完成的。loadBeanDefinitions
方法对传入的 ConfigurationClass
集合进行遍历,然后交由 loadBeanDefinitionsForConfigurationClass
方法处理。
java
代码解读
复制代码
//加载所有的BeanDefinition public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) { for (ConfigurationClass configClass : configurationModel) { loadBeanDefinitionsForConfigurationClass(configClass); } }
loadBeanDefinitionsForConfigurationClass
方法一共处理了三种情况,分别是配置类、工厂方法、导入类的注册。工厂方法和导入类的处理之后再讲,我们先来看配置类是如何注册的。首先需要判断当前配置类是不是「导入的」,我们在讲解 ConfigurationClass
类时说过,内部类和通过 @Import
注解加载的配置类是导入的,也就是说需要注册的是这两种配置类。下面列出了各种配置类的特点,有助于形成较为全面的认识:
-
引导配置类:非导入的,且已经注册过
-
通过组件扫描加载的配置类:非导入的,且已经注册过
-
内部配置类:是通过外部类导入的,尚未注册(本节实现)
-
导入配置类:通过
@Import
注解导入的,尚未注册(待实现)
java
代码解读
复制代码
//从ConfigurationClass中加载BeanDefinition private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) { //1. 注册配置类 if(configClass.isImported()){ registerBeanDefinitionForImportedConfigurationClass(configClass); } //2. 注册BeanMethod(TODO) //3. 注册导入的类(TODO) } //注册导入的配置类(包括内部配置类) private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) { AnnotationMetadata metadata = configClass.getMetadata(); AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata); AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata); String configBeanName = this.beanNameGenerator.generateBeanName(configBeanDef); configClass.setBeanName(configBeanName); this.registry.registerBeanDefinition(configBeanName, configBeanDef); }
6. 讲解思路
6.1 概述
在一个典型的配置类中,上述六种情况可能同时存在。在实际应用中,Spring Boot 中的 WebMvcAutoConfiguration
涵盖了其中的四种情况。在示例代码中,精简了 WebMvcAutoConfiguration
的实现。此外,第五种和第六种情况在源码中并不存在,这里仅仅是为了模拟完整的使用场景。
java
代码解读
复制代码
//5) 组件扫描(源码无,仅展示) @ComponentScan //6) 加载属性文件(源码无,仅展示) @PropertySource("application.properties") @Configuration public class WebMvcAutoConfiguration { //1) 通过工厂方法的方式Bean @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } //2) 内部类处理,WebMvcAutoConfigurationAdapter //3)父类处理,WebMvcConfigurerAdapter //4) 使用导入的方式加载配置类 @Configuration @Import(EnableWebMvcConfiguration.class) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {} }
根据功能的不同,六种情况还可以分为三类。第一,内部类和父类都属于配置类的结构,不涉及具体的解析过程。第二,属性文件是用于处理配置信息的,与加载组件无关。第三,组件扫描、导入、工厂方法都是加载组件的,是整个解析过程的关键所在。我们将根据从易到难的顺序对各项功能进行介绍,而不是按照代码中原有的顺序,本节先来看最简单的内部类和父类的处理。
6.2 内部类处理
在处理内部类时,首先调用 ConfigurationClass
的 getMemberClass
方法,获取所有的内部类。然后判断内部类是不是一个配置类,如果是则递归调用 processConfigurationClass
方法,进入解析内部配置类的流程。从这里可以看出,内部配置类是作为单独的配置类进行解析的。
java
代码解读
复制代码
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser] protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { //1. 处理内部类 processMemberClasses(configClass, sourceClass); ...... } //step-1 处理内部类 private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { for (SourceClass memberClass : sourceClass.getMemberClasses()) { if(ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata())){ //将内部类转换成ConfigurationClass processConfigurationClass(memberClass.asConfigClass(configClass)); } } }
6.3 父类处理
父类的处理是在整个解析流程的末尾,优先级最低。首先检查配置类的全类名是否以 java 开头,由于以 java 开头的包名是 JDK 自用的,是受保护的,因此非 java 开头的全类名代表是用户自定义的类。也就是说,如果父类存在且是自定义的类则返回,由外层方法进行递归处理,即再次执行 doProcessConfigurationClass
方法的逻辑。从这里可以看出,父类是作为当前配置类的一部分存在的,它不是一个单独的配置类。由于父类的低优先级,其作用是提供一些兜底的配置信息,这对框架来说有一定的意义。
java
代码解读
复制代码
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser] protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { //1. 处理内部类(略) //6. 处理父类 if(sourceClass.getMetadata().hasSuperClass()){ String superClass = sourceClass.getMetadata().getSuperClassName(); if(!superClass.startsWith("java")){ return sourceClass.getSuperClass(); } } return null; }
7. 测试
首先定义了一个配置类 OuterConfig
作为外部类,其内部类 InnerConfig
也声明了 @Configuration
注解,因此这是一个内部配置类。
java
代码解读
复制代码
//测试类:表示一个独立的配置类 @Configuration public class OuterConfig extends AbstractConfig { @Configuration static class InnerConfig { public InnerConfig() { System.out.println("我是配置类的内部类..."); } } }
其次,OuterConfig
还有一个父类,需要注意的是,父类 AbstractConfig
并没有声明 Configuration
注解,说明它与子类是一体的。为了说明父类确实被解析了,我们在父类中也声明了一个内部类 InnerConfig2
。
java
代码解读
复制代码
//测试类:配置类的父类 public abstract class AbstractConfig { @Configuration static class InnerConfig2 { public InnerConfig2() { System.out.println("我是父类的内部类2..."); } } }
本次测试有两个目的,一是验证核心组件 ConfigurationClassPostProcessor
正常执行,二是配置类的内部类和父类确实被解析。测试方法的逻辑很简单,首先将配置类 OuterConfig
注册到 Spring 容器中,然后刷新容器,自动执行配置类的解析操作。
java
代码解读
复制代码
//测试方法 @Test public void testInnerAndSuperClass() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(OuterConfig.class); //注册引导配置类 context.refresh(); }
从测试结果可以看到,内部类的解析是优先执行的,然后是对父类的解析。
erlang
代码解读
复制代码
我是配置类的内部类... 我是父类的内部类2...
8. 总结
本节初步讨论了配置类,典型的配置类使用 @Configuration
注解来标识,具体的解析过程由三个组件类完成。
ConfigurationClassPostProcessor
是整个流程的入口,寻找容器中已存在的配置类,并交给ConfigurationClassParser
组件解析。ConfigurationClassParser
负责具体的解析逻辑,将已解析的配置类包装成ConfigurationClass
对象。ConfigurationClassBeanDefinitionReader
负责处理ConfigurationClass
集合,提取出BeanDefnition
并注册到 Spring 容器中。
配置类解析的过程最为繁杂,为了便于讲解,我们将主要功能分为六种,分别是内部类、父类、属性文件、组件扫描、工厂方法、导入。内部类和父类实际上是配置类的结构,并不涉及实际的解析逻辑,因此代码实现也是最简单的。本节通过对内部类和父类的处理,初步实现了对配置类的解析。
9. 项目结构
新增修改一览,新增(9),修改(1)。
scss
代码解读
复制代码
context └─ src ├─ main │ └─ java │ └─ cn.stimd.spring.context │ └─ annotation │ ├─ AnnotationConfigUtils.java (*) │ ├─ Configuration.java (+) │ ├─ ConfigurationClass.java (+) │ ├─ ConfigurationClassBeanDefinitionReader.java (+) │ ├─ ConfigurationClassParser.java (+) │ ├─ ConfigurationClassPostProcessor.java (+) │ └─ ConfigurationClassUtils.java (+) └─ test └─ java └─ context └─ config ├─ AbstractConfig.java (+) ├─ ConfigTest.java (+) └─ OuterConfig.java (+) 注:+号表示新增、*表示修改