項目參數外部配置化

  • 2019 年 10 月 8 日
  • 筆記

開發一個項目,參數是必不可少的,規模越大參數越多。在不同的測試環境中部署,或者是依賴項目的信息發生了變化,你有沒有想跳樓的感覺?如果有,恭喜你,你至少已經不是在開發玩具系統了。

本文試圖列舉一些配置參數的方法,希望對你的項目有所幫助。

一、可用性模式-外部配置

引用自圖書《Java應用架構設計:模塊化模式與OSGi》10.2

「模塊應該可以在外部進行配置」

當把模塊部署到運行時環境中時,在使用它之前通常要進行初始化。例如,為了讓模塊能夠訪問數據庫中的數據,要用必要的用戶ID和密碼來初始化模塊。但是,我們也希望避免將配置信息與模塊緊密耦合。如果這樣做,將會使模塊與單一的上下文環境耦合,這樣就限制了模塊在其他可選的上下文中進行重用。

外部配置使得模塊可以跨環境上下文配置。下圖展現了外部配置,在這裡Client類使用一個XML配置文件配置client.jar模塊。要注意的是,用來初始化client.jar的配置信息與表示模塊行為的Client類分開了。能夠配置模塊到環境上下文中會增強跨環境重用模塊的能力。

配置文件的位置,有三種處理方式:

1、配置信息包含在模塊中,優勢是在模塊的默認上下文中很易於使用,不足在於在其他的上下文中不能正常工作。

2、配置信息不在模塊中,但是在初始化的時候由外部提供給模塊。優勢是能跨環境重用,不足是每個環境都要配置所有參數。

3、更靈活的方案是在模塊中提供默認配置文件,但是允許模塊外部提供替代的配置文件。下圖是圖書中的一個例子。

這三種方案中,最後一種看起來最有誘惑,能夠實現比較靈活的配置方式。後續我們用這種方案進行設計。

二、默認+替代的配置方案

考慮一個企業開發中一個相對簡單的項目,同時提供WEB界面和API接口。為了方便其他系統調用API,同時提供一個 client jar供調用。

1、系統設計

各個模塊的簡單介紹:

  • base-util.jar : 通用的基礎包,實現基本工具類。我們自定義的讀取配置文件工具類(PropsUtil)就在這個包中。
  • business-core.jar : 業務系統的基礎包,如model定義等
  • business-web.war : 業務系統的WEB項目,實現基本的業務邏輯,並提供API實現。
  • business-client.jar : 業務系統的client包,供其它系統調用。

圖中的箭頭代表依賴關係。題外話,在設計module時,尤其要注意的是不能出現循環依賴。

2、配置參數的約定

本文不考慮數據庫連接信息等特殊需求的配置,重點放在能夠通過配置工具類PropsUtil讀取的那一類參數。如線程池的大小、client調用api的是服務器地址和uri等。

  • 在每個module中都放置一個配置文件conf.properties,將配置信息寫在這個配置文件中。
  • 相同名稱的參數加載,module中的參數會覆蓋所依賴module中的參數。
  • 讀取配置參數,必須使用PropsUtil.getString()/getInt()/getBoolean()的函數來讀取。

3、PropsUtil的實現

工具類的實現,核心是需要解決兩個問題:

  • 如何將各個jar中的conf.properties都加載
  • 如何處理各個conf.properties的加載順序

使用SpringFrameworks的ResourcePatternResolver,可以將多個jar包、war包中的特定文件讀取成Resource對象,然後加載到apache的commons configuration Configuration中。下面用代碼解釋一下實現。

3.1 加載Resource List

String filePattern = "classpath*:conf.properties";

// 根據文件名讀取Resource列表,並做必要的排序

public static List<Resource> getResources(String filePattern) {

List<Resource> resultResources = new ArrayList<Resource>();

try {

ResourcePatternResolver resolver =

new PathMatchingResourcePatternResolver();

Resource[] resources = (Resource[]) resolver.getResources(filePattern);

List<Resource> jarResources = new ArrayList<Resource>();

List<Resource> webResources = new ArrayList<Resource>();

// 將各個jar包中發現的conf.properties文件按順序放到jarResources

// 將war包中發現的conf.properties文件按順序放到webResources

// 這部分代碼自行腦補

// 最終合併到 resultResources

for (Resource oneResource : jarResources) {

resultResources.add(oneResource);

}

for (Resource oneResource : webResources) {

resultResources.add(oneResource);

}

} catch (IOException e1) {

logger.error("getResources", e1);

}

return resultResources;

}

