【重写SpringFramework】配置类概述(chapter 3-5)在早期的 Spring 项目中,使用 XML
1. 前言
在早期的 Spring 项目中,使用 XML 文件进行配置。如下所示,在 applicationContext.xml
配置文件中,使用 bean
标签注册组件,使用 context:component-scan
标签批量扫描组件,使用 import
标签引入其他的 XML 文件,使用 context:property-placeholder
标签加载配置文件,等等。此外,有的标签带有前缀,需要引入相应的名称空间,操作起来异常繁琐。鉴于此,Spring 提供了一套声明式的解决方案,以配置类为核心,为我们提供了更加方便快捷、灵活多样的配置应用的方式。
<?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
,因此配置类可以通过扫描的方式来加载。
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
接口的实现类集合(导入相关,待实现)
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
实例的时候,自动拥有了处理配置类的能力。
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
注册到容器中
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
字段保存了所有已解析的配置类。
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
方法,也就是说该方法可能多次执行,直到所有的配置类都被处理。
/**
* 对指定的配置类进行解析
* @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 - 父类:如果配置类继承了父类,则尝试解析父类
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
也有这个属性
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
方法处理。
//加载所有的BeanDefinition
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass);
}
}
loadBeanDefinitionsForConfigurationClass
方法一共处理了三种情况,分别是配置类、工厂方法、导入类的注册。工厂方法和导入类的处理之后再讲,我们先来看配置类是如何注册的。首先需要判断当前配置类是不是「导入的」,我们在讲解 ConfigurationClass
类时说过,内部类和通过 @Import
注解加载的配置类是导入的,也就是说需要注册的是这两种配置类。下面列出了各种配置类的特点,有助于形成较为全面的认识:
-
引导配置类:非导入的,且已经注册过
-
通过组件扫描加载的配置类:非导入的,且已经注册过
-
内部配置类:是通过外部类导入的,尚未注册(本节实现)
-
导入配置类:通过
@Import
注解导入的,尚未注册(待实现)
//从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
的实现。此外,第五种和第六种情况在源码中并不存在,这里仅仅是为了模拟完整的使用场景。
//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
方法,进入解析内部配置类的流程。从这里可以看出,内部配置类是作为单独的配置类进行解析的。
//所属类[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
方法的逻辑。从这里可以看出,父类是作为当前配置类的一部分存在的,它不是一个单独的配置类。由于父类的低优先级,其作用是提供一些兜底的配置信息,这对框架来说有一定的意义。
//所属类[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
注解,因此这是一个内部配置类。
//测试类:表示一个独立的配置类
@Configuration
public class OuterConfig extends AbstractConfig {
@Configuration
static class InnerConfig {
public InnerConfig() {
System.out.println("我是配置类的内部类...");
}
}
}
其次,OuterConfig
还有一个父类,需要注意的是,父类 AbstractConfig
并没有声明 Configuration
注解,说明它与子类是一体的。为了说明父类确实被解析了,我们在父类中也声明了一个内部类 InnerConfig2
。
//测试类:配置类的父类
public abstract class AbstractConfig {
@Configuration
static class InnerConfig2 {
public InnerConfig2() {
System.out.println("我是父类的内部类2...");
}
}
}
本次测试有两个目的,一是验证核心组件 ConfigurationClassPostProcessor
正常执行,二是配置类的内部类和父类确实被解析。测试方法的逻辑很简单,首先将配置类 OuterConfig
注册到 Spring 容器中,然后刷新容器,自动执行配置类的解析操作。
//测试方法
@Test
public void testInnerAndSuperClass() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(OuterConfig.class); //注册引导配置类
context.refresh();
}
从测试结果可以看到,内部类的解析是优先执行的,然后是对父类的解析。
我是配置类的内部类...
我是父类的内部类2...
8. 总结
本节初步讨论了配置类,典型的配置类使用 @Configuration
注解来标识,具体的解析过程由三个组件类完成。
ConfigurationClassPostProcessor
是整个流程的入口,寻找容器中已存在的配置类,并交给ConfigurationClassParser
组件解析。ConfigurationClassParser
负责具体的解析逻辑,将已解析的配置类包装成ConfigurationClass
对象。ConfigurationClassBeanDefinitionReader
负责处理ConfigurationClass
集合,提取出BeanDefnition
并注册到 Spring 容器中。
配置类解析的过程最为繁杂,为了便于讲解,我们将主要功能分为六种,分别是内部类、父类、属性文件、组件扫描、工厂方法、导入。内部类和父类实际上是配置类的结构,并不涉及实际的解析逻辑,因此代码实现也是最简单的。本节通过对内部类和父类的处理,初步实现了对配置类的解析。
9. 项目结构
新增修改一览,新增(9),修改(1)。
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 (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。
转载自:https://juejin.cn/post/7402968643950329883