01 引言

我们在使用一些Spring开关时,可能会发现导入的类很多都实现了org.springframework.context.annotation.ImportSelector接口,这个接口到底是干什么的?我们一起了解一下。

我们都知道,Spring的开关无非是控制Spring按需加载资源到容器中,或者通过开关加载启动类默认无法加载的资源。ImportSelector扮演者怎样的角色呢?

02 简介

ImportSelector 是 Spring 框架中一个非常强大的接口,位于 org.springframework.context.annotation 包下。它的核心作用是在运行时动态地选择一组配置类(即 @Configuration 类)或普通组件的全限定类名,并让 Spring 容器将它们导入并注册为 Bean 定义。

写过插件或者公用代码库的朋友都可能考虑过这样的问题。定义好的公用组件,肯定是独立的,可能会被引入到不同的模块项目中,其他项目默认不一定能扫描到自己的容器中。我们通常通过@Import手动导入或者配置到spring.factories中,以达到被加载的目的。

而今天要说的ImportSelectorImport的功能相同,但更具有灵活性,可以通过逻辑程序更加细粒度的控制资源的加载。可以把它理解为 “一个可以编程的、有逻辑的 @Import 注解”

  • @Import 注解是静态的:必须在编译时就把要导入的配置类写在注解里。
  • ImportSelector 是动态的:它允许你根据当前的运行时环境(如:系统属性、环境变量、项目类路径、其他 Bean 的存在情况等)来决定到底要导入哪些配置。

通常情况下@ImportImportSelector一起使用,通过@Import导入ImportSelector, 而 ImportSelector通过逻辑动态控制资源。

03 核心方法和工作原理

3.1 源码

selectImports方法时在Spring 3.1时才有的,而在Spring 5.2.4的新版本中有新增了getExclusionFilter,用来过滤不必要的资源。

3.2 方法说明

最主要的方法就是selectImports,用来导入所需要的资源信息。

AnnotationMetadata importingClassMetadata

不得不说,Spring为参数取的名字非常有讲究,能够帮助我们理解其含义。importingClassMetadata表示导入类的元数据。如果你在 @Configuration /@Component修改的类(如:ExampleConfig.java)上使用了 @Import(MyImportSelector.class),那么这个 importingClassMetadata 对象就能让你获取到该配置类(ExampleConfig.java)上的所有注解信息(如 @Configuration 的属性,或者其他自定义注解)。

String[]:

方法需要返回一个由类的全限定名组成的字符串数组。这些类将会被 Spring 容器处理,就好像它们被直接写在 @Import 注解里一样。这些类通常就是 @Configuration 类,但也可以是任何需要被 Spring 管理的组件类(如标有 @Component, @Service 等的类)。

getExclusionFilter

Predicate接口,对应的泛型是类的全限定名。

3.3 工作流程

  1. Spring 容器启动,解析一个配置类(比如 AppConfig)或启动类。
  2. 发现 AppConfig 上使用了 @Import(MyImportSelector.class)
  3. 容器实例化 MyImportSelector
  4. 调用 MyImportSelector.selectImports(...) 方法。
  5. MyImportSelector 根据自身的逻辑(检查环境等)计算出需要导入的类名数组。
  6. Spring 容器拿到这个数组,将这些类名对应的类进行解析、注册为 Bean 定义。
  7. 继续正常的启动流程

04 最佳实践

我们假设一个场景,我们根据不同的参数或者环境,加载不同的资源。这些资源默认不会被加载。

4.1 定义动态选择类

public class MyImportSelect implements ImportSelector, EnvironmentAware {

    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println(importingClassMetadata.getClassName());
        String env = environment.getProperty("env");
        if (StringUtils.equalsIgnoreCase(env, "a")) {
            return new String[]{AConfig.class.getName()};
        }

        if (StringUtils.equalsIgnoreCase(env, "b")) {
            return new String[]{BConfig.class.getName()};
        }


        return new String[]{AConfig.class.getName(),
                BConfig.class.getName()};
    }

}

默认加载AConfigBConfig,并根据不同的环境参数,加载不同的资源。

4.2 定义资源

@Component
public class AConfig {
    @Bean
    public A a() {
        return new A();
    }
}

public class A {
    public A() {
        System.out.println("A 被实例化了!");
    }
}

@Component
public class BConfig {

    @Bean
    public B b() {
        return new B();
    }
}

public class B {
    public B() {
        System.out.println("B 被实例化了!");
    }
}

4.3 定义开关

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyImportSelect.class)
public @interface EnableLoadBean {
    
}

这里的注解没有意义,只是一个开关。通过@Import导入MyImportSelect.class,当然也可以去掉开关,直接加在配置类上也可以。

4.4 打开开关

直接加在启动类上。

4.5 测试

增加启动参数:-Denv=a

我们可以看到生效了:

importingClassMetadata我们也打出来了,就是启动类。我们就可以获取启动类上的注解以及被继承的注解。

05 小结

ImportSelector 的核心优势在于它将配置选择逻辑从静态的注解或XML配置中解放出来,提供了运行时动态决策的能力。这使得应用程序能够更加灵活地适应不同的运行环境和需求变化。

在实际开发中,结合 @Conditional 系列注解和 ImportSelector,可以构建出高度可配置和可扩展的 Spring 应用程序。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]