手把手教你定製標準Spring Boot starter,真的很清晰

發表於2019-11-12

寫在前面

我們每次構建一個 Spring 應用程式時,我們都不希望從頭開始實現具有「橫切關注點」的內容;相反,我們希望一次性實現這些功能,並根據需要將它們包含到任何我們要構建的應用程式中

橫切關注點

橫切關注點: 指的是一些具有橫越多個模組的行為 (來自維基百科的介紹)
說白了就是多個專案或模組都可以用到的內容,比如一個 SDK

在Spring Boot中,用於表示提供這種橫切關注點的模組的術語是 starter,通過依賴 starter 可以輕鬆使用其包含的一些功能特性,無論你的工作中是否會構建自己的 starter,你都要具有構建 「starter」的思想,本文將結合 Spring Boot 官方標準構建一個簡單的 starter

自定義 starter

在我們深入瞭解如何自定義 starter 之前,為了更好的理解我們每一步在幹什麼,以及 starter 是如何起作用的,我們先從巨集觀角度來看 starter 的結構組成到底是什麼樣的

通常一個完整的 starter 需要包含下面兩個元件:

  1. Auto-Configure Module
  2. Starter Module

如果你看下面這兩個元件的解釋有些抽象,大概瞭解一下,閱讀完該文章回看這裡就會豁然開朗了

Auto-Configure Module

Auto-Configure Module (自動配置模組) 是包含自動配置類的 Maven 或 Gradle 模組。通過這種方式,我們可以構建可以自動貢獻於應用程式上下文的模組,以及新增某個特性或提供對某個外部庫的訪問

Starter Module

Spring Boot Starter 是一個 Maven 或 Gradle 模組,其唯一目的是提供 "啟動" 某個特性所需的所有依賴項。可以包含一個或多個 Auto-Configure Module (自動配置模組)的依賴項,以及可能需要的任何其他依賴項。這樣,在Spring 啟動應用程式中,我們只需要新增這個 starter 依賴就可以使用其特性

⚠️: Spring 官方參考手冊建議將自動配置分離,並將每個自動配置啟動到一個獨立的 Maven 或 Gradle 模組中,從而將自動配置和依賴項管理分離開來。如果你沒有建立一個供成千上萬使用者使用的開源庫,也可以將二者合併到一個 module 中
You may combine the auto-configuration code and the dependency management in a single module if you do not need to separate those two concerns

命名

來自 Spring 官方的 starter 都是 以 spring-boot-starter 開頭,比如:

  • spring-boot-starter-web
  • spring-boot-starter-aop

如果我們自定義 starter 功能名稱叫acme,那麼我們的命名是這樣的:

  • acme-spring-boot-starter
  • acme-spring-boot-autoconfigure

如果 starter 中用到了配置 keys,也要注意不要使用 Spring Boot 使用的名稱空間,比如(server,management,spring)

Parent Module 建立

先來全域性看一下專案結構:
一級目錄結構:

.
├── pom.xml
├── rgyb-spring-boot-autoconfigure
├── rgyb-spring-boot-sample
└── rgyb-spring-boot-starter

二級目錄結構:

.
├── pom.xml
├── rgyb-spring-boot-autoconfigure
│   ├── pom.xml
│   └── src
├── rgyb-spring-boot-sample
│   ├── pom.xml
│   └── src
└── rgyb-spring-boot-starter
    ├── pom.xml
    └── src

建立一個空的父親 Maven Module,主要提供依賴管理,這樣 SubModule 不用單獨維護依賴版本號,來看 pom.xml 內容:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>

    <!--  新增其他全域性依賴管理到這裡,submodule預設不引入這些依賴,需要顯式的指定  -->
</dependencyManagement>

Auto-Configure Module 構建

新建類 GreetingAutoConfiguration

@Configuration
public class GreetingAutoConfiguration {

    @Bean
    public GreetingService greetingService(GreetingProperties greetingProperties){
        return new GreetingService(greetingProperties.getMembers());
    }
}

