手把手教你定制標(biāo)準(zhǔn)Spring Boot starter,真的很清晰
寫在前面
我們每次構(gòu)建一個(gè) Spring 應(yīng)用程序時(shí),我們都不希望從頭開始實(shí)現(xiàn)具有「橫切關(guān)注點(diǎn)」的內(nèi)容;相反,我們希望一次性實(shí)現(xiàn)這些功能,并根據(jù)需要將它們包含到任何我們要構(gòu)建的應(yīng)用程序中
橫切關(guān)注點(diǎn)
橫切關(guān)注點(diǎn): 指的是一些具有橫越多個(gè)模塊的行為 (來自維基百科的介紹)
說白了就是多個(gè)項(xiàng)目或模塊都可以用到的內(nèi)容,比如一個(gè) SDK
在Spring Boot中,用于表示提供這種橫切關(guān)注點(diǎn)的模塊的術(shù)語是 starter,通過依賴 starter 可以輕松使用其包含的一些功能特性,無論你的工作中是否會構(gòu)建自己的 starter,你都要具有構(gòu)建 「starter」的思想,本文將結(jié)合 Spring Boot 官方標(biāo)準(zhǔn)構(gòu)建一個(gè)簡單的 starter
自定義 starter
在我們深入了解如何自定義 starter 之前,為了更好的理解我們每一步在干什么,以及 starter 是如何起作用的,我們先從宏觀角度來看 starter 的結(jié)構(gòu)組成到底是什么樣的
通常一個(gè)完整的 starter 需要包含下面兩個(gè)組件:
- Auto-Configure Module
- Starter Module
如果你看下面這兩個(gè)組件的解釋有些抽象,大概了解一下,閱讀完該文章回看這里就會豁然開朗了
Auto-Configure Module
Auto-Configure Module (自動配置模塊) 是包含自動配置類的 Maven 或 Gradle 模塊。通過這種方式,我們可以構(gòu)建可以自動貢獻(xiàn)于應(yīng)用程序上下文的模塊,以及添加某個(gè)特性或提供對某個(gè)外部庫的訪問
Starter Module
Spring Boot Starter 是一個(gè) Maven 或 Gradle 模塊,其唯一目的是提供 "啟動" 某個(gè)特性所需的所有依賴項(xiàng)。可以包含一個(gè)或多個(gè) Auto-Configure Module (自動配置模塊)的依賴項(xiàng),以及可能需要的任何其他依賴項(xiàng)。這樣,在Spring 啟動應(yīng)用程序中,我們只需要添加這個(gè) starter 依賴就可以使用其特性
: Spring 官方參考手冊建議將自動配置分離,并將每個(gè)自動配置啟動到一個(gè)獨(dú)立的 Maven 或 Gradle 模塊中,從而將自動配置和依賴項(xiàng)管理分離開來。如果你沒有建立一個(gè)供成千上萬用戶使用的開源庫,也可以將二者合并到一個(gè) 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 創(chuàng)建
先來全局看一下項(xiàng)目結(jié)構(gòu):
一級目錄結(jié)構(gòu):.
- ├── pom.xml
- ├── rgyb-spring-boot-autoconfigure
- ├── rgyb-spring-boot-sample
- └── rgyb-spring-boot-starter
二級目錄結(jié)構(gòu):.
- ├── pom.xml
- ├── rgyb-spring-boot-autoconfigure
- │ ├── pom.xml
- │ └── src
- ├── rgyb-spring-boot-sample
- │ ├── pom.xml
- │ └── src
- └── rgyb-spring-boot-starter
- ├── pom.xml
- └── src
創(chuàng)建一個(gè)空的父親 Maven Module,主要提供依賴管理,這樣 SubModule 不用單獨(dú)維護(hù)依賴版本號,來看 pom.xml 內(nèi)容:
- <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默認(rèn)不引入這些依賴,需要顯式的指定 -->
- </dependencyManagement>
Auto-Configure Module 構(gòu)建
新建類 GreetingAutoConfiguration
- @Configuration
- public class GreetingAutoConfiguration {
- @Bean
- public GreetingService greetingService(GreetingProperties greetingProperties){
- return new GreetingService(greetingProperties.getMembers());
- }
- }
我們用 @Configuration 注解標(biāo)記類 GreetingAutoConfiguration,作為 starter 的入口點(diǎn)。這個(gè)配置包含了我們需要提供starter特性的所有 @Bean 定義,在本例中,為了簡單闡述問題,我們只將 GreetingService Bean 添加到應(yīng)用程序上下文
GreetingService 內(nèi)容如下:
- @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 不存在需要手工創(chuàng)建),向文件寫入內(nèi)容:
- org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
- top.dayarch.autoconfigure.GreetingAutoConfiguration
Spring 啟動時(shí)會在其 classpath 中所有的 spring.factoreis 文件,并加載里面的聲明配置,GreetingAutoConfiguration 類就緒后,我們的 Spring Boot Starter 就有了一個(gè)自動激活的入口點(diǎn)
到這里這個(gè) "不完全的 starter" 已經(jīng)可以使用了。但因?yàn)樗亲詣蛹せ畹?,為了個(gè)讓其靈活可用,我們需要讓其按照我們的意愿來激活使用,所以我們需要條件注解來幫忙
條件配置
為類添加兩個(gè)條件注解:
- @Configuration
- @ConditionalOnProperty(value = "rgyb.greeting.enable", havingValue = "true")
- @ConditionalOnClass(DummyEmail.class)
- public class GreetingAutoConfiguration {
- ...
- }
- 通過使用 @ConditionalOnProperty 注解,我們告訴 Spring,只有屬性 rgyb.greeting.enable 值被設(shè)置為 true 時(shí),才將 GreetingAutoConfiguration (以及它聲明的所有 bean ) 包含到應(yīng)用程序上下文中
- 通過使用 @ConditionalOnClass 注解,我們告訴Spring 只有類 DummyEmail.class 存在于 classpath 時(shí),才將 GreetingAutoConfiguration (以及它聲明的所有 bean ) 包含到應(yīng)用程序上下文中
多個(gè)條件是 and/與的關(guān)系,既只有滿足全部條件時(shí),才會加載 GreetingAutoConfiguration
如果你對條件注解的使用還不是很明確,可以查看我之前的文章: @Conditional注解,靈活配置 Spring Boot
配置屬性管理
上面使用了 @ConditionalOnProperty 注解,實(shí)際 starter 中可能有非常多的屬性,所以我們需要將這些屬性集中管理:
- @Data
- @ConfigurationProperties(prefix = "rgyb.greeting")
- public class GreetingProperties {
- /**
- * GreetingProperties 開關(guān)
- */
- boolean enable = false;
- /**
- * 需要打招呼的成員列表
- */
- List<String> members = new ArrayList<>();
- }
我們知道這些屬性是要在 application.yml 中使用的,當(dāng)我們需要使用這些屬性時(shí),為了讓 IDE 給出更友好的提示,我們需要在 pom.xml 中添加依賴:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-configuration-processor</artifactId>
- <optional>true</optional>
- </dependency>
這樣當(dāng)我們 mvn compile 時(shí),會在生成一個(gè)名為 spring-configuration-metadata.json JSON 文件,文件內(nèi)容如下:
生成的內(nèi)容在接下來的內(nèi)容中用到,且看
提升啟動時(shí)間
對于類路徑上的每個(gè)自動配置類,Spring Boot 必須計(jì)算 @Conditional… 條件值,用于決定是否加載自動配置及其所需的所有類,根據(jù) Spring 啟動應(yīng)用程序中 starter 的大小和數(shù)量,這可能是一個(gè)非常昂貴的操作,并且會影響啟動時(shí)間,為了提升啟動時(shí)間,我們需要在 pom.xml 中添加另外一個(gè)依賴:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-autoconfigure-processor</artifactId>
- <optional>true</optional>
- </dependency>
這個(gè)注解會生成一個(gè)名為 spring-autoconfigure-metadata.properties Property 文件,其內(nèi)容如下:
這樣,Spring Boot 在啟動期間讀取這些元數(shù)據(jù),可以過濾出不滿足條件的配置,而不必實(shí)際檢查這些類,提升啟動速度
到這里關(guān)于 Auto-Configure Module 就構(gòu)建完了,我們需要繼續(xù)完成 Starter Module 的構(gòu)建
Starter Module 構(gòu)建
Starter Module 的構(gòu)建很簡單了,你可以認(rèn)為它就是一個(gè)空 module,除了依賴 Auto-Configure Module,其唯一作用就是為了使用 starter 功能特性提供所有必須依賴,所以我們?yōu)?starter module 的 pom.xml 文件添加如下內(nèi)容:
- <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 , 其內(nèi)容如下:
- providers: rgyb-spring-boot-autoconfigure
該文件主要作用是說明 starter module 的依賴信息,多個(gè)依賴以逗號分隔就好,該文件不會影響 starter 的使用,可有可無
Starter Module 就可以這么簡單,將兩個(gè) module 分別 mvn install 到本地 Maven Repository,接下來我們創(chuàng)建 sample module 引入這個(gè) starter 依賴時(shí)就會從本地 Maven Repository 中拉取
創(chuàng)建 Sample Module
我們可以通過 Spring Initializr 正常初始化一個(gè) Spring Boot 項(xiàng)目 (rgyb-spring-boot-sample),引入我們剛剛創(chuàng)建的 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 的時(shí)候,會出現(xiàn)下圖的提示,這樣會更友好,當(dāng)然為了規(guī)范,屬性描述最好也用英文描述,這里為了說明問題用了中文描述:
編寫測試類
我們編寫測試用例:
- @Autowired(required = false)
- private GreetingService greetingService;
- @Test
- public void testGreeting() {
- greetingService.sayHello();
- }
測試結(jié)果如下:
- hello 李雷
- hello 韓梅梅
總結(jié)
到這里完整的 starter 開發(fā)就結(jié)束了,希望大家了解其構(gòu)建過程,目錄結(jié)構(gòu)及命名等標(biāo)準(zhǔn),這樣有相應(yīng)的業(yè)務(wù)需求時(shí)都可以開發(fā)自己的 starter 被其他人應(yīng)用起來
starter 開發(fā)好了,別人可以手動添加依賴引入 starter 的相關(guān)功能,那我們?nèi)绾蜗?Spring Initializr 一樣,通過下來菜單選擇我們的 starter 呢,這樣直接初始化好整個(gè)項(xiàng)目,接下來的文章我們會模仿 Spring Initializr 自定義我們自的 Initializr
知識點(diǎn)說明
Dependency optinal
為什么 Auto-Configure Module 的 dependency 都是 optional = true 呢?
這涉及到 Maven 傳遞性依賴的問題,詳情請看 Maven 依賴傳遞性透徹理解
spring.factories
Spring Boot 是如何加載這個(gè)文件并找到我們的配置類的
下圖是 Spring Boot 應(yīng)用程序啟動的調(diào)用棧的一部分,我添加了斷點(diǎn):
打開 SpringFactoriesLoader 類,映入眼簾的就是這個(gè)內(nèi)容:
這兩張圖應(yīng)該足夠說明問題了,是 SPI 的一種加載方式,更細(xì)節(jié)的內(nèi)容請大家自己去發(fā)現(xiàn)吧
實(shí)際案例
這里推薦查看 mybatis-spring-boot-starter 這個(gè)非 Spring 官方的案例,從中我們:
- 模仿其目錄結(jié)構(gòu)
- 模仿其設(shè)計(jì)理念
- 模仿其編碼規(guī)范
另外,本文的案例我已上傳,公眾號回復(fù)「demo」,打開鏈接,查看 customstarter 目錄下內(nèi)容即可
靈魂追問
- 在生成 spring-autoconfigure-metadata.properties 文件時(shí),為什么 @ConditionalOnProperty 的內(nèi)容沒有被寫進(jìn)去
- 如果我們要將依賴上傳至 remote central repository,你知道怎樣搭建自己的 maven repository 嗎?
- 你的燈還亮著嗎?