【重写SpringFramework】配置类4:导入机制(chapter 3-8)组件扫描和 BeanMethod 是加
1. 前言
前边提到,组件扫描和 BeanMethod 是加载组件的主要方式,涵盖了大多数使用场景。如果想在一个配置类中引入另外一个配置类,该如何处理?我们能想到的是通过 BeanMethod 来实现,但配置类没有需要设置的属性,这种做法固然可行,但不够简洁。示例代码如下,在配置类 AConfig
的工厂方法中创建了 BConfig
实例。
//示例代码
@Configration
public class AConfig {
@Bean
public BConfig bConfig() {
return new BConfig();
}
}
一般来说,工厂方法主要解决的是复杂对象的创建问题,用来创建配置类有点大材小用。鉴于此,Spring 提供导入机制,用来解决配置类的加载问题。在配置类上声明 @Import
注解开启导入功能,这种方式替代了 Spring 配置文件中的 import 标签。此外,导入机制还可以实现更复杂的功能,比如根据条件来决定导入哪些组件。
2. 导入组件
2.1 @Import
@Import
注解只有一个默认参数,value
属性的类型是 Class
数组,一共处理了三种情况,一是自定义的配置类,二是 ImportSelector
接口类型,三是 ImportBeanDefinitionRegistrar
接口类型。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Import {
Class<?>[] value();
}
对于开发者来说,最常用的方式是第一种,也就是直接导入一个配置类,示例代码如下。
//示例代码:直接导入配置类
@Configuration
@Import(BConfig.class)
public class AConfig {}
注:第一种情况当然可以是非配置类,但对于组件类我们有多种加载方式可以替代,比如扫描加载、工厂方法等多种形式。更为重要的是,
@Import
注解最终注册的是BeanDefinition
,也就是通过无参构造器创建对象,这对于组件类来说,远没有其他方式灵活。因此第一种情况主要用于配置类。
2.2 ImportSelector
ImportSelector
接口的作用是根据一定的条件选择导入哪些类,通常来说这些条件来自注解的属性。selectImports
方法可以实现比较复杂的导入逻辑,返回的字符串数组表示一组全类名。这里有个问题,为什么返回的是 String
数组而不是 Class
数组?这是因为 ImportSelector
接口可以通过 SPI 机制加载组件,相关信息保存在特殊的配置文件中,得到的正是一组全类名。这个功能与自动配置有关,我们将在第二部 Spring Boot 的自动配置章节进行介绍。
public interface ImportSelector {
//获取需要导入的全类名数组
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
示例代码如下,首先解析配置类的 @Import
注解,然后执行 XxxImportSelector
的 selectImports
方法,这里我们模拟 SPI 机制,直接返回了全类名数组。接下来,全类名数组会被解析成一组 BeanDefinition
,并注册到 Spring 容器中。
//示例代码
@Configuration
@Import(XxxImportSelector.class)
public class ImportConfig {}
public class XxxImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] { "cn.stimd.context.test.XxxConfig" };
}
}
2.3 ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar
接口的作用是直接注册 BeanDefinition
,这是与 ImportSelector
接口的主要区别。registerBeanDefinitions
方法也可以执行一些复杂的逻辑,根据条件来注册 BeanDefinition
。
public interface ImportBeanDefinitionRegistrar {
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
ImportBeanDefinitionRegistrar
接口的使用比较复杂,我们通过一个例子来了解。之前我们讲过 AnnotationConfigUtils
工具类可以注册一些常用的 BeanPostProcessor
组件,包括依赖注入、配置类解析、生命周期处理等,但是不能直接注册 AOP 组件,因为需要考虑多种情况。其一,创建代理的方式是 JDK 接口代理,还是 CGLIB 代理。其二,AOP 的构建模式是 Spring AOP,还是 AspecetJ 框架。这些信息都保存在注解上,通过配置类来解析注解,然后由 ImportBeanDefinitionRegistrar
接口的实现类注册 AOP 组件。
在 Spring 源码中,@EnableTransactionManagement
注解的作用是开启事务管理,而 Spring 事务是建立在 AOP 的基础之上。该注解稍后会介绍,这里使用自定义的 @EnableAopProxy
注解来模拟。@EnableAopProxy
注解定义了两个属性,其中 proxyTargetClass
属性表示创建代理的方式,mode
属性表示 AOP 的构建方式。
//示例代码,模拟@EnableTransactionManagement
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AutoProxyRegistrar.class)
public @interface EnableAopProxy {
//指定代理对象的生成模式,为true时表示使用CGLIB的子类代理,为false表示使用JDK接口代理。
boolean proxyTargetClass() default false;
//指定事务的通知模式,默认为JDK动态代理
AdviceMode mode() default AdviceMode.PROXY;
}
@EnableAopProxy
注解还声明了 @Import
注解,引入了 AutoProxyRegistrar
,这是 ImportBeanDefinitionRegistrar
接口的重要实现类,作用是自动注册 AOP 相关的组件。registerBeanDefinitions
方法可以分为三步,如下所示:
- 获取配置类声明的所有注解,
annTypes
为全类名的集合。 - 遍历注解集合,根据
mode
属性决定使用基于 Spring AOP 还是 AspectJ 构建的切面,AutoProxyRegistrar
只实现了 Spring AOP 的构建方式。 - 通过
AopConfigUtils
注册InfrastructureAdvisorAutoProxyCreator
组件,如果proxyTargetClass
属性为 true,则指定代理创建方式为 CGLIB 子类代理。
public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar{
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//1. 获取声明的所有注解,形式为全类名数组
Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
for (String annType : annTypes) {
//获取某个注解的所有属性值
AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
if (candidate == null) {
continue;
}
Object mode = candidate.get("mode");
Object proxyTargetClass = candidate.get("proxyTargetClass");
//2. 基于 Spring AOP 构建的切面
if (mode == AdviceMode.PROXY) {
//3. 注册InfrastructureAdvisorAutoProxyCreator
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
//如果proxyTargetClass为true,即使用Cglib创建代理对象,则需要修改属性
if((Boolean)proxyTargetClass){
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
}
}
}
}
总的来说,@EnableAopProxy
注解的作用是注册 InfrastructureAdvisorAutoProxyCreator
组件,用于创建代理对象。该组件需要设置一些属性,前提是根据注解信息进行判断。由此可见,ImportBeanDefinitionRegistrar
接口的特点之一就是能实现比较复杂的逻辑。
3. 代码实现
3.1 解析@Import 注解
回到 ConfigurationClassParser
类的 doProcessConfigurationClass
方法,第四步处理了导入流程。首先由 getImports
方法处理,获取配置类上声明的所有 @Import
注解。然后由 processImports
方法进行具体的处理。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
//1. 处理内部类(略)
//2. 处理配置文件(略)
//3. 组件扫描(略)
//4. 处理导入@Import
processImports(configClass, sourceClass, getImports(sourceClass));
}
第一步,获取待导入的类。 先来看 getImports
方法。变量 imports
表示最终导入类的集合,变量 visited
表示已经处理的配置类或注解。这两个变量都是 Set
类型,目的是为了去重。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
//获取所有需要导入的类,以SourceClass的形式表示
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
在 collectImports
方法中,首先遍历配置类声明的所有注解,如果注解的全类名不是以 java 开头或者 @Import
注解本身,那么递归地调用 collectImports
方法,尝试解析嵌套注解。需要注意的是,@Import
注解并不一定直接声明在配置类上,而是作为嵌套注解声明在另外一个注解上,比如前边提到的 @EnableAopProxy
注解。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
//解析@Import注解,获取value属性的值
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited) throws IOException{
// 尝试添加到set集合中,防止可能出现重复导入的情况
if(visited.add(sourceClass)){
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
//遍历类的注解,如果注解不是Java注解或@Import,则尝试获取嵌套注解(即注解上的注解)
if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
//将@Import注解指定的Class添加到集合中
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}
第二步,处理待导入的类。 此时已经得到了当前配置类声明的所有 @Import
注解的 value
属性的集合,之前我们讲过,value
属性的类型有三种。processImports
方法处理了这三种导入类的情况,如下所示:
- 首先是对
ImportSelector
接口的处理,这里又分为三小步。第一步加载类、实例化、调用感知接口设置相关的组件。第二步将需要延迟加载的实例缓存起来,稍后处理。第三步回调selectImports
方法,将返回的全类名数组转换成SourceClass
集合,递归调用processImports
方法。由此可见,该分支流程只是中间过程,最终会以另外两种形式处理。 - 然后是
ImportBeanDefinitionRegistrar
接口,这一步比较简单,先实例化对象,然后调用接口方法即可。注册BeanDefinition
的操作是在addImportBeanDefinitionRegistrar
方法内部执行的,因此不需要额外的操作。 - 最后是自定义配置类,递归调用
processConfigurationClass
方法当成新的配置类处理。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates) {
for (SourceClass candidate : importCandidates) {
//1. 实现ImportSelector接口,返回全限定类名的数组,需要递归地调用processImports方法,直到以2或3的的方式来处理
if (candidate.isAssignable(ImportSelector.class)) {
//1) 加载类、实例化、调用感知接口设置相关的组件
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = (ImportSelector) BeanUtils.instantiateClass(candidateClass);
invokeAwareMethods(selector);
//2) 将DeferredImportSelector加入缓存,稍后处理
if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
this.deferredImportSelectors.add(new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
} else{
//3) 回调selectImports方法
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
//递归处理返回的全限定类名数组
processImports(configClass, currentSourceClass, importSourceClasses);
}
}
//2. 实现ImportBeanDefinitionRegistrar接口的,缓存到ConfigurationClass稍后处理
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar = (ImportBeanDefinitionRegistrar) BeanUtils.instantiateClass(candidateClass);
invokeAwareMethods(registrar);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
//3. 当作普通配置类,这里也是递归处理
else{
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
3.2 注册 BeanDefinition
解析完配置类之后,来到 ConfigurationClassBeanDefinitionReader
类的 loadBeanDefinitionsForConfigurationClass
方法,接下来需要注册通过导入加载的类。导入分为三种情况,处理逻辑是不同的,区别如下所示:
ImportSelector
接口:返回全类名数组,已经转换成其他两种情况了ImportBeanDefinitionRegistrar
接口:存储在ConfigurationClass
的importBeanDefinitionRegistrars
字段中,由第三步处理- 配置类:内部类和导入的配置类保存在
ConfigurationClass
的importedBy
字段中,由第一步处理(已实现)
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassBeanDefinitionReader]
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) {
//1. 注入配置类(略)
//2. 注册BeanMethod(略)
//3. 注册导入的类
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
loadBeanDefinitionsFromRegistrars
方法的逻辑比较简单,遍历 ConfigurationClass
的 importBeanDefinitionRegistrars
字段,回调 ImportBeanDefinitionRegistrar
接口的 registerBeanDefinitions
方法,比如 AutoProxyRegistrar
完成了 AOP 组件的注册工作。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassBeanDefinitionReader]
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
for (Map.Entry<ImportBeanDefinitionRegistrar, AnnotationMetadata> entry : registrars.entrySet()) {
//key为ImportBeanDefinitionRegistrar接口的实现类,value为待注册组件的元数据
entry.getKey().registerBeanDefinitions(entry.getValue(), this.registry);
}
}
3.3 流程复盘
导入的处理逻辑比较复杂,涉及多个分支以及递归调用,我们需要从全局来审视一下整个流程。如下图所示,导入的三种情况分别使用不同的颜色来标识。事实上,无论流程上如何变化,三种导入情况最终的处理是确定的,如下所示:
- 配置类:最终都会递归调用
processConfigurationClass
方法进行解析 ImportBeanDefinitionRegistrar
接口:直接处理,无循环逻辑ImportSelector
接口:仅作为中间过程存在,最终转换为上述两种情况
4. 延迟导入
4.1 DeferredImportSelector
DeferredImportSelector
是一个标记接口,没有实际内容,延迟导入的选择器在解析时的优先级最低。在实际使用中,Spring Boot 的自动配置使用了延迟导入的功能,原因有二。一是自动配置作为兜底选项,可以在用户没有指定某些组件时提供默认选项。二是为了配合条件判定的相关注解,比如 @ConditionalOnBean
注解需要确保某个 Bean 在容器中,那么自动配置出现的位置越靠后越好。
public interface DeferredImportSelector extends ImportSelector {
//标识接口
}
4.2 代码实现
ConfigurationClassParser
的 deferredImportSelectors
字段表示一组延迟导入的选择器。DeferredImportSelectorHolder
作为内部类,用于临时存储正在解析的配置类与选择器。
class ConfigurationClassParser {
private List<DeferredImportSelectorHolder> deferredImportSelectors;
//内部类,临时保存延迟导入的类
private static class DeferredImportSelectorHolder {
private final ConfigurationClass configurationClass;
private final DeferredImportSelector importSelector;
}
}
在 processImports
方法中,我们将 DeferredImportSelector
临时缓存到 deferredImportSelectors
字段中。当所有的配置类都处理完毕,回到 parse
方法,最后一步处理延迟加载。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
//处理配置类的入口方法
public void parse(Set<BeanDefinitionHolder> configCandidates) {
this.deferredImportSelectors = new LinkedList<>();
for (BeanDefinitionHolder holder : configCandidates) {
//略
}
//等到所有的配置类都处理完毕,最后再执行需要延迟导入的选择器
processDeferredImportSelectors();
}
processDeferredImportSelectors
方法分为两步,首先对延迟导入的集合进行排序,然后遍历集合并调用 processImports
方法完成导入的处理。
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
//处理需要延迟导入的组件
private void processDeferredImportSelectors() {
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR); //排序
for (DeferredImportSelectorHolder deferredImport : deferredImports) {
ConfigurationClass configClass = deferredImport.getConfigurationClass();
String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
//导入类的处理
processImports(configClass, asSourceClass(configClass), asSourceClasses(imports));
}
}
5. 测试
5.1 自动注册 AOP 组件
本测试模拟了自动注册 AOP 组件的流程,目的是为了验证 ImportBeanDefinitionRegistrar
接口加载指定 BeanDefinition
的功能。@EnableAopProxy
注解见上文示例代码,配置类 AopConfig
声明了该注解。
//测试类
@Configuration
@EnableAopProxy(proxyTargetClass = true)
public class AopConfig {
}
测试方法的逻辑较为简单,在解析配置类的过程中,由于 @EnableAopProxy
存在嵌套注解 @Import
,因此会进入处理导入的流程。导入类为 AutoProxyRegistrar
,自动注册了 InfrastructureAdvisorAutoProxyCreator
组件。
//测试方法
@Test
public void testImportBeanDefinitionRegistrar() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
InfrastructureAdvisorAutoProxyCreator creator = context.getBean(InfrastructureAdvisorAutoProxyCreator.class);
System.out.println("测试ImportBeanDefinitionRegistrar接口:" + creator);
}
从测试结果中可以看到,InfrastructureAdvisorAutoProxyCreator
的确存在于 Spring 容器中。
测试ImportBeanDefinitionRegistrar接口:cn.stimd.spring.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator@5abca1e0
5.2 ImportSelector
本测试模拟 Spring Boot 应用的简单加载流程,使用到的技术包括 @SpringBootApplication
注解作为应用启动的入口,@Import
注解的解析,延迟导入,以及使用 ImportSelector
接口和 SPI 机制加载配置类。相关的测试类一种有五个,比较重要的有三个,如下所示:
-
@SpringBootApplication
注解需要注意两点,一是声明了@Configuration
注解,说明该注解可以标识配置类。二是声明了@Import
注解,会执行导入流程。 -
AutoConfigurationImportSelector
实现了DeferredImportSelector
接口,在selectImports
方法中,模拟 SPI 加载了两个配置类。这两个配置类无实质内容,仅打印日志。 -
AppStartup
模拟应用启动类,声明了@SpringBootApplication
注解,会被当做配置类来解析。此外还导入了一个自定义的配置类CustomConfig
,目的有二,一是验证直接导入配置类的功能,二是验证延迟导入的配置类是最后加载的。
综上所述,本次测试一共验证了三个功能。一是普通配置类的导入,二是 ImportSelector
接口的机制,三是延迟导入,这一点可以通过与普通配置类的加载顺序比较得出。
//测试类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@Import({AutoConfigurationImportSelector.class})
@Inherited
public @interface SpringBootApplication {
}
public class AutoConfigurationImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//模拟SPI加载配置类
return new String[]{"context.config.imports.WebMvcAutoConfiguration",
"context.config.imports.DataSourceAutoConfiguration"};
}
}
@SpringBootApplication
@Import(CustomConfig.class)
public class AppStartup {
}
在测试方法中,AppStartup
作为配置类被解析。首先检测到嵌套注解 @Import
存在,开始处理导入的流程。在导入类 AutoConfigurationImportSelector
的 selectImports
方法中,返回了全类名的数组,继而向容器中注册 WebMvcAutoConfiguration
和 DataSourceAutoConfiguration
这两个配置类。
//测试方法
@Test
public void testImportSelector(){
new AnnotationConfigApplicationContext(AppStartup.class);
}
从测试结果可以看到,自定义配置类优先加载,另外两个配置类是通过延迟导入的方式加载的。
加载配置类CustomConfig
加载配置类WebMvcAutoConfiguration
加载配置类DataSourceAutoConfiguration
6. 总结
本节我们讨论了配置类的导入机制。导入提供了多种灵活的加载组件的方式,主要是框架内部使用。导入功能的入口点是 @Import
注解,value
属性的类型是 Class
数组,表示待导入的类。Spring 处理了三种类型的导入,一是普通类,主要是导入新的配置类。二是 ImportBeanDefinitionRegistrar
接口,可以直接注册 BeanDefinition
。三是 ImportSelector
接口,返回全类名数组,最终需要通过另外两种方式解析。在实际使用中,这三种情况可以互相配合,比如 Spring Boot 通过它们实现了非常灵活的自动配置功能。
此外还提供了延迟导入的功能,Spring Boot 的自动配置就用到了延迟导入。目的是优先加载用户定义的组件,最后导入框架默认的组件作为兜底措施,而这正符合 Spring Boot 标榜的约定大于配置的理念。
7. 项目信息
新增修改一览,新增(14),修改(4)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring.context
│ └─ annotation
│ ├─ AdviceMode.java (+)
│ ├─ AutoProxyRegistrar.java (+)
│ ├─ ConfigurationClass.java (*)
│ ├─ ConfigurationClassBeanDefinitionReader.java (*)
│ ├─ ConfigurationClassParser.java (*)
│ ├─ DeferredImportSelector.java (+)
│ ├─ Import.java (+)
│ ├─ ImportBeanDefinitionRegistrar.java (+)
│ └─ ImportSelector.java (+)
└─ test
└─ java
└─ context
└─ config
├─ imports
│ ├─ AopConfig.java(+)
│ ├─ AppStartup.java(+)
│ ├─ AutoConfigurationImportSelector.java(+)
│ ├─ CustomConfig.java(+)
│ ├─ DataSourceAutoConfiguration.java(+)
│ ├─ EnableAopProxy.java(+)
│ ├─ SpringBootApplication.java(+)
│ └─ WebMvcAutoConfiguration.java(+)
└─ ConfigTest.java (*)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。
转载自:https://juejin.cn/post/7408163165416423439