咱們都知道可使用 SpringBoot 快速的開發基於 Spring 框架的項目。因爲圍繞 SpringBoot 存在不少開箱即用的 Starter 依賴,使得咱們在開發業務代碼時可以很是方便的、不須要過多關注框架的配置,而只須要關注業務便可。html
例如我想要在 SpringBoot 項目中集成 Redis,那麼我只須要加入 spring-data-redis-starter 的依賴,並簡單配置一下鏈接信息以及 Jedis 鏈接池配置就能夠。這爲咱們省去了以前不少的配置操做。甚至有些功能的開啓只須要在啓動類或配置類上增長一個註解便可完成。java
那麼若是咱們想要本身實現本身的 Starter 須要作些什麼呢?下面就開始介紹如何實現本身的 spring-boot-starter-xxx。web
從整體上來看,無非就是將Jar包做爲項目的依賴引入工程。而如今之因此增長了難度,是由於咱們引入的是Spring Boot Starter,因此咱們須要去了解Spring Boot對Spring Boot Starter的Jar包是如何加載的?下面我簡單說一下。redis
SpringBoot 在啓動時會去依賴的 starter 包中尋找 /META-INF/spring.factories 文件,而後根據文件中配置的路徑去掃描項目所依賴的 Jar 包,這相似於 Java 的 SPI 機制。spring
細節上可使用@Conditional 系列註解實現更加精確的配置加載Bean的條件。編程
JavaSPI 其實是「基於接口的編程+策略模式+配置文件」組合實現的動態加載機制。json
接下來我會實現一個普通的Spring Boot Web工程,該工程有一個Service類,類的sayHello方法會返回一個字符串,字符串能夠經過application配置文件進行配置。數組
1.新建一個Spring Boot工程,命名爲spring-boot-starter-hello,pom.xml依賴:瀏覽器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
工程命名規範tomcat
官方命名格式爲: spring-boot-starter-{name}
非官方建議命名格式:{name}-spring-boot-starter
這裏只是爲了演示,我的項目建議跟隨官方命名規範。
2.新建HelloProperties類,定義一個hello.msg參數(默認值World!)。
@ConfigurationProperties(prefix = "hello") public class HelloProperties { /** * 打招呼的內容,默認爲「World!」 */ private String msg = "World!"; public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } }
3.新建HelloService類,使用HelloProperties類的屬性。
@Service public class HelloService { @Autowired private HelloProperties helloProperties; /** * 打招呼方法 * * @param name 人名,向誰打招呼使用 * @return */ public String sayHello(String name) { return "Hello " + name + " " + helloProperties.getMsg(); } }
4.自動配置類,能夠理解爲實現自動配置功能的一個入口。
//定義爲配置類 @Configuration //在web工程條件下成立 @ConditionalOnWebApplication //啓用HelloProperties配置功能,並加入到IOC容器中 @EnableConfigurationProperties({HelloProperties.class}) //導入HelloService組件 @Import(HelloService.class) //@ComponentScan public class HelloAutoConfiguration { }
5.在resources目錄下新建META-INF目錄,並在META-INF下新建spring.factories文件,寫入:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.springbootstarterhello.HelloAutoConfiguration
6.項目到這裏就差很少了,不過做爲依賴,最好仍是再作一下收尾工做。
7.執行mvn install將spring-boot-starter-hello安裝到本地。
當你直接執行時應該會報錯,由於咱們還須要在pom.xml去掉spring-boot-maven-plugin,也就是下面這段代碼。
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
8.隨便新建一個Spring Boot工程,引入spring-boot-starter-hello依賴。
<dependency> <groupId>com.example</groupId> <artifactId>spring-boot-starter-hello</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
9.在新工程中使用spring-boot-starter-hello的sayHello功能。
@SpringBootApplication @Controller public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Autowired private HelloService helloService; @RequestMapping(value = "/sayHello") @ResponseBody public String sayHello(String name){ System.out.println(helloService.sayHello(name)); return helloService.sayHello(name); } }
訪問http://localhost:8080/sayHello?name=Mark
瀏覽器打印:Hello Mark World!
在application.properties文件中配置屬性:hello.msg = 你好!
重啓項目,再次刷新訪問,瀏覽器響應:Hello Mark 你好!
若是你遇到中文亂碼,能夠參考 Spring boot讀取application.properties中文亂碼
到目前爲止,spring-boot-starter-hello的自動配置功能已實現,而且正確使用了,但還有一點不夠完美,若是你也按上面步驟實現了本身的spring-boot-starter-hello自動配置,在application.properties中配置hello.msg屬性時,你會發現並無提示你有關該配置的信息,可是若是你想配置tomcat端口時,輸入server.port是有提示的:
這種功能如何作呢?在Spring Boot官方文檔中就已經給出了方法,新建META-INF/spring-configuration-metadata.json文件,進行配置。
那如何對spring-boot-starter-hello項目配置元數據呢?代碼以下:
{ "hints":[{ "name":"hello.msg", "values":[{ "value":"你好", "description":"中文方式打招呼" },{ "value":"Hi", "description":"英文方式打招呼" }] }], "groups":[ { "sourceType": "com.example.springbootstarterhello.HelloProperties", "name": "hello", "type": "com.example.springbootstarterhello.HelloProperties" }], "properties":[ { "sourceType": "com.example.springbootstarterhello.HelloProperties", "name": "hello.msg", "type": "java.lang.String", "description": "打招呼的內容", "defaultValue": "Worlds" }] }
而後咱們將spring-boot-starter-hello項目從新打包使用,以下圖所示,就有了屬性的提示:
下面咱們就列出有關groups、properties、hints具體使用,不過我建議你能夠先跳過這部分枯燥的內容。
「groups」中包含的JSON對象能夠包含下表中顯示的屬性:
名稱 | 類型 | 用途 |
---|---|---|
name | String | 「groups」的全名。這個屬性是強制性的 |
type | String | group數據類型的類名。例如,若是group是基於一個被@ConfigurationProperties註解的類,該屬性將包含該類的全限定名。若是基於一個@Bean方法,它將是該方法的返回類型。若是該類型未知,則該屬性將被忽略 |
description | String | 一個簡短的group描述,用於展現給用戶。若是沒有可用描述,該屬性將被忽略。推薦使用一個簡短的段落描述,第一行提供一個簡潔的總結,最後一行以句號結尾 |
sourceType | String | 貢獻該組的來源類名。例如,若是組基於一個被@ConfigurationProperties註解的@Bean方法,該屬性將包含@Configuration類的全限定名,該類包含此方法。若是來源類型未知,則該屬性將被忽略 |
sourceMethod | String | 貢獻該組的方法的全名(包含括號及參數類型)。例如,被@ConfigurationProperties註解的@Bean方法名。若是源方法未知,該屬性將被忽略 |
properties數組中包含的JSON對象可由如下屬性構成:
名稱 | 類型 | 用途 |
---|---|---|
name | String | property的全名,格式爲小寫虛線分割的形式(好比server.servlet-path)。該屬性是強制性的 |
type | String | property數據類型的類名。例如java.lang.String。該屬性能夠用來指導用戶他們能夠輸入值的類型。爲了保持一致,原生類型使用它們的包裝類代替,好比boolean變成了java.lang.Boolean。注意,這個類多是個從一個字符串轉換而來的複雜類型。若是類型未知則該屬性會被忽略 |
description | String | 一個簡短的組的描述,用於展現給用戶。若是沒有描述可用則該屬性會被忽略。推薦使用一個簡短的段落描述,開頭提供一個簡潔的總結,最後一行以句號結束 |
sourceType | String | 貢獻property的來源類名。例如,若是property來自一個被@ConfigurationProperties註解的類,該屬性將包括該類的全限定名。若是來源類型未知則該屬性會被忽略 |
defaultValue | Object | 當property沒有定義時使用的默認值。若是property類型是個數組則該屬性也能夠是個數組。若是默認值未知則該屬性會被忽略 |
deprecated | Deprecated | 指定該property是否過時。若是該字段沒有過時或該信息未知則該屬性會被忽略 |
level | String | 棄用級別,能夠是警告(默認)或錯誤。當屬性具備警告棄用級別時,它仍然應該在環境中綁定。然而,當它具備錯誤棄用級別時,該屬性再也不受管理,也不受約束 |
reason | String | 對屬性被棄用的緣由的簡短描述。若是沒有理由,能夠省略。建議描述應是簡短的段落,第一行提供簡明的摘要。描述中的最後一行應該以句點(.)結束 |
replacement | String | 替換這個廢棄屬性的屬性的全名。若是該屬性沒有替換,則能夠省略該屬性。 |
hints數組中包含的JSON對象能夠包含如下屬性:
名稱 | 類型 | 用途 |
---|---|---|
name | String | 該提示引用的屬性的全名。名稱以小寫虛構形式(例如server.servlet-path)。果屬性是指地圖(例如 system.contexts),則提示能夠應用於map()或values()的鍵。此屬性是強制性的system.context.keyssystem.context.values |
values | ValueHint[] | 由ValueHint對象定義的有效值的列表(見下文)。每一個條目定義該值而且能夠具備描述 |
providers | ValueProvider[] | 由ValueProvider對象定義的提供者列表(見下文)。每一個條目定義提供者的名稱及其參數(若是有)。 |
每一個"hints"元素的values屬性中包含的JSON對象能夠包含下表中描述的屬性:
名稱 | 類型 | 用途 |
---|---|---|
value | Object | 提示所指的元素的有效值。若是屬性的類型是一個數組,那麼它也能夠是一個值數組。這個屬性是強制性的 |
description | String | 能夠顯示給用戶的值的簡短描述。若是沒有可用的描述,能夠省略。建議描述應是簡短的段落,第一行提供簡明的摘要。描述中的最後一行應該以句點(.)結束。 |
每一個"hints"元素的providers屬性中的JSON對象能夠包含下表中描述的屬性:
名稱 | 類型 | 用途 |
---|---|---|
name | String | 用於爲提示所指的元素提供額外內容幫助的提供者的名稱。 |
parameters | JSON object | 提供程序支持的任何其餘參數(詳細信息請參閱提供程序的文檔)。 |
配置上述數據是挺麻煩的,若是能夠提供一種自動生成spring-configuration-metadata.json的依賴就行了。別說,還真有。spring-boot-configuration-processor依賴就能夠作到,它的基本原理是在編譯期使用註解處理器自動生成spring-configuration-metadata.json文件。文件中的數據來源於你是如何在類中定義hello.msg這個屬性的,它會自動採集hello.msg的默認值和註釋信息。不過我在測試時發現了中文亂碼問題,並且網上有關spring-boot-configuration-processor的學習文檔略少。
下面我貼出使用spring-boot-configuration-processor自動生成的spring-configuration-metadata.json文件內容:
{ "groups": [ { "name": "hello", "type": "com.example.springbootstarterhello.HelloProperties", "sourceType": "com.example.springbootstarterhello.HelloProperties" } ], "properties": [ { "name": "hello.msg", "type": "java.lang.String", "description": "打招呼的內容,默認爲「World!」", "sourceType": "com.example.springbootstarterhello.HelloProperties", "defaultValue": "World!" } ], "hints": [] }
能夠看到properties裏的description屬性值來源於註釋信息,defaultValue值來源於代碼中書寫的默認值。
這一步須要在idea設置中搜索Annotation Processors,勾住Enable annonation processing。
以前提到了在細節上可使用@Conditional 系列註解實現更加精確的配置加載Bean的條件。下面列舉 SpringBoot 中的全部 @Conditional 註解及做用
註解 | 做用 |
---|---|
@ConditionalOnBean | 當容器中有指定的Bean的條件下 |
@ConditionalOnClass | 當類路徑下有指定的類的條件下 |
@ConditionalOnExpression | 基於SpEL表達式做爲判斷條件 |
@ConditionalOnJava | 基於JVM版本做爲判斷條件 |
@ConditionalOnJndi | 在JNDI存在的條件下查找指定的位 |
@ConditionalOnMissingBean | 當容器中沒有指定Bean的狀況下 |
@ConditionalOnMissingClass | 當類路徑下沒有指定的類的條件下 |
@ConditionalOnNotWebApplication | 當前項目不是Web項目的條件下 |
@ConditionalOnProperty | 指定的屬性是否有指定的值 |
@ConditionalOnResource | 類路徑下是否有指定的資源 |
@ConditionalOnSingleCandidate | 當指定的Bean在容器中只有一個,或者在有多個Bean的狀況下,用來指定首選的Bean |
@ConditionalOnWebApplication | 當前項目是Web項目的條件下 |
好比,註解@ConditionalOnProperty(prefix = "example.service",name= "enabled",havingValue = "true",matchIfMissing = false)
的意思是當配置文件中example.service.enabled=true
時,條件才成立。
當這些註解再也不知足咱們的需求以後,還能夠經過實現 Condition 接口,自定義條件判斷:
public class RedisExistsCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { StringRedisTemplate redisTemplate = null; try { redisTemplate = context.getBeanFactory().getBean(StringRedisTemplate.class); } catch (BeansException e) { // e.printStackTrace(); } if (redisTemplate == null){ return false; } return true; } } //使用示例 @Conditional(RedisExistsCondition.class)
本文鳴謝: