写了这么久的 Spring 系列博文,发现了一个问题,之前所有的文章都是围绕的让一个东西生效;那么有没有反其道而行之的呢?
我们知道可以通过@ConditionOnXxx
来决定一个配置类是否可以加载,那么假设有这么个应用场景
针对上面的 case,当然也可以使用@ConditionOnExpression
来实现,除此之外推荐一种更优雅的选择注入方式ImportSelector
<!-- more -->
本文使用的 spring boot 版本为 2.1.2.RELEASE
接下来我们使用 ImportSelector 来实现上面提出的 case
一个接口类,三个实现类
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");
}
}
自定义一个 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();
}
}
}
主要用来注入PrintConfigSelector
来生效,其中 value 属性,用来具体选择让哪一个配置生效,默认注册ConsolePrint
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(PrintConfigSelector.class)
public @interface PrintSelector {
Class<?> value() default PrintConfigSelector.ConsoleConfiguration.class;
}
//@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 实现类
虽然上面通过一个实际的 case 实现来演示了ImportSelector
的使用姿势,可以用来选择某些配置类生效。但还有一些其他的知识点,有必要指出一下
通过 ImportSelector 选择的配置类中的 bean 加载顺序,在不强制指定依赖的情况下是怎样的呢?
在默认的加载条件下,包下面的 bean 加载顺序是根据命名的排序来的,接下来让我们来创建一个用来测试 bean 加载顺序的 case
Demo0
, DemoA
, DemoB
, DemoC
, DemoD
, DemoE
Demo0
DemoE
为普通的 beanDemoA
, 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 的前面
稍微修改一下前面的启动类,加上@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 呢?
注意
上面的分析只是考虑默认的 bean 初始化顺序,我们依然是可以通过构造方法引入的方式或者@DependOn
注解来强制指定 bean 的初始化顺序的
最后小结一下 ImportSelector 的用法
@Import
直接来使ImportSelector
接口生效此外还有一个类似的接口DeferredImportSelector
,区别在于实现DeferredImportSelector
的类优先级会低与直接实现ImportSelector
的类,而且可以通过@Order
决定优先级;优先级越高的越先被调用执行