likes
comments
collection
share

【重写SpringFramework】配置类4:导入机制(chapter 3-8)组件扫描和 BeanMethod 是加

作者站长头像
站长
· 阅读数 21

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 注解,然后执行 XxxImportSelectorselectImports 方法,这里我们模拟 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 方法可以分为三步,如下所示:

  1. 获取配置类声明的所有注解,annTypes 为全类名的集合。
  2. 遍历注解集合,根据 mode 属性决定使用基于 Spring AOP 还是 AspectJ 构建的切面,AutoProxyRegistrar 只实现了 Spring AOP 的构建方式。
  3. 通过 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 接口:存储在 ConfigurationClassimportBeanDefinitionRegistrars 字段中,由第三步处理
  • 配置类:内部类和导入的配置类保存在 ConfigurationClassimportedBy 字段中,由第一步处理(已实现)
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassBeanDefinitionReader]
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) {
    //1. 注入配置类(略)
    //2. 注册BeanMethod(略)
    //3. 注册导入的类
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

loadBeanDefinitionsFromRegistrars 方法的逻辑比较简单,遍历 ConfigurationClassimportBeanDefinitionRegistrars 字段,回调 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 接口:仅作为中间过程存在,最终转换为上述两种情况

【重写SpringFramework】配置类4:导入机制(chapter 3-8)组件扫描和 BeanMethod 是加

4. 延迟导入

4.1 DeferredImportSelector

DeferredImportSelector 是一个标记接口,没有实际内容,延迟导入的选择器在解析时的优先级最低。在实际使用中,Spring Boot 的自动配置使用了延迟导入的功能,原因有二。一是自动配置作为兜底选项,可以在用户没有指定某些组件时提供默认选项。二是为了配合条件判定的相关注解,比如 @ConditionalOnBean 注解需要确保某个 Bean 在容器中,那么自动配置出现的位置越靠后越好。

public interface DeferredImportSelector extends ImportSelector {
    //标识接口
}

4.2 代码实现

ConfigurationClassParserdeferredImportSelectors 字段表示一组延迟导入的选择器。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 存在,开始处理导入的流程。在导入类 AutoConfigurationImportSelectorselectImports 方法中,返回了全类名的数组,继而向容器中注册 WebMvcAutoConfigurationDataSourceAutoConfiguration 这两个配置类。

//测试方法
@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 标榜的约定大于配置的理念。

【重写SpringFramework】配置类4:导入机制(chapter 3-8)组件扫描和 BeanMethod 是加

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
评论
请登录