引言
配置文件幾乎是每一個項目中不可或缺的文件類型之一,經常使用的配置文件類型有xml、properties等,配置文件的好處不言而喻,利用配置文件能夠靈活地設置軟件的運行參數,而且能夠在更改配置文件後在不重啓應用的狀況下即時生效。html
寫這篇文章的緣由是最近在一個項目中引入了我對配置文件的管理方式,我以爲有必要與你們分享,但願能拋磚引玉。java
1. 我所知道的配置文件管理方式
下面大概列出了幾類對配置文件的管理方式,請對號入座^_^git
1.1 配置文件是什麼?
除非應用不須要配置文件(例如Hello World),不然請無視,continue;github
1.2 數據庫方式
把配置文件保存在數據庫,看起來這種方式很不錯,配置不會丟失方便管理,其實問題很大;舉個簡單的例子,在A模塊須要調用B模塊的服務,訪問B模塊的URL配置在數據庫,首先須要在A中讀取B模塊的URL,而後再發送請求,問題緊隨而來,若是這個功能只有一個開發人員負責還好,假如多我的都要調試那就麻煩了,由於配置保存在數據庫(整個Team使用同一個數據庫測試),每一個開發人員的B模塊的訪問端口(Web容器的端口)又不相同或者應用的ContextPath不一樣,要想順利調試就要更改數據庫的B模塊URL值,這樣多個開發人員就發生了衝突;問題很嚴重!web
1.3 XML方式
對於使用SSH或者其餘的框架、插件的應用在src/main/resources下面確定有很多的xml配置文件,今天的主題是應用級的配置管理,因此暫且拋開框架必須的XML配置文件,先來看看下面的XML配置文件內容。算法
?spring
1sql 2數據庫 3apache 4 5 |
<!--?xml version="1.0" encoding="UTF-8"?--> < systemconfig > < param code = "SysName" name = "系統名稱" type = "String" value = "XXX後臺系統" > < param code = "SysVersion" name = "系統版本" type = "String" value = "1.0" > </ systemconfig > |
這種方式在以前很受歡迎,也是系統屬性的主要配置方式,不過使用起來總歸不太簡潔、靈活(不要和我爭論XML與Properties)。
1.4 屬性文件方式
下面是kft-activiti-demo中application.properties文件的一部分:
?
1 2 3 4 5 6 7 8 9 10 11 12 |
sql. type =h2 jdbc.driver=org.h2.Driver jdbc.url=jdbc:h2: file :~ /kft-activiti-demo ;AUTO_SERVER=TRUE jdbc.username=sa jdbc.password= system.version=${project.version} activiti.version=${activiti.version} export .diagram.path=${ export .diagram.path} diagram.http.url=${diagram.http.url} |
相比而言屬性文件方式比XML方式要簡潔一些,不用嚴格的XML標籤包裝便可設置屬性的名稱,對於多級配置能夠用點號(.)分割的方式。
2. 分析個人管理方式
我在幾個項目中所採用的是第4中方式,也就是屬性文件的方式來管理應用的配置,讀取屬性文件能夠利用Java的Properties對象,或者藉助Spring的properties模塊。
2.1 利用Maven資源過濾設置屬性值
簡單來講就是利用Maven的Resource Filter功能,pom.xml中的build配置以下:
?
1 2 3 4 5 6 7 8 |
< build > < resources > < resource > < directory >src/main/resources</ directory > < filtering >true</ filtering > </ resource > </ resources > </ build > |
這樣在編譯時src/main/resources目錄下面中文件()只要有${foo}佔位符就能夠自動替換成實際的值了,例如1.4節中屬性system.version使用的是一個佔位符而非實際的值,${project.version}表示pom.xml文件中的project標籤的version。
屬性export.diagram.path、diagram.http.url也是使用了佔位符的方式,很明顯這兩個屬性的值不是pom.xml文件能夠提供,因此若是要動態設置值能夠經過在pom.xml中添加的方式,或者把放在profile中,如此能夠根據不一樣的環境(開發、UAT、生產)動態設置不一樣的值。
固然也能夠在編譯時經過給JVM傳遞參數的方式,例如:
mvm clean compile -Dexport.diagram.path=/var/kft/diagrams
編譯完成後查看target/classes/application.properties文件的內容,屬性export.diagram.path的值被正確替換了:
export.diagram.path=/var/kft/diagrams
2.2 讀取配置文件
讀取配置文件能夠直接裏面Java的Properties讀取,下面的代碼簡單實現了讀取屬性集合:
?
1 2 3 4 5 |
Properties props = new Properties(); ResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource(location); InputStream is = resource.getInputStream(); propertiesPersister.load(props, new InputStreamReader(is, "UTF-8" )); |
若是在把讀取的屬性集合保存在一個靜態Map對象中就能夠在任何能夠執行Java代碼的地方獲取應用的屬性了,工具類PropertiesFileUtil簡單實現了屬性緩存功能:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class PropertyFileUtil { private static Properties properties; public void loadProperties(String location) { ResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource(location); InputStream is = resource.getInputStream(); PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); propertiesPersister.load(properties, new InputStreamReader(is, "UTF-8" )); } // 獲取屬性值 public static String get(String key) { String propertyValue = properties.getProperty(key); return propertyValue; } } |
先拋出一個問題:屬性文件中定義了屬性的值和平臺有關,團隊中的成員使用的平臺有Window、Linux、Mac,對於這樣的狀況目前只能修改application.properties文件,可是不能把更改提交到SCM上不然會影響其餘人的使用……目前沒有好辦法,稍後給出解決辦法。
2.2 多配置文件重載功能
2.1節中簡單的PropertyFileUtil工具只能作到讀取一個配置文件,這對於一些多餘一個子系統來講就不太能知足需求了。
對於一個項目拆分的多個子系統來講若是每一個子系統都配置一套屬性集合最後就會出現一個問題——配置重複,修改起來也會比較麻煩;解決的辦法很簡單——把公共的屬性抽取出來單獨保存在一個公共的屬性文件中,我喜歡命名爲:application.common.properties。
這個公共的屬性文件能夠用來保存一些數據庫鏈接、公共目錄、子模塊的URL等信息,如此一來子系統中的application.properties中只須要設置當前子系統須要的屬性便可,在讀取屬性文件時能夠依次讀取多個(先讀取application.common.properties,再讀取application.properties),這樣最終獲取的屬性集合就是兩個文件的並集。
在剛剛的PropertyFileUtil類中添加一個loadProperties方法,接收的參數是一個可變數組,循環讀取屬性文件。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** * 載入多個properties文件, 相同的屬性在最後載入的文件中的值將會覆蓋以前的載入. * 文件路徑使用Spring Resource格式, 文件編碼使用UTF-8. * * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer */ public static Properties loadProperties(String... resourcesPaths) throws IOException { Properties props = new Properties(); for (String location : resourcesPaths) { Resource resource = resourceLoader.getResource(location); InputStream is = resource.getInputStream(); propertiesPersister.load(props, new InputStreamReader(is, DEFAULT_ENCODING)); } return props; } |
有了這個方法咱們能夠這樣調用這個工具類:
PropertyFileUtil.loadProperties("application.common.properties", "application.properties");
在2.1中拋出的問題也迎刃而解了,把配置文件再根據類型劃分:
- application.common.properties 公共屬性
- application.properties 各個子系統的屬性
- application.local.properties 本地屬性(用於開發時)
請注意:不要把application.local.properties歸入到版本控制(SCM)中,這個文件只能在本地開發環境出現!!!
最後讀取的順序應該這樣寫:
PropertyFileUtil.loadProperties("application.common.properties", "application.properties", "application.local.properties");
2.3 根據環境不一樣選擇不一樣的配置文件
2.2的多文件重載解決了2.1中的問題,可是如今又遇到一個新問題:如何根據不一樣的環境讀取不一樣的屬性文件。
- 開發時最後讀取application.local.properties
- 測試時最後讀取application.test.properties
- 生產環境最後讀取/etc/foo/application.properties
這麼作的目的在於每個環境的配置都不相同,第一步讀取公共配置,第二步讀取子系統的屬性,最後讀取不一樣環境使用的特殊配置,這樣才能作到最完美、最靈活。
既然屬性的值能夠經過國佔位符的方式替換,咱們也能夠順藤摸瓜把讀取文件的順序也管理起來,因此又引入了一個屬性文件:application-file.properties;它的配置以下:
?
1 2 3 |
A=application.common.properties B=application.properties C=${ env .prop.application. file } |
佔位符env.prop.application.file的值能夠動態指定,能夠利用Maven的Profile功能實現,例如針對開發環境配置一個ID爲dev的profile並設置默認激活狀態;對於部署到測試、生產環境能夠在打包時添加-Ptest或者-Pproduct參數使用不一樣的Profile;關鍵在於每個profile中配置的env.prop.application.file值不一樣,例如:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
< profile > < id >dev</ id > < properties > < dev.mode >true</ dev.mode > < env.spring.application.file > classpath*:/application.local.properties </ env.spring.application.file > </ properties > </ profile > < profile > < id >product</ id > < properties > < dev.mode >true</ dev.mode > < env.spring.application.file > file:/etc/foo/application.properties </ env.spring.application.file > </ properties > </ profile > < activeprofiles > < activeprofile >dev</ activeprofile > </ activeprofiles > |
而對於生產環境來講能夠把env.spring.application.file改成/etc/foo/application.properties。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<xmp> public class PropertyFileUtil { private static Logger logger = LoggerFactory.getLogger(PropertyFileUtil. class ); private static Properties properties; private static PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); private static ResourceLoader resourceLoader = new DefaultResourceLoader(); private static final String DEFAULT_ENCODING = "UTF-8" ; /** * 初始化讀取配置文件,讀取的文件列表位於classpath下面的application-files.properties * * 多個配置文件會用最後面的覆蓋相同屬性值 * * @throws IOException 讀取屬性文件時 */ public static void init() throws IOException { String fileNames = "application-files.properties" ; innerInit(fileNames); } /** * 初始化讀取配置文件,讀取的文件列表位於classpath下面的application-[type]-files.properties * * 多個配置文件會用最後面的覆蓋相同屬性值 * * @param type 配置文件類型,application-[type]-files.properties * * @throws IOException 讀取屬性文件時 */ public static void init(String type) throws IOException { String fileNames = "application-" + type + "-files.properties" ; innerInit(fileNames); } /** * 內部處理 * @param fileNames * @throws IOException */ private static void innerInit(String fileNames) throws IOException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); InputStream resourceAsStream = loader.getResourceAsStream(fileNames); // 默認的Properties實現使用HashMap算法,爲了保持原有順序使用有序Map Properties files = new LinkedProperties(); files.load(resourceAsStream); Set<Object> fileKeySet = files.keySet(); String[] propFiles = new String[fileKeySet.size()]; List<Object> fileList = new ArrayList<Object>(); fileList.addAll(files.keySet()); for ( int i = 0 ; i < propFiles.length; i++) { String fileKey = fileList.get(i).toString(); propFiles[i] = files.getProperty(fileKey); } logger.debug( "讀取屬性文件:{}" , ArrayUtils.toString(propFiles));; properties = loadProperties(propFiles); Set<Object> keySet = properties.keySet(); for (Object key : keySet) { logger.debug( "property: {}, value: {}" , key, properties.getProperty(key.toString())); } } </xmp> |
默認的Properties類使用的是Hash算法故無序,爲了保持多個配置文件的讀取順序與約定的一致 因此須要一個自定義的有序Properties實現,參加:LinkedProperties.java
2.4 啓動載入與動態重載
爲了能讓其餘類讀取到屬性須要有一個地方統一管理屬性的讀取、重載,咱們能夠建立一個Servlet來處理這件事情,在Servlet的init()方法中讀取屬性(調用PropertiesFileUtil.init()方法),能夠根據請求參數的action值的不一樣作出不一樣的處理。
咱們把這個Servlet命名爲PropertiesServlet,映射路徑爲:/properties-servlet。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import java.io.IOException; import java.util.Set; import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import me.kafeitu.demo.activiti.util.PropertyFileUtil; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * classpath下面的屬性配置文件讀取初始化類 */ public class PropertiesServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected Logger logger = LoggerFactory.getLogger(getClass()); /** * @see Servlet#init(ServletConfig) */ public void init(ServletConfig config) throws ServletException { try { PropertyFileUtil.init(); ServletContext servletContext = config.getServletContext(); setParameterToServerContext(servletContext);; logger.info( "++++ 初始化[classpath下面的屬性配置文件]完成 ++++" ); } catch (IOException e) { logger.error( "初始化classpath下的屬性文件失敗" , e); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = StringUtils.defaultString(req.getParameter( "action" )); resp.setContentType( "text/plain;charset=UTF-8" ); if ( "reload" .equals(action)) { // 重載 try { PropertyFileUtil.init(); setParameterToServerContext(req.getSession().getServletContext()); logger.info( "++++ 從新初始化[classpath下面的屬性配置文件]完成 ++++,{IP={}}" , req.getRemoteAddr()); resp.getWriter().print("<b>屬性文件重載成功!</b> "); writeProperties(resp); } catch (IOException e) { logger.error( "從新初始化classpath下的屬性文件失敗" , e); } } else if ( "getprop" .equals(action)) { // 獲取屬性 String key = StringUtils.defaultString(req.getParameter( "key" )); resp.getWriter().print(key + "=" + PropertyFileUtil.get(key)); } else if ( "list" .equals(action)) { // 獲取屬性列表 writeProperties(resp); } } private void setParameterToServerContext(ServletContext servletContext) { servletContext.setAttribute( "prop" , PropertyFileUtil.getKeyValueMap()); } } |
Servlet發佈以後就能夠動態管理配置了,例如發佈到生產環境後若是有配置須要更改(編輯服務器上保存的配置文件)能夠訪問下面的路徑重載配置:
http://yourhost.com/appname/properties-servlet?action=reload
2.5 讓人凌亂的佔位符配置
下面的log4j代碼來自實際項目,看一看${…}的數量,有點小恐怖了,對於大型項目配置文件會更多,佔位符也會更多。
3. 配置文件輔助Maven插件--portable-config-maven-plugin
portable-config-maven-plugin是《Maven實戰》做者Juven Xu剛剛發佈的一款插件,這個插件的用途就是在打包時根據不一樣環境替換原有配置,這個插件獨特的地方在於不用使用佔位符預先定義在配置文件中,而是直接替換的方式覆蓋原有配置。
該插件支持替換properties、xml格式的配置文件,使用方法也很簡單,在pom.xml中添加插件的定義:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
< plugin > < groupid >com.juvenxu.portable-config-maven-plugin</ groupid > < artifactid >portable-config-maven-plugin</ artifactid > < version >1.0.1</ version > < executions > < execution > < goals > < goal >replace-package</ goal > </ goals > </ execution > </ executions > < configuration > < portableconfig >src/main/portable/test.xml</ portableconfig > </ configuration > </ plugin > |
src/main/portable/test.xml文件的內容就是須要替換的屬性集合,下面列出了properties和xml的不一樣配置,xml替換使用XPATH規則。
?
1 2 3 4 5 6 7 |
<!--?xml version="1.0" encoding="utf-8" ?--> < portable-config > < config-file path = "WEB-INF/classes/db.properties" > < replace key = "database.jdbc.username" >test</ replace > < replace key = "database.jdbc.password" >test_pwd</ replace > </ config-file > </ portable-config > |
?
1 2 3 4 5 6 |
<!--?xml version="1.0" encoding="utf-8" ?--> < portable-config > < config-file path = "WEB-INF/web.xml" > < replace xpath = "/web-app/display-name" >awesome app</ replace > </ config-file > </ portable-config > |
固然你能夠定義幾個不一樣環境的profile來決定使用哪一個替換規則,在打包(mvn package)時該插件會被激活執行替換動做。
有了這款插件對於一些默認的屬性能夠不使用佔位符定義,取而代之的是實際的配置,因此對現有的配置無任何影響,又能夠靈活的更改配置。