優雅解決 SpringBoot 工程中多環境下 application.properties 的維護問題
- 2019 年 12 月 22 日
- 筆記
個人微信號:geekoftaste, 期待與大家一起探討!
背景
我們知道 SpringBoot 有一個全局的配置文件 application.properties, 可以把工程里用到的佔位符,第三方庫的配置項如 dubbo 端口,工程的 db 配置等統一放在這個配置文件里,方便對工程里所有配置項的統一管理。我們知道,在企業開發中,我們一般會先在測試環境中開發,在預發環境環境,最後上生產環境部署,也就意味着一個工程需要分別部署在測試,預發,生產環境上,而這三種環境的一些配置項(如測試環境和生產環境的 db 配置)很多時候都是不一樣的,所以我們通常需要為每一個環境準備一份 application.properties, 接下來就引申出一個問題
如何維護多環境下的 application.properties 文件
方法一:在工程里維護多個環境的 application.properties ,部署的時候通過 spring.profiles.active 來指定工程應用哪個環境的 application.properties 文件,比如預發配置文件我們用 application-pre.properties, 線上配置文件我們用 application-prod.properties,當想在預發部署工程時,我們在部署腳本里用 java -jar xxxxx.jar –spring.profiles.active=pre 這樣的方式來指定工程啟動使用預發的 application-pre.properties 配置文件
以上這種方式雖然可以滿足要求,但有一個比較棘手的問題:在部署腳本里需要先根據 ip 等來判定當前環境是預發還是線上
if [ "$flag" == "預發" ]; then java -jar xxxxx.jar --spring.profiles.active=pre else java -jar xxxxx.jar --spring.profiles.active=prod
如上所示,部署腳本需要根據一個標誌來判斷是預發還是線上,以便指定相應的 spring.profiles.active,這個標誌的維護就是一個成本 ,每個環境的部署機器都要能正確設置這個 flag 的值,維護的成本很大
方法二:也就是我們工程當前採用的方式除了用 spring.profiles.active 來指定到底用工程中哪種環境的 application.properties,我們還可以用
java -jar xxxxx.jar --spring.config.location=/opt/conf/application.properties
這種指定配置文件位置的方式來使用指定的 application.properties 文件
使用這種方式就解決了方法一的問題,只要在工程里維護多個環境下的 application.properties 文件(如下)

當 gradle build 通過之後,在每個環境的機器部署前都會把工程里相應環境對應的 application.properties 統一 copy 到機器的固定目錄下,如 /opt/conf下, 然後在啟動的時候,通過在啟動命令里指定 spring.config.location=/opt/conf/application.properties 的方式來指定 jar 包使用此環境下的 application.properties 文件即可
問題初現:多環境下的 application.properties 如何維護
在上圖我們可以看到,由於我們有多個預發及線上環境,不得不為這些環境分別指定一個 application.properties,這樣導致的後果就是如果我需要在一個 application.properties 文件里添加一個配置,不得不在其他環境下的 application.properties 文件里也 手動 添加此配置,這樣的工作不僅煩瑣,而且很容易出錯, 之前就有發生過同事只在預發的 application.properties 加配置而忘記在線上加導致的線上部署失敗的問題
如何解決
實際上預發和線上的配置大部分都是一樣的,只有少部分是不一樣的,所以我們想是否能將大部分一樣的配置都統一放到一個文件(姑且叫 application-common.properties)里維護,這樣如果要配置一個屬性,只需要統一在這一個文件里配置即可,極大地降低了維護成本
那如何解決不同環境下某些配置不同的問題呢,比如在預發和線上我們對 MQ 的 topic 的命名有一個統一的規範,預發我們統一叫 topic-pre-xxx, 線上我們統一叫 topic-prod-xxx。
針對這種不同環境下配置不同的問題,我們可以單獨為預發和線上建一個文件,比如預發叫 application-pre.properties, 線上叫 application-prod.properties,把各個環境獨有的配置都分別放在這兩個文件里即可
好了,現在我們有了三個文件,公用配置文件:application-common.properties, 以及兩個環境配置文件:application-pre.properties, application-prod.properties,那怎麼生成如下最終各個環境的 application.properties 文件呢

很明顯應該把公用配置文件與各個環境的配置文件合併

合併工作的思路很簡單,我們以生成預發環境的 application.properties為例

具體步驟如下:
- 遍歷 application-common.properties 文件中的每一行,然後取出每一行的 key,value(以等號分割),將其存儲到 map中
- 遍歷 application-pre.properties 的每一行,然後取出每一行的 key,value(以等號分割),取出的同時拿 key 到上一步的 map去查找, 如果存在則覆蓋,如果不存在則在上一步的 map 中新增 key, value
- 遍歷步驟 2 最終生成的 map,將每個鍵值對以 key=value 的形式寫入 pre 中的 applicaton.properties 文件
還有一個問題,這個合併工作寫在哪裡呢,答案是 gradle 的 task 里,我們的工程是基於 gradle 構建的,一般我們是通過 gradle build 來編譯打包工程的,我們可以在 gradle build 打包之後再執行這個 task ,假設這個 task 名為 regeneratePropertyFile ,則可寫成如下形式
build.finalize(regeneratePropertyFile)
這樣在 build 構建完之後就會自動執行合併文件的工作了。最後我們來看看最終的合併生成各個環境 application.properties 的 task
task regeneratePropertyFile(type:Exec){ ["pre", "prod"].each { env -> def envMap = [:] // 取出公共文件的鍵值對 def destFileName = "src/main/resources/META-INF/spring/properties/application-common.properties" def destFile = file(destFileName) String content = destFile.text def commonLines = destFile.readLines() for (line in commonLines) { Integer equalSignLoc = line.indexOf("=") if (equalSignLoc == -1) { continue } String key = line.substring(0, equalSignLoc) String value = line.substring(equalSignLoc + 1) envMap.put(key, value) } // 取出環境配置文件 application.properties 的鍵值對,寫入envMap中 File envFile = file("src/main/resources/META-INF/spring/properties/application-${env}.properties") def envLines = envFile.readLines() for (line in envLines) { Integer equalSignLoc = line.indexOf("=") if (equalSignLoc == -1) { continue } String key = line.substring(0, equalSignLoc) String value = line.substring(equalSignLoc + 1) envMap.put(key, value) } content = "" envMap.each { key, val -> content = "${content}n" } // 寫入最終環境的 application.properties 文件 destFile = file("${rootProject.projectDir}/bb_conf/${env}/application.properties") destFile.text = content } }
搞定!媽媽再也不用擔心我在多環境下維護多個 application.properties 的噩夢了


