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 都会被加载,根据输出结果来判断默认的加载顺序

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

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

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

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

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

注意

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

小结

最后小结一下 ImportSelector 的用法

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

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

II. 其他

0. 项目

1. 一灰灰 Blog

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

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

SpringBoot 系列教程自动配置选择生效的更多相关文章

  1. SpringBoot 系列教程之事务不生效的几种 case

    SpringBoot 系列教程之事务不生效的几种 case 前面几篇博文介绍了声明式事务@Transactional的使用姿势,只知道正确的使用姿势可能还不够,还得知道什么场景下不生效,避免采坑.本文 ...

  2. SpringBoot系列教程web篇之404、500异常页面配置

    接着前面几篇web处理请求的博文,本文将说明,当出现异常的场景下,如404请求url不存在,,403无权,500服务器异常时,我们可以如何处理 原文友链: SpringBoot系列教程web篇之404 ...

  3. 案例解析:springboot自动配置未生效问题定位(条件断点)

    Spring Boot在为开发人员提供更高层次的封装,进而提高开发效率的同时,也为出现问题时如何进行定位带来了一定复杂性与难度.但Spring Boot同时又提供了一些诊断工具来辅助开发与分析,如sp ...

  4. SpringBoot系列教程JPA之新增记录使用姿势

    SpringBoot系列教程JPA之新增记录使用姿势 上一篇文章介绍了如何快速的搭建一个JPA的项目环境,并给出了一个简单的演示demo,接下来我们开始业务教程,也就是我们常说的CURD,接下来进入第 ...

  5. SpringBoot 系列教程之编程式事务使用姿势介绍篇

    SpringBoot 系列教程之编程式事务使用姿势介绍篇 前面介绍的几篇事务的博文,主要是利用@Transactional注解的声明式使用姿势,其好处在于使用简单,侵入性低,可辨识性高(一看就知道使用 ...

  6. Java工程师之SpringBoot系列教程前言&目录

    前言 与时俱进是每一个程序员都应该有的意识,当一个Java程序员在当代步遍布的时候,你就行该想到我能多学点什么.可观的是后端的框架是稳定的,它们能够维持更久的时间在应用中,而不用担心技术的更新换代.但 ...

  7. SpringBoot系列教程起步

    本篇学习目标 Spring Boot是什么? 构建Spring Boot应用程序 三分钟开发SpringBoot应用程序 本章源码下载 Spring Boot是什么? spring Boot是由Piv ...

  8. 【玩转SpringBoot】给自动配置来个整体大揭秘

    上一篇文章中提到的条件注解,只是自动配置整体解决方案中的一个环节而已,可以说是管中窥豹. 本文就逐步擦除迷雾,让整体浮现出来,这样就会有一个宏观的认识. 除了写代码之外,还能干点什么? 提到“配置”这 ...

  9. SpringBoot系列教程web篇之过滤器Filter使用指南

    web三大组件之一Filter,可以说是很多小伙伴学习java web时最早接触的知识点了,然而学得早不代表就用得多.基本上,如果不是让你从0到1写一个web应用(或者说即便从0到1写一个web应用) ...

随机推荐

  1. StringBuilder.sb.AppendLine();

    StringBuilder sb=new StringBuilder(); sb.AppendLine("The world is fair and wonderful.Everything ...

  2. js数组试列题

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  3. http接口和webservice接口的区别

    web service(SOAP)与HTTP接口的区别 什么是web service? soap请求是HTTP POST的一个专用版本,遵循一种特殊的xml消息格式Content-type设置为: t ...

  4. maven-dependencies插件的模拟实现

    maven-dependencies插件的作用就是从本地的maven仓库中提取jar包,放到某个文件夹下面.这个功能其实是很简单的. 我在一家银行工作时,公司电脑都无法连外网,所以无法通过maven下 ...

  5. 使用jqPrint.js调用浏览器打印界面,打印网页中的某一部分该部分含有ECharts图表

    1.准备好js文件(我用的是谷歌浏览器) 这个文件是为了防止你的jQuery版本过高而不适配的问题 这是调用浏览器打印的js插件 2.引入js文件 <script src="js/jq ...

  6. 图解Java数据结构之环形链表

    本篇文章介绍数据结构中的环形链表. 介绍 环形链表,类似于单链表,也是一种链式存储结构,环形链表由单链表演化过来.单链表的最后一个结点的链域指向NULL,而环形链表的建立,不要专门的头结点,让最后一个 ...

  7. vue路由跳转传参的两种方法

    路由跳转: this.$router.push({ name: '工单列表', params: {p_camera_dev_name: 'xxx'} }); 使二级菜单呈点击状态: $('[index ...

  8. jquery实现一些小动画二

    jquery实现一些小动画二 jquery实现拖拽功能 <!DOCTYPE html> <html lang="en"> <head> < ...

  9. typeof运算对于null会返回“Object"

    您也许会问,为什么 typeof 运算符对于 null 值会返回 "Object".这实际上是 JavaScript 最初实现中的一个错误,然后被 ECMAScript 沿用了.现 ...

  10. 201871010128-杨丽霞《面向对象程序设计(java)》第六-七周学习总结

    201871010128-杨丽霞<面向对象程序设计(java)>第六-七周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...