引言
配置文件几乎是每一个项目中不可或缺的文件类型之一,经常使用的配置文件类型有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)时该插件会被激活执行替换动做。
有了这款插件对于一些默认的属性能够不使用占位符定义,取而代之的是实际的配置,因此对现有的配置无任何影响,又能够灵活的更改配置。