深入Spring Boot:編寫相容Spring Boot1和Spring Boot2的Starter
前言
Spring Boot 2正式釋出已經有段時間,應用升級之前,starter先要升級,那麼如何支援Spring Boot 2?
為什麼選擇starter同時相容spring boot 1和spring boot 2
從使用者角度來看
如果不在一個starter裡相容,比如用版本號來區分,spring boot 1的使用者使用
1.*
,spring boot 2使用者使用2.*
,這樣使用者升級會有很大困擾。另外,我們的starter是以日期為版本號的,如果再分化,則就會出現
2018-06-stable-boot1
,2018-06-stable-boot2
,這樣子很醜陋。從開發者角度來看
要同時維護兩個分支,修改程式碼時要合到兩個分支上,發版本時要同時兩個。如果有統一的bom檔案,也需要維護兩份。工作量翻倍,而且很容易出錯。
因此,我們決定在同一個程式碼分支裡,同時支援spring boot 1/2。減少開發維護成本,減少使用者使用困擾。
編寫相容的starter的難點
spring boot starter的程式碼入口都是在各種@Configuration
類裡,這為我們編寫相容starter提供了條件。
但還是有一些難點:
- 某些類不相容,比如在spring boot 2裡刪除掉了
- 程式碼模組,maven依賴怎樣組織
- 怎樣保證starter在spring boot 1/2裡都能正常工作
通過ASM分析現有的starter裡不相容的類
springboot-classchecker可以從jar包裡掃描出哪些類在spring boot 2裡不存在的。
工作原理:springboot-classchecker自身在pom.xml裡依賴的是spring boot 2,掃描jar包裡通過ASM分析到所有的String,提取出類名之後,再嘗試在ClassLoader里加載,如果載入不到,則說明這個類在spring boot 2裡不存在。
例如掃描demo-springboot1-starter.jar
:
mvn clean package
java -jar target/classchecker-0.0 .1-SNAPSHOT.jar demo-springboot1-starter.jar
結果是:
path: demo-springboot1-starter.jar
org.springframework.boot.actuate.autoconfigure.ConditionalOnEnabledHealthIndicator
org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration
那麼這些類在spring boot 2在哪裡了?
實際上是改了package:
org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator
org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration
通過掃描20多個starter jar包,發現不相容的類有:
- org.springframework.boot.env.PropertySourcesLoader
- org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder
- org.springframework.boot.bind.RelaxedDataBinder
- Endpoint/HealthIndicator 相關的類
可以總結:
- spring boot核心的類,autoconfigure相關的沒有改動
- 大部分修改的是Endpoint/HealthIndicator 相關的類
spring-boot-utils相容工具類
spring-boot-utils提供相容工具類,同時支援spring boot 1/2。
BinderUtils
在spring boot 1裡,注入環境變數有時需要用到RelaxedDataBinder
:
MyProperties myProperties = new MyProperties();
MutablePropertySources propertySources = environment.getPropertySources();
new RelaxedDataBinder(myProperties, "spring.my").bind(new PropertySourcesPropertyValues(propertySources));
在spring boot 2裡,RelaxedDataBinder
刪除掉了,新的寫法是用Binder
:
Binder binder = Binder.get(environment);
MyProperties myProperties = binder.bind("spring.my", MyProperties.class).get();
通過BinderUtils,則可以同時支援spring boot1/2:
MyProperties myProperties = BinderUtils.bind(environment, "spring.my", MyProperties.class);
@ConditionalOnSpringBoot1/@ConditionalOnSpringBoot2
spring boot starter的功能大部分都是通過@Configuration
組裝起來的。spring boot 1的Configuration類,不能在spring boot 2裡啟用。則可以通過@ConditionalOnSpringBoot1
,@ConditionalOnSpringBoot2
這兩個註解來分別支援。
其實原理很簡單,判斷spring boot 1/2裡各自有的存在的類就可以了。
@ConditionalOnClass(name = "org.springframework.boot.bind.RelaxedDataBinder")
public @interface ConditionalOnSpringBoot1 {
}
@ConditionalOnClass(name = "org.springframework.boot.context.properties.bind.Binder")
public @interface ConditionalOnSpringBoot2 {
}
Starter程式碼模組組織
下面以實際的一個starter來說明。
spring boot web應用的mappings資訊,可以在
/mappings
endpoint查詢到。但是這麼多endpoint,它們都提供了哪些url?endpoints-spring-boot-starter的功能是展示所有endpoints的url mappings資訊
endpoints-spring-boot-starter
裡需要給spring boot 1/2同時提供endpoint功能,程式碼模組如下:
endpoints-spring-boot-starter
|__ endpoints-spring-boot-autoconfigure1
|__ endpoints-spring-boot-autoconfigure2
- endpoints-spring-boot-autoconfigure1模組在pom.xml裡依賴的是spring boot 1相關的jar,並且都設定為
<optional>true</optional>
- endpoints-spring-boot-autoconfigure2的配置類似
- endpoints-spring-boot-starter依賴autoconfigure1 和 autoconfigure2
- 如果有公共的邏輯,可以增加一個commons模組
Endpoint相容
以 endpoints-spring-boot-autoconfigure1模組為例說明怎樣處理。
EndPointsEndPoint
類繼承自spring boot 1的AbstractMvcEndpoint
:@ConfigurationProperties("endpoints.endpoints") public class EndPointsEndPoint extends AbstractMvcEndpoint {
通過
@ManagementContextConfiguration
引入@ManagementContextConfiguration public class EndPointsEndPointManagementContextConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnEnabledEndpoint("endpoints") public EndPointsEndPoint EndPointsEndPoint() { EndPointsEndPoint endPointsEndPoint = new EndPointsEndPoint(); return endPointsEndPoint; } }
在
META-INF/resources/spring.factories
裡配置org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\ io.github.hengyunabc.endpoints.autoconfigure1.EndPointsEndPointManagementContextConfiguration
因為org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration
是隻在spring boot 1裡,在spring boot 2的應用裡不會載入它,所以autoconfigure1模組天然相容spring boot 2。
那麼類似的,autoconfigure2模組裡在META-INF/resources/spring.factories
配置的是
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=\
io.github.hengyunabc.endpoints.autoconfigure2.ManagementApplicationcontextHolderConfiguration
仔細對比,可以發現是spring boot 2下面修改了ManagementContextConfiguration
的包名,所以對於Endpoint天然是相容的,不同的模組自己編繹就可以了。
HealthIndicator的相容
類似Endpoint的處理,spring boot 1/2的程式碼分別放不同的autoconfigure模組裡,然後各自的@Configuration
類分別使用@ConditionalOnSpringBoot1/@ConditionalOnSpringBoot2
來判斷。
通過整合測試保證相容性
還是以endpoints-spring-boot-autoconfigure1模組為例。
這個模組是為spring boot 1準備的,則它的整合測試要配置為spring boot 2。
參考相關的程式碼:檢視
- 在
springboot2demo/pom.xml
裡依賴spring boot 2 - 在
verify.groovy
裡檢測應用是否啟動成功
總結
- 通過ASM分析現有的starter裡不相容的類
- 配置注入通過
BinderUtils
解決 - 各自的
@Configuration
類分別用@ConditionalOnSpringBoot1/@ConditionalOnSpringBoot2
來判斷 - 程式碼分模組:commons放公共邏輯, autoconfigure1/autoconfigure2 對應 spring boot 1/2的自動裝配,starter給應用依賴
- Endpoint的Configuration入口是ManagementContextConfiguration,因為spring boot 2裡修改了package,所以直接在
spring.factories
裡配置即可 - 通過整合測試保證相容性
- 如果某一天,不再需要支援spring boot 1了,則直接把autoconfigure1模組去掉即可