我們用 @Configuration 註解標記類 GreetingAutoConfiguration,作為 starter 的入口點。這個配置包含了我們需要提供starter特性的所有 @Bean 定義,在本例中,為了簡單闡述問題,我們只將 GreetingService Bean 新增到應用程式上下文

GreetingService 內容如下:

@AllArgsConstructor
public class GreetingService {

    private List<String> members = new ArrayList<>();

    public void sayHello(){
        members.forEach(s -> System.out.println("hello " + s));
    }
}

在 resources 目錄下新建檔案 META-INF/spring.factories (如果目錄 META-INF 不存在需要手工建立),向檔案寫入內容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 top.dayarch.autoconfigure.GreetingAutoConfiguration

Spring 啟動時會在其 classpath 中所有的 spring.factoreis 檔案,並載入裡面的宣告配置,GreetingAutoConfiguration 類就緒後,我們的 Spring Boot Starter 就有了一個自動啟用的入口點

到這裡這個 "不完全的 starter" 已經可以使用了。但因為它是自動啟用的,為了個讓其靈活可用,我們需要讓其按照我們的意願來啟用使用,所以我們需要條件註解來幫忙

條件配置

為類新增兩個條件註解:

@Configuration
@ConditionalOnProperty(value = "rgyb.greeting.enable", havingValue = "true")
@ConditionalOnClass(DummyEmail.class)
public class GreetingAutoConfiguration {
    ...
}
  • 通過使用 @ConditionalOnProperty 註解,我們告訴 Spring,只有屬性 rgyb.greeting.enable 值被設定為 true 時,才將 GreetingAutoConfiguration (以及它宣告的所有 bean ) 包含到應用程式上下文中
  • 通過使用 @ConditionalOnClass 註解,我們告訴Spring 只有類 DummyEmail.class 存在於 classpath 時,才將 GreetingAutoConfiguration (以及它宣告的所有 bean ) 包含到應用程式上下文中

多個條件是 and/與的關係,既只有滿足全部條件時,才會載入 GreetingAutoConfiguration

如果你對條件註解的使用還不是很明確,可以檢視我之前的文章: @Conditional註解,靈活配置 Spring Boot

配置屬性管理

上面使用了 @ConditionalOnProperty 註解,實際 starter 中可能有非常多的屬性,所以我們需要將這些屬性集中管理:

@Data
@ConfigurationProperties(prefix = "rgyb.greeting")
public class GreetingProperties {

    /**
     * GreetingProperties 開關
     */
    boolean enable = false;

    /**
     * 需要打招呼的成員列表
     */
    List<String> members = new ArrayList<>();
}

我們知道這些屬性是要在 application.yml 中使用的,當我們需要使用這些屬性時,為了讓 IDE 給出更友好的提示,我們需要在 pom.xml 中新增依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

這樣當我們 mvn compile 時,會在生成一個名為 spring-configuration-metadata.json JSON 檔案,檔案內容如下:

生成的內容在接下來的內容中用到,且看

提升啟動時間

對於類路徑上的每個自動配置類,Spring Boot 必須計算 @Conditional… 條件值,用於決定是否載入自動配置及其所需的所有類,根據 Spring 啟動應用程式中 starter 的大小和數量,這可能是一個非常昂貴的操作,並且會影響啟動時間,為了提升啟動時間,我們需要在 pom.xml 中新增另外一個依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>

這個註解會生成一個名為 spring-autoconfigure-metadata.properties Property 檔案,其內容如下:

這樣,Spring Boot 在啟動期間讀取這些後設資料,可以過濾出不滿足條件的配置,而不必實際檢查這些類,提升啟動速度

到這裡關於 Auto-Configure Module 就構建完了,我們需要繼續完成 Starter Module 的構建

Starter Module 構建

Starter Module 的構建很簡單了,你可以認為它就是一個空 module,除了依賴 Auto-Configure Module,其唯一作用就是為了使用 starter 功能特性提供所有必須依賴,所以我們為 starter module 的 pom.xml 檔案新增如下內容:

