前言
??現在微服務火的一塌糊涂,但凡出來面個試,好像你不會微服務就跟你什么都不會一樣。但是像我們這種做外包的小公司,上微服務就不太現實,首先技術支撐不夠,其次開發速度無法滿足快速開發、快速交付的要求,各種服務治理、服務熔斷和降級、鏈路追蹤等等,簡直能把甲方的規劃進度拖死,而且根據我們的經驗來說,找我們的甲方70%的都是沒有自己的技術團隊,這樣交付后即使讓甲方自己組建技術團隊,對他們來說也是一筆更高的成本。
??所以在這種背景下,我們選擇依托springboot的starter特性,把業務邏輯進行封裝,使不同的甲方,重復的項目需求能夠達到即插即拔,達到快速構建、快速開發,節省成本的目的。由此定義名字為spring-lego:像樂高積木一樣,隨意組合,快速成型。 (此項目持續集成中),以下便是在進行starter封裝過程中遇到的問題以及注意的細節。
技術棧
名稱 | 版本 |
---|---|
springboot | 2.2.0.RELEASE |
mybatisPlus | 3.2.0 |
redis | 5.0.6 |
mysql | 5.7 |
swagger | 2.7.0 |
github:https://github.com/qismyq/spring-lego
starter結構
- lego-frame-spring-boot-starter(基礎starter,其它所有starter包括主項目直接依賴此starter即可,主工程無需在引入springboot-starter依賴。以下簡稱frame-starter)
- lego-frame-spring-boot-starter
- lego-frame-spring-boot-autoconfigure
- lego-user-spring-boot-starter(用戶業務starter,以下簡稱user-starter)
- lego-user-spring-boot-starter
- lego-user-spring-boot-autoconfigure
- lego-shiro-spring-boot-starter(權限控制starter,需依賴user-starter,以下簡稱shiro-starter)
- lego-shiro-spring-boot-starter
- lego-shiro-spring-boot-autoconfigure
- .....
1. yml配置無法覆蓋使用的問題
問題描述
??本身是希望如果其他starter或者主工程依賴frame-stater時,frame-starter中的yml配置文件可以直接生效或者覆蓋生效,即如果主工程中不配置yml時,即使用frame-starter中的yml配置,如果主工程中配置了相同的配置,則進行覆蓋操作,以主工程中的配置為生效配置。但是當我再主工程中只是配置了spring.server.port,而其他任何都沒有配置時,發現項目啟動失敗,失敗原因為無法創建datasource這個bean 。
問題結論
??此問題只能使用主工程中的yml文件,發現starter在install的時候好像相關的yml文件并沒有被打包進去。即像mybatis或者jdbc一樣,需要在主工程中進行配置。
2. starter中的autoConfig問題
問題描述
??網上很多資料對封裝starter都是一個簡單的示例,大多都是什么xxxServiceAutoConfiguration,然后加一堆@Configuration、@ConditionalOnxxx之類的注解,然后在spring.factories中配置該自動配置類。
??而我遇到的問題是在frame-starter中配置了不止一個autoConfig類,如:DuridConfig、MabatisPlusConfig、RedisConfig等。因為在單體應用中只是在這些配置類上增加@Configuration注解即可,啟動時會自動生效,但是當我封裝好starter后,偶然間發現這些配置類并沒有再主工程的啟動中生效。
解決方案
??經多次測試,關鍵問題有三點:
??其實三點中最關鍵的點是第三點,如果能保證主工程能夠掃描到frame-starter中的配置類,則不需要關注第一和第二點。如果無法保證第三點,則建議使用第一點,保證在spring.factories文件中顯式的指定要自動配置的類。至于第二點,如果在frame-autuconfiguration的pom中引入此插件,則在install時會找主啟動類中的main方法,否則會提示"Unable to find main class",
- 在frame-autoconfiguration的spring-factories文件中是否有明確指明自動配置類。
- 在frame-autoconfiguration的pom文件中是否有配置spring-boot-maven-plugin插件,此插件的作用是用于springboot依靠java -jar啟動時可以找到主啟動類,如果沒有使用此插件,則打包后使用java -jar命令則會報錯:“frame-spring-boot-autoconfigure-0.0.1-SNAPSHOT.jar中沒有主清單屬性”。
- 引入方的主啟動類是否可以掃描到frame-autoconfiguration中的自動配置類。
而當你第一點和第二點都存在的情況下,會報錯“java.lang.IllegalStateException: Unable to read meta-data for class net.yunqihui.starter.config.DruidConfig”(此問題會在下邊詳細描述)。所以建議在封裝starter時一定不要在pom中引用spring-boot-maven-plugin插件以及不要將主啟動類打包進去。
關于第一點:在spring.factories中顯示指定這些配置類的目錄,注意“,”后不要有空格之類的符合,不然會出現無法找到此bean的錯誤。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
net.yunqihui.starter.config.DruidConfig,\
net.yunqihui.starter.config.web.SwaggerConfig,\
net.yunqihui.starter.config.MessageConvertConfig,\
net.yunqihui.starter.config.MybatisPlusConfig,\
net.yunqihui.starter.config.RedisConfig,\
net.yunqihui.starter.config.SchedulerConfig
關于第三點:在主工程的主啟動類上增加compantScan注解,注解的值需要添加stater中的configBean的路徑,以及主工程中的掃描包路徑(注意,此路徑不能丟)
@SpringBootApplication
@MapperScan("net.yunqihui.**.mapper")
@EnableCaching
@ComponentScan({"net.yunqihui","net.yunqihui.config"})
public class SpringBoot08StarterTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot08StarterTestApplication.class, args);
}
}
問題結論
??一開始想的簡單了,認為那些xxxServiceAutoConfiguration就相當于starter的入口而已,只要在spring.factories中配置了此配置類,即可達到starter中的其它的@configuration自動配置。其實并不是,根據springboot的自動配置原理,當開啟了EnableAutoConfiguration(記住這個注解,這個是后續的關鍵)后,會掃描主啟動類所在的包以及子包,但是通常的starter屬于第三方包,包名并不一定和主工程的包名一致,自然無法像單體工程一樣掃描到stater中的Bean。而boot是怎么掃描到上邊舉例的那些xxxServiceAutoConfiguration的呢?是通過spring.factories文件的內容進行查找class然后實現自動配置的。
還記得上邊說的關鍵的EnabaleAutoConfiguration嗎?他的注解上有個元注解@Import,此注解的AutoConfigurationImportSeclector.class中調用方法getCandidateConfigurations中會拿到spring.factories中配置的配置類,所以這便是上述第一點問題。
configurations
??至于上述第三點,原理便是@ComponentScan,它會讓主啟動類掃描當前所在包以及子包,所以如果你的主啟動類是在包net.yunqihui下,同樣各種starter的Bean也是再net.yunqihui包或者子包下,則無需指定spring.factories也可以將配置類加載進容器中,甚至@Controller等各種component組件均可(==關鍵點不在于starter是否是第三方包,而在于掃描路徑==)。
注意: 如果第二點和第三點同時存在,你會發現請求starter中的controller時會有404狀態的錯誤,這說明即使包路徑相同,好像主工程并沒有將starter中的component注入進容器,在我看來,好像并不是沒有被注入,而是在啟動主工程的主啟動類時,啟動的是兩個容器(主工程容器和starter工程容器),而starter中的component被注冊到了后者中。所以,建議任何starter中都不要使用spring-boot-maven-plugin插件,并且在install時把主啟動類排除在外。
3. java.lang.IllegalStateException: Unable to read meta-data for class xxx.xxx.xxx問題
問題描述
封裝starter時,如果你在spring.factories中顯示指定了某些自動配置類,并且在pom文件中使用了spring-boot-maven-plugin插件,且install時將主啟動類打包進去,則在主工程(也即引入方)啟動時,會報如下錯誤:
java.lang.IllegalStateException: Unable to read meta-data for class net.yunqihui.starter.config.DruidConfig
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationMetadata(AutoConfigurationSorter.java:233) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getOrder(AutoConfigurationSorter.java:204) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.access$000(AutoConfigurationSorter.java:150) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
at org.springframework.boot.autoconfigure.AutoConfigurationSorter.lambda$getInPriorityOrder$0(AutoConfigurationSorter.java:62) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$$Lambda$178/100708535.compare(Unknown Source) ~[na:na]
at java.util.TimSort.countRunAndMakeAscending(Unknown Source) ~[na:1.8.0_25]
解決方案
關于此錯誤,有很多資料會有以下幾點說法:
- spring.factories中有一些肉眼不可見的錯誤,比如“,”后有不小心多出一個空格的
- starter間的傳遞依賴,導致無法找到此class,比如主工程依賴frame-starter,social-starter也依賴frame-starter,但是同時主工程也依賴了social-starter,這個時候會出現重復依賴的問題,那么則需要將social-stater的依賴排除掉frame-starter,如下:
social-starter的pom依賴主工程的pom依賴
但是對于以上解決方案,我發現我的沒有任何改變(對不起,我上邊只是多提供一些解決手段,不是故意啰嗦),而我這邊出現此問題的原因好像就是上邊所說的,貌似啟動了兩個容器,主容器之所以找不到這個class,是因為這個class被注入到了starter中的容器中。要解決此問題,還是要干掉spring-boot-maven-plugin插件和starter的啟動類。
4. 到底是分兩個模塊還是一個模塊
問題描述
??一開始我所了解的自定義一個starter,是需要一個auto configure模塊和一個starter模塊的,即前者負責具體的處理,而starter模塊只是一個空殼,只是對外依賴的一個門戶而已。但是后來當我先發布到中央倉庫上時,發現需要將兩個模塊都進行打包發布,我就有點開始懷疑了,這樣做的意義是什么,那么有人使用時直接依賴autoconfigure,而不使用starter模塊不是浪費了嗎?
問題結果
??我參考了mybatisplus的starter以及其他第三方的starter,發現他們都是直接只搞一個starter,autoconfigure直接就在這里邊了。我不死心,因為看到spring-boot-starter-redis等這些官方starter都是有對外暴露的starter,還區分實際執行的autoconfigure,總覺得這不符合規范,一直到在spring.io上看到了官方描述
既然官方都這樣說了,那也沒什么糾結的了,直接更改為一個module。
5. starter中的schema.sql無法執行
問題描述
??springboot中提供應用程序啟動時執行sql腳本,通常為在yml中添加spring.datasource.initialization-mode: always(2.x版本),這樣在應用程序啟動時找classpath:schema.sql(我一開始的顯式配置)文件。我的本意是在每個業務starter中配置相應的schema.sql,用來創建相應業務表結構。但是測試時發現,并沒有執行自定義starter中的相關sql腳本。
解決方案
??其實這個問題完全跟我畫蛇添足有關,springboot的DataSourceInitializer.class中的createSchema()雖然會傳入spring.datasource.schema的配置,且默認fallback參數為schema。,但是getScripts()中會先判斷resources是否為null,不為null,則執行顯示配置的文件路徑,當發現配置文件中沒有相關配置時,才會查找classpath:下的schema.sql和schema-all.sql文件。
如果不是我畫蛇添足的顯示指定加載腳本路徑,其實并不會出現無法執行starter中的腳本文件的問題。
由此,此問題的根本原因是classpath:和classpath:的區別(前者為主項目的class路徑,后者為包含第三方jar文件中進行查找)的問題。所以即使我顯示配置schema為classpath*:schema.sql也是可以的。