转载

191214-SpringBoot系列教程自动配置选择生效

写了这么久的Spring系列博文,发现了一个问题,之前所有的文章都是围绕的让一个东西生效;那么有没有反其道而行之的呢?

我们知道可以通过 @ConditionOnXxx 来决定一个配置类是否可以加载,那么假设有这么个应用场景

  • 有一个Print的抽象接口,有多个实现,如输出到控制台的ConsolePrint, 输出到文件的 FilePrint, 输出到db的 DbPrint
  • 我们在实际使用的时候,根据用户的选择,使用其中的一个具体实现

针对上面的case,当然也可以使用 @ConditionOnExpression 来实现,除此之外推荐一种更优雅的选择注入方式 ImportSelector

I. 配置选择

本文使用的spring boot 版本为 2.1.2.RELEASE

接下来我们使用ImportSelector来实现上面提出的case

1. Print类

一个接口类,三个实现类

public interface IPrint {
    void print();
}

public class ConsolePrint implements IPrint {
    @Override
    public void print() {
        System.out.println("控制台输出");
    }
}

public class DbPrint implements IPrint {
    @Override
    public void print() {
        System.out.println("db print");
    }
}

public class FilePrint implements IPrint {
    @Override
    public void print() {
        System.out.println("file print");
    }
}

2. 选择类

自定义一个PrintConfigSelector继承ImportSelector,主要在实现类中,通过我们自定义的注解来选择具体加载三个配置类中的哪一个

public class PrintConfigSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes =
                AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(PrintSelector.class.getName()));

        Class config = attributes.getClass("value");
        return new String[]{config.getName()};
    }

    public static class ConsoleConfiguration {
        @Bean
        public ConsolePrint consolePrint() {
            return new ConsolePrint();
        }
    }

    public static class FileConfiguration {
        @Bean
        public FilePrint filePrint() {
            return new FilePrint();
        }
    }

    public static class DbConfiguration {
        @Bean
        public DbPrint dbPrint() {
            return new DbPrint();
        }
    }
}

3. PrintSelector注解

主要用来注入 PrintConfigSelector 来生效,其中value属性,用来具体选择让哪一个配置生效,默认注册 ConsolePrint

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(PrintConfigSelector.class)
public @interface PrintSelector {
    Class<?> value() default PrintConfigSelector.ConsoleConfiguration.class;
}

4. 测试

//@PrintSelector(PrintConfigSelector.FileConfiguration .class)
//@PrintSelector(PrintConfigSelector.DbConfiguration .class)
@PrintSelector
@SpringBootApplication
public class Application {

    public Application(IPrint print) {
        print.print();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

在实际的测试中,通过修改 @PrintSelector 的value来切换不同的Print实现类

II. 扩展

虽然上面通过一个实际的case实现来演示了 ImportSelector 的使用姿势,可以用来选择某些配置类生效。但还有一些其他的知识点,有必要指出一下

通过ImportSelector选择的配置类中的bean加载顺序,在不强制指定依赖的情况下是怎样的呢?

1. demo设计

在默认的加载条件下,包下面的bean加载顺序是根据命名的排序来的,接下来让我们来创建一个用来测试bean加载顺序的case

  • 同一个包下,创建6个bean: Demo0 , DemoA , DemoB , DemoC , DemoD , DemoE
  • 其中 Demo0 DemoE 为普通的bean
  • 其中 DemoA , DemoC 由配置类1注册
  • 其中 DemoB , DemoD 有配置类2注册

具体代码如下

@Component
public class Demo0 {
    private String name = "demo0";
    public Demo0() {
        System.out.println(name);
    }
}
public class DemoA {
    private String name = "demoA";
    public DemoA() {
        System.out.println(name);
    }
}
public class DemoB {
    private String name = "demoB";
    public DemoB() {
        System.out.println(name);
    }
}
public class DemoC {
    private String name = "demoC";
    public DemoC() {
        System.out.println(name);
    }
}
public class DemoD {
    private String name = "demoD";
    public DemoD() {
        System.out.println(name);
    }
}
@Component
public class DemoE {
    private String name = "demoE";
    public DemoE() {
        System.out.println(name);
    }
}

对应的配置类

public class ToSelectorAutoConfig1 {
    @Bean
    public DemoA demoA() {
        return new DemoA();
    }
    @Bean
    public DemoC demoC() {
        return new DemoC();
    }
}

public class ToSelectorAutoConfig2 {
    @Bean
    public DemoB demoB() {
        return new DemoB();
    }
    @Bean
    public DemoD demoD() {
        return new DemoD();
    }
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigSelector.class)
public @interface DemoSelector {
    String value() default "all";
}
public class ConfigSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes =
                AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(DemoSelector.class.getName()));

