開發一個項目,參數是必不可少的,規模越大參數越多。在不同的測試環境中部署,或者是依賴項目的信息發生了變化,你有沒有想跳樓的感覺?如果有,恭喜你,你至少已經不是在開發玩具系統了。 本文試圖列舉一些配置參數的方法,希望對你的項目有所幫助。 一、可用性模式-外部配置 引用自圖書《Java應用架構設計:模塊 ...
開發一個項目,參數是必不可少的,規模越大參數越多。在不同的測試環境中部署,或者是依賴項目的信息發生了變化,你有沒有想跳樓的感覺?如果有,恭喜你,你至少已經不是在開發玩具系統了。
本文試圖列舉一些配置參數的方法,希望對你的項目有所幫助。
一、可用性模式-外部配置
引用自圖書《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自動同步到應用系統後,項目中讀取參數值時,就能載入到最新的參數值。