3.2 將內容加載到Configuration

private volatile static Configuration[] configs = null;

private static void initConfigArray() {

configs = new Configuration[] {};

try {

int index = 0;

List<Resource> resourceList = ResourceFileUtil.getResources(propFile);

for (Resource resource : resourceList) {

InputStream inputStream = resource.getInputStream();

if (inputStream != null) {

FileConfiguration oneFileConfig = new PropertiesConfiguration();

oneFileConfig.setEncoding(StringPool.UTF8);

oneFileConfig.load(inputStream);

index++;

configs = ArrayUtil.append(configs, oneFileConfig);

}

inputStream.close();

}

} catch (IOException e1) {

}

}

3.3 讀取配置參數

public static String getString(String key, String defaultValue) {

String stringValue = null;

for (Configuration oneConfig : configs) {

if (oneConfig.containsKey(key)) {

String tempValue = oneConfig.getString(key);

if (Validator.isNotNull(tempValue)) {

stringValue = tempValue;

}

}

}

if (Validator.isNull(stringValue) && Validator.isNotNull(defaultValue)) {

stringValue = defaultValue;

} else if (stringValue == null) {

stringValue = StringPool.BLANK;

}

return stringValue;

}

這兒只寫了讀取字符串類型的配置,如果是其他數據格式,自行從String做必要的轉換即可。

至此,在需要讀取配置參數的時候,只需要調用 PropsUtil.getString(),就可以取到相應的參數值。這種方法已經實現了「默認+替代」的方案,在基礎模塊的conf.properties中提供缺省設置,在依賴模塊的conf.properties中使用新的參數值替換。

當不同的WEB項目調用同一個基礎模塊時,因參數不同,只需要在web的conf.properties中重新設置新的參數值即可。

三、利用Maven Profile解決多環境部署問題

conf.properties是項目的源碼。如果一套系統需要在多個環境中進行部署,並且在不同的環境中參數值還不同。如果直接修改conf.properties文件,那會給打包部署帶來繁瑣的手工工作量。

如果項目使用Maven進行管理,則可以方便的利用maven profile對參數進行管理。

1、修改conf.properties中的參數值

以下用兩個參數為例,

# 數據處理線程數

disrupter.handler.threads=2

# 向門戶推送消息的嘗試次數

notify.portal.try.times=5

修改後的參數值為

# 數據處理線程數

disrupter.handler.threads=${param.disrupter.handler.threads}

# 向門戶推送消息的嘗試次數

notify.portal.try.times=${param.notify.portal.try.times}

注意,參數值中的變量名稱,不能跟前面的參數名相同,否則maven會拋異常。最簡單的處理方式,就是在變量名前面加上param.

2、pom.xml中增加profiles

假設系統的部署有四套環,分別是

  • dev: 開發環境
  • testa: 第一輪測試
  • testb: 第二輪測試
  • product: 生產環境

那麼,修改pom.xml文件,相關部分代碼為:

<profiles>

<profile>

<id>dev</id>

<activation>

<activeByDefault>true</activeByDefault>

</activation>

<properties>

<param.disrupter.handler.threads>1</param.disrupter.handler.threads>

<param.notify.portal.try.times>1</param.notify.portal.try.times>

</properties>

<build>

<filters>

<filter>src/main/resources/conf.properties</filter>

</filters>

</build>

</profile>

<profile>

<id>testa</id>

<activation>

<activeByDefault>false</activeByDefault>

</activation>

<properties>

<param.disrupter.handler.threads>1</param.disrupter.handler.threads>

<param.notify.portal.try.times>2</param.notify.portal.try.times>

</properties>

<build>

<filters>

<filter>src/main/resources/conf.properties</filter>

</filters>

</build>

</profile>

<profile>

