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 - 其中
Demo0DemoE为普通的 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. 项目
- 工程: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

SpringBoot 系列教程自动配置选择生效的更多相关文章
- SpringBoot 系列教程之事务不生效的几种 case
SpringBoot 系列教程之事务不生效的几种 case 前面几篇博文介绍了声明式事务@Transactional的使用姿势,只知道正确的使用姿势可能还不够,还得知道什么场景下不生效,避免采坑.本文 ...
- SpringBoot系列教程web篇之404、500异常页面配置
接着前面几篇web处理请求的博文,本文将说明,当出现异常的场景下,如404请求url不存在,,403无权,500服务器异常时,我们可以如何处理 原文友链: SpringBoot系列教程web篇之404 ...
- 案例解析:springboot自动配置未生效问题定位(条件断点)
Spring Boot在为开发人员提供更高层次的封装,进而提高开发效率的同时,也为出现问题时如何进行定位带来了一定复杂性与难度.但Spring Boot同时又提供了一些诊断工具来辅助开发与分析,如sp ...
- SpringBoot系列教程JPA之新增记录使用姿势
SpringBoot系列教程JPA之新增记录使用姿势 上一篇文章介绍了如何快速的搭建一个JPA的项目环境,并给出了一个简单的演示demo,接下来我们开始业务教程,也就是我们常说的CURD,接下来进入第 ...
- SpringBoot 系列教程之编程式事务使用姿势介绍篇
SpringBoot 系列教程之编程式事务使用姿势介绍篇 前面介绍的几篇事务的博文,主要是利用@Transactional注解的声明式使用姿势,其好处在于使用简单,侵入性低,可辨识性高(一看就知道使用 ...
- Java工程师之SpringBoot系列教程前言&目录
前言 与时俱进是每一个程序员都应该有的意识,当一个Java程序员在当代步遍布的时候,你就行该想到我能多学点什么.可观的是后端的框架是稳定的,它们能够维持更久的时间在应用中,而不用担心技术的更新换代.但 ...
- SpringBoot系列教程起步
本篇学习目标 Spring Boot是什么? 构建Spring Boot应用程序 三分钟开发SpringBoot应用程序 本章源码下载 Spring Boot是什么? spring Boot是由Piv ...
- 【玩转SpringBoot】给自动配置来个整体大揭秘
上一篇文章中提到的条件注解,只是自动配置整体解决方案中的一个环节而已,可以说是管中窥豹. 本文就逐步擦除迷雾,让整体浮现出来,这样就会有一个宏观的认识. 除了写代码之外,还能干点什么? 提到“配置”这 ...
- SpringBoot系列教程web篇之过滤器Filter使用指南
web三大组件之一Filter,可以说是很多小伙伴学习java web时最早接触的知识点了,然而学得早不代表就用得多.基本上,如果不是让你从0到1写一个web应用(或者说即便从0到1写一个web应用) ...
随机推荐
- SqlServer 创建数据库两种方式
一个SqlServer 数据库实例大概可以创建三万多个数据库. 创建数据库的第一种方式:SqlServer Management Studio管理工具进行可视化创建. 1).打开数据库管理工具,在&q ...
- WebGIS之MapBox篇
前面在Arcgis的基础上玩了玩,这不最近又去摸索了一下Web上开源的GIS;这次选择了基于MapBox来实现一些效果: 1.加载自己发布的本地瓦片效果 2.加载热力图.Echarts.三位建筑.路况 ...
- MySQL入门——在Windows下安装MySQL
MySQL入门——在Windows下安装MySQL 摘要:本文主要说明了如何下Windows环境下安装MySQL. 查看电脑上是否安装了MySQL 打开cmd窗口,输入 services.msc 命令 ...
- mp4文件格式解析二
目前MP4的概念被炒得很火,也很乱.最开始MP4指的是音频(MP3的升级版),即MPEG-2 AAC标准.随后MP4概念被转移到视频上,对应的是MPEG-4标准.而现在我们流行的叫法,多半是指能播放M ...
- element-ui Upload 上传组件源码分析整理笔记(十四)
简单写了部分注释,upload-dragger.vue(拖拽上传时显示此组件).upload-list.vue(已上传文件列表)源码暂未添加多少注释,等有空再补充,先记下来... index.vue ...
- ios-tableview加载卡顿的解决方案
参考文章:http://www.cocoachina.com/articles/11968 总结: 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法 ...
- python从入门到放弃之进程
在理解进程之前我们先了解一下什么是进程的概念吧 以下就是我总结的一些基本的进程概念 进程就是正在运行的程序,它是操作系统中,资源分配的最小单位(通俗易懂点也就是电脑给程序分配的一定内存操作空间).资源 ...
- xshell5运行hadoop集群
---恢复内容开始--- 1.CentOS主机配置 在配置Hadoop过程中,防火墙必须优先关闭SELinux,否则将影响后续Hadoop配置与使用,命令如下: # 查看 “系统防火墙” 状态命令 s ...
- 解决Error: ENOENT: no such file or directory, scandir 'xxx\node-sass\vendor'
解决方案是执行以下方法: npm rebuild node-sass
- 201871010114-李岩松《面向对象程序设计(java)》第十七周学习总结
项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...