        String config = attributes.getString("value");
        if ("config1".equalsIgnoreCase(config)) {
            return new String[]{ToSelectorAutoConfig1.class.getName()};
        } else if ("config2".equalsIgnoreCase(config)) {
            return new String[]{ToSelectorAutoConfig2.class.getName()};
        } else {
            return new String[]{ToSelectorAutoConfig2.class.getName(), ToSelectorAutoConfig1.class.getName()};
        }
    }
}

注意一下 ConfigSelector ,默认的 DemoSelector 注解表示全部加载,返回的数组中,包含两个配置类,其中Config2在Confgi1的前面

2. 加载顺序实测

稍微修改一下前面的启动类,加上 @DemoSelector 注解

PrintSelector(PrintConfigSelector.FileConfiguration .class)
//@PrintSelector(PrintConfigSelector.DbConfiguration .class)
//@PrintSelector
@DemoSelector
@SpringBootApplication
public class Application {
    public Application(IPrint print) {
        print.print();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

上面的case中,我们定义的六个bean都会被加载,根据输出结果来判断默认的加载顺序

191214-SpringBoot系列教程自动配置选择生效

从输出结果来看,先加载普通的bean对象;然后再加载Config2中定义的bean,最后则是Config1中定义的bean;

接下来调整一下ImportSelector返回的数组对象中,两个配置类的顺序,如果最终输出是Config1中定义的bean先被加载,那么就可以说明返回的顺序指定了这些配置类中bean的加载顺序

191214-SpringBoot系列教程自动配置选择生效

输出的结果印证了我们的猜想

最后一个疑问,在默认的bean初始化顺序过程中,普通的bean对象加载顺序是否是优于我们通过 ImportSelector 来注册的bean呢?

  • 从输出结果好像是这样的,但是这个case并不充分,没法完全验证这个观点,想要确切的搞清楚这一点,还是得通过源码分析(虽然实际上是这样的)

注意

上面的分析只是考虑默认的bean初始化顺序,我们依然是可以通过构造方法引入的方式或者 @DependOn 注解来强制指定bean的初始化顺序的

小结

最后小结一下ImportSelector的用法

  • 实现接口,返回String数组,数组成员为配置类的全路径
  • 在配置类中定义bean
  • 返回数组中配置类的顺序,指定了配置类中bean的默认加载顺序
  • 通过 @Import 直接来使 ImportSelector 接口生效

此外还有一个类似的接口 DeferredImportSelector ,区别在于实现 DeferredImportSelector 的类优先级会低与直接实现 ImportSelector 的类,而且可以通过 @Order 决定优先级;优先级越高的越先被调用执行

II. 其他

0. 项目

  • 工程: https://github.com/liuyueyi/spring-boot-demo
  • 项目: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/005-config-selector

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 https://blog.hhui.top
  • 一灰灰Blog-Spring专题博客 http://spring.hhui.top

191214-SpringBoot系列教程自动配置选择生效

打赏 如果觉得我的文章对您有帮助,请随意打赏。

原文  http://spring.hhui.top/spring-blog/2019/12/14/191214-SpringBoot系列教程自动配置选择生效/
正文到此结束
Loading...