SpringBoot 系列教程自動配置選擇生效

191214-SpringBoot 系列教程自動配置選擇生效java


寫了這麼久的 Spring 系列博文,發現了一個問題,以前全部的文章都是圍繞的讓一個東西生效;那麼有沒有反其道而行之的呢?git

咱們知道能夠經過@ConditionOnXxx來決定一個配置類是否能夠加載,那麼假設有這麼個應用場景github

  • 有一個 Print 的抽象接口,有多個實現,如輸出到控制檯的 ConsolePrint, 輸出到文件的 FilePrint, 輸出到 db 的 DbPrint
  • 咱們在實際使用的時候,根據用戶的選擇,使用其中的一個具體實現

針對上面的 case,固然也可使用@ConditionOnExpression來實現,除此以外推薦一種更優雅的選擇注入方式ImportSelectorspring

<!-- more -->數組

I. 配置選擇

本文使用的 spring boot 版本爲 2.1.2.RELEASEide

接下來咱們使用 ImportSelector 來實現上面提出的 casespring-boot

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 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索