1.文章前言
我們都知道可以使用 SpringBoot 快速的開發基於 Spring 框架的項目。由於圍繞 SpringBoot 存在很多開箱即用的 Starter 依賴,使得我們在開發業務代碼時能夠非常方便的、不需要過多關注框架的配置,而只需要關注業務即可。
例如我想要在 SpringBoot 項目中集成 Redis,那麼我只需要加入 spring-data-redis-starter 的依賴,並簡單配置一下連接信息以及 Jedis 連接池配置就可以。這爲我們省去了之前很多的配置操作。甚至有些功能的開啓只需要在啓動類或配置類上增加一個註解即可完成。
那麼如果我們想要自己實現自己的 Starter 需要做些什麼呢?下面就開始介紹如何實現自己的 spring-boot-starter-xxx。
2.原理淺談
從總體上來看,無非就是將Jar包作爲項目的依賴引入工程。而現在之所以增加了難度,是因爲我們引入的是Spring Boot Starter,所以我們需要去了解Spring Boot對Spring Boot Starter的Jar包是如何加載的?下面我簡單說一下。
SpringBoot 在啓動時會去依賴的 starter 包中尋找 /META-INF/spring.factories 文件,然後根據文件中配置的路徑去掃描項目所依賴的 Jar 包,這類似於 Java 的 SPI 機制。
細節上可以使用@Conditional 系列註解實現更加精確的配置加載Bean的條件。
JavaSPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。
3.實現自動配置
接下來我會實現一個普通的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>
工程命名規範
官方命名格式爲: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.項目到這裏就差不多了,不過作爲依賴,最好還是再做一下收尾工作。
-
刪除自動生成的啓動類SpringBootStarterHelloApplication。 -
刪除resources下的除META-INF目錄之外的所有文件目錄。 -
刪除spring-boot-starter-test依賴並且刪除test目錄。
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 你好!
4.元數據的配置
到目前爲止,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具體使用,不過我建議你可以先跳過這部分枯燥的內容。
4.1 Group屬性
“groups”中包含的JSON對象可以包含下表中顯示的屬性:
名稱 | 類型 | 用途 |
---|---|---|
name | String | “groups”的全名。這個屬性是強制性的 |
type | String | group數據類型的類名。例如,如果group是基於一個被@ConfigurationProperties註解的類,該屬性將包含該類的全限定名。如果基於一個@Bean方法,它將是該方法的返回類型。如果該類型未知,則該屬性將被忽略 |
description | String | 一個簡短的group描述,用於展示給用戶。如果沒有可用描述,該屬性將被忽略。推薦使用一個簡短的段落描述,第一行提供一個簡潔的總結,最後一行以句號結尾 |
sourceType | String | 貢獻該組的來源類名。例如,如果組基於一個被@ConfigurationProperties註解的@Bean方法,該屬性將包含@Configuration類的全限定名,該類包含此方法。如果來源類型未知,則該屬性將被忽略 |
sourceMethod | String | 貢獻該組的方法的全名(包含括號及參數類型)。例如,被@ConfigurationProperties註解的@Bean方法名。如果源方法未知,該屬性將被忽略 |
4.2 Property屬性
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 | 替換這個廢棄屬性的屬性的全名。如果該屬性沒有替換,則可以省略該屬性。 |
4.3 hints屬性
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 | 提供程序支持的任何其他參數(詳細信息請參閱提供程序的文檔)。 |
5.spring-boot-configuration-processor
配置上述數據是挺麻煩的,如果可以提供一種自動生成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。
6.@Conditional 註解及作用
之前提到了在細節上可以使用@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)
作者:薛勤,互聯網從業者,編程愛好者。
本文首發自公衆號:代碼藝術(ID:onblog)未經許可,不可轉載
本文分享自微信公衆號 - 代碼藝術(onblog)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。