<dependencies>
    <dependency>
        <groupId>top.dayarch.learnings</groupId>
        <artifactId>rgyb-spring-boot-autoconfigure</artifactId>
        <version>1.0.0.RELEASE</version>
    </dependency>

    <!-- 在此處新增其他必要依賴,保證starter可用 -->
</dependencies>

同樣在 resources 目錄下新建檔案 META-INF/spring.providers , 其內容如下:

providers: rgyb-spring-boot-autoconfigure

該檔案主要作用是說明 starter module 的依賴資訊,多個依賴以逗號分隔就好,該檔案不會影響 starter 的使用,可有可無

Starter Module 就可以這麼簡單,將兩個 module 分別 mvn install 到本地 Maven Repository,接下來我們建立 sample module 引入這個 starter 依賴時就會從本地 Maven Repository 中拉取

建立 Sample Module

我們可以通過 Spring Initializr 正常初始化一個 Spring Boot 專案 (rgyb-spring-boot-sample),引入我們剛剛建立的 starter 依賴,在 sample pom.xml 中新增依賴:

<dependency>
    <groupId>top.dayarch.learnings</groupId>
    <artifactId>rgyb-spring-boot-starter</artifactId>
    <version>1.0.0.RELEASE</version>
</dependency>

接下來配置 application.yml 屬性

rgyb:
  greeting:
    enable: true
    members:
      - 李雷
      - 韓梅梅

在我們配置 YAML 的時候,會出現下圖的提示,這樣會更友好,當然為了規範,屬性描述最好也用英文描述,這裡為了說明問題用了中文描述:

編寫測試類

我們編寫測試用例:

@Autowired(required = false)
private GreetingService greetingService;

@Test
public void testGreeting() {
    greetingService.sayHello();
}

測試結果如下:

hello 李雷
hello 韓梅梅

總結

到這裡完整的 starter 開發就結束了,希望大家瞭解其構建過程,目錄結構及命名等標準,這樣有相應的業務需求時都可以開發自己的 starter 被其他人應用起來

starter 開發好了,別人可以手動新增依賴引入 starter 的相關功能,那我們如何像 Spring Initializr 一樣,通過下來選單選擇我們的 starter 呢,這樣直接初始化好整個專案,接下來的文章我們會模仿 Spring Initializr 自定義我們自的 Initializr

知識點說明

Dependency optinal

為什麼 Auto-Configure Module 的 dependency 都是 optional = true 呢?
這涉及到 Maven 傳遞性依賴的問題,詳情請看 Maven 依賴傳遞性透徹理解

spring.factories

Spring Boot 是如何載入這個檔案並找到我們的配置類的
下圖是 Spring Boot 應用程式啟動的呼叫棧的一部分,我新增了斷點:

開啟 SpringFactoriesLoader 類,映入眼簾的就是這個內容:

這兩張圖應該足夠說明問題了,是 SPI 的一種載入方式,更細節的內容請大家自己去發現吧

實際案例

這裡推薦檢視 mybatis-spring-boot-starter 這個非 Spring 官方的案例,從中我們:

  • 模仿其目錄結構
  • 模仿其設計理念
  • 模仿其編碼規範

另外,本文的案例我已上傳,公眾號回覆「demo」,開啟連結,檢視 customstarter 目錄下內容即可

靈魂追問

  1. 在生成 spring-autoconfigure-metadata.properties 檔案時,為什麼 @ConditionalOnProperty 的內容沒有被寫進去
  2. 如果我們要將依賴上傳至 remote central repository,你知道怎樣搭建自己的 maven repository 嗎?
  3. 你的燈還亮著嗎?

部落格已改版,提前發現更多精彩,請訪問: https://dayarch.top

歡迎關注我的公眾號 「日拱一兵」,趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地
如果對我的專題內容感興趣,或搶先看更多內容,歡迎訪問我的部落格 dayarch.top

相關文章