優雅解決 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為例

具體步驟如下:

  1. 遍歷 application-common.properties 文件中的每一行,然後取出每一行的 key,value(以等號分割),將其存儲到 map中
  2. 遍歷 application-pre.properties 的每一行,然後取出每一行的 key,value(以等號分割),取出的同時拿 key 到上一步的 map去查找, 如果存在則覆蓋,如果不存在則在上一步的 map 中新增 key, value
  3. 遍歷步驟 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 的噩夢了