<id>testb</id>

<activation>

<activeByDefault>false</activeByDefault>

</activation>

<properties>

<param.disrupter.handler.threads>1</param.disrupter.handler.threads>

<param.notify.portal.try.times>2</param.notify.portal.try.times>

</properties>

<build>

<filters>

<filter>src/main/resources/conf.properties</filter>

</filters>

</build>

</profile>

<profile>

<id>product</id>

<activation>

<activeByDefault>false</activeByDefault>

</activation>

<properties>

<param.disrupter.handler.threads>2</param.disrupter.handler.threads>

<param.notify.portal.try.times>5</param.notify.portal.try.times>

</properties>

<build>

<filters>

<filter>src/main/resources/conf.properties</filter>

</filters>

</build>

</profile>

</profiles>

其中,activeByDefault表示是否為缺省profile。設置完參數後,就是在不同的環境中應用不同profile的方法問題。

3、Maven啟動WEB項目時應用profile

這種方式,需要在pom.xml中增加tomcat7-maven-plugin這個plugin。

如果是在命令行使用Maven啟動Tomcat,可使用如下命令:

mvn tomcat7:run -P testa

其中,-P testa , 代表的是使用testa這個profile。

如果使用Eclipse中的Run進行啟用,方法類似,配置界面為:

使用maven進行項目打包,也是相同的方法, 在profile處選擇testa即可。

4、在Eclipse中使用Server啟動

在Eclipse中添加Server Runtime Environments後,將項目部署到Server中。在項目上右鍵,選擇「屬性」,在彈出的窗口中選擇「Maven」,即可輸入相應額Profile。

四、實現參數實時更新

之前的實現,已經很好的解決了多環境部署的問題。考慮到生產環境的特殊性,不能隨便重啟應用。如果某一個關鍵參數需要修改,按照之前的方案,需要重新打包並部署到生產環境,應用將會重新啟動。

如果項目是關鍵業務,客戶要求不能停機,必須實現參數的實時修改,怎麼辦?多點環境灰度發佈,是一種解決方案;osgi模塊化開發部署應該也是一種解決方案。只是這兩種方案,很難在已有的項目中實現,我們還是考慮簡單一點的處理方式。

1、提供參數管理功能(DB)

在系統中實現一個參數設置功能,由管理員將最新的參數值保存在數據庫中。系統首先讀取數據庫中的參數值,如果為空再從properties文件中讀取。當需要調整系統參數時,管理員進入管理界面修改並保存即可。

可以看出,系統要實現這個定製功能,需要完成:參數數據表、參數封裝Service和維護界面。這種方案,比較適合產品化銷售的獨立運行系統,能夠適應不同客戶的需求。

2、利用disconf實現

如果一個運營性系統中有多個Project,則每個Project都需要開發管理功能,比較繁瑣。Disconf就是針對這種情況的解決方案,在此不仔細介紹它,請自行前往網站學習https://github.com/knightliao/disconf 。

Disconf的應用有兩種方案:註解式分佈式配置使用方式和XML配置式分佈式配置方式。使用註解式,需要為配置信息定義一個專門的Java類,增減參數都需要修改這個Java類,不太適合於我們之前的配置解決方案。所以,建議採用「XML配置式分佈式配置方式」。

2.1 Disconf分發配置文件

為了簡化實現,項目中在原有的conf.properties文件之外,設計一個專門用於disconf更新的文件conf-disconf.properties。項目結構變為

2.2 PropsUtil的修改

這是在前面PropsUtil的基礎之上進行修改,不詳述,概要介紹一下需要修改的內容。

1、增加一個Resource

讀取資源文件的定義為classpath*:conf-disconf.properties。這個配置文件需要記錄更新時間。

2、增加一個Configuration,用於加載新配置文件的內容。這個配置需要檢查資源文件的更新時間,如果發現時間有變化,則重新加載內容。

3、讀取配置參數時,首先讀取conf-disconf.properties中的內容,如果沒有再加載原順序加載的配置信息。

這樣,當disconf Server中的配置信息發生變化,由disconf-client自動同步到應用系統後,項目中讀取參數值時,就能加載到最新的參數值。