11.24 手把手教你定製標準Spring Boot starter,看起來神清氣爽

寫在前面

我們每次構建一個 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>


/<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));
}
}/<string>

在 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<>();
}/<string>

我們知道這些屬性是要在 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 starter,看起來神清氣爽

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

提升啟動時間

對於類路徑上的每個自動配置類,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 starter,看起來神清氣爽

這樣,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>


/<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 的時候,會出現下圖的提示,這樣會更友好,當然為了規範,屬性描述最好也用英文描述,這裡為了說明問題用了中文描述:

手把手教你定製標準Spring Boot starter,看起來神清氣爽

編寫測試類

我們編寫測試用例:

@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 應用程序啟動的調用棧的一部分,我添加了斷點:

手把手教你定製標準Spring Boot starter,看起來神清氣爽

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

手把手教你定製標準Spring Boot starter,看起來神清氣爽

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

實際案例

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

  • 模仿其目錄結構
  • 模仿其設計理念
  • 模仿其編碼規範
手把手教你定製標準Spring Boot starter,看起來神清氣爽


分享到:


相關文章: