一个基于Maven的配置文件管理成功案例

标签: maven 文件管理 成功 | 发表时间:2013-06-29 15:00 | 作者:
出处:http://www.kafeitu.me/

引言

配置文件几乎是每个项目中不可或缺的文件类型之一,常用的配置文件类型有xml、properties等,配置文件的好处不言而喻,利用配置文件可以灵活地设置软件的运行参数,并且可以在更改配置文件后在不重启应用的情况下即时生效。

写这篇文章的原因是最近在一个项目中引入了我对配置文件的管理方式,我觉得有必要与大家分享,希望能抛砖引玉。

1. 我所知道的配置文件管理方式

下面大概列出了几类对配置文件的管理方式,请对号入座 _^

1.1 配置文件是什么?

除非应用不需要配置文件(例如Hello World),否则请无视,continue;

1.2 数据库方式

把配置文件保存在数据库,看起来这种方式很不错,配置不会丢失方便管理,其实问题很大;举个简单的例子,在A模块需要调用B模块的服务,访问B模块的URL配置在数据库,首先需要在A中读取B模块的URL,然后再发送请求,问题紧随而来,如果这个功能只有一个开发人员负责还好,假如多个人都要调试那就麻烦了,因为配置保存在数据库(整个Team使用同一个数据库测试),每个开发人员的B模块的访问端口(Web容器的端口)又不相同或者应用的ContextPath不同,要想顺利调试就要更改数据库的B模块URL值,这样多个开发人员就发生了冲突;问题很严重!

1.3 XML方式

对于使用SSH或者其他的框架、插件的应用在src/main/resources下面肯定有不少的xml配置文件,今天的主题是应用级的配置管理,所以暂且抛开框架必须的XML配置文件,先来看看下面的XML配置文件内容。



    
    

这种方式在之前很受欢迎,也是系统属性的主要配置方式,不过使用起来总归不太简洁、灵活(不要和我争论XML与Properties)。

1.4 属性文件方式

下面是kft-activiti-demo中 application.properties文件的一部分:

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配置如下:


    
        
            src/main/resources
            true
        
    

这样在编译时 src/main/resources目录下面中文件()只要有 ${foo}占位符就可以自动替换成实际的值了,例如1.4节中属性 system.version使用的是一个占位符而非实际的值,${project.version}表示pom.xml文件中的 project标签的 version

属性 export.diagram.pathdiagram.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读取,下面的代码简单实现了读取属性集合:

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简单实现了属性缓存功能:

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方法,接收的参数是一个可变数组,循环读取属性文件。

/**
 * 载入多个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;它的配置如下:

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值不同,例如:


    dev
    
        true
        
            classpath*:/application.local.properties
        
    



    product
    
        true
        
            file:/etc/foo/application.properties
        
    



    dev

而对于生产环境来说可以把 env.spring.application.file改为 /etc/foo/application.properties



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<br/>
     *
     * 多个配置文件会用最后面的覆盖相同属性值
     *
     * @throws IOException  读取属性文件时
     */
    public static void init() throws IOException {
        String fileNames = "application-files.properties";
        innerInit(fileNames);
    }

    /**
     * 初始化读取配置文件,读取的文件列表位于classpath下面的application-[type]-files.properties<br/>
     *
     * 多个配置文件会用最后面的覆盖相同属性值
     *
     * @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()));
        }
    }

默认的Properties类使用的是Hash算法故无序,为了保持多个配置文件的读取顺序与约定的一致 所以需要一个自定义的有序Properties实现,参加: LinkedProperties.java

2.4 启动载入与动态重载

为了能让其他类读取到属性需要有一个地方统一管理属性的读取、重载,我们可以创建一个Servlet来处理这件事情,在Servlet的 init()方法中读取属性(调用PropertiesFileUtil.init()方法),可以根据请求参数的 action值的不同做出不同的处理。

我们把这个Servlet命名为 PropertiesServlet,映射路径为:/properties-servlet。

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("  属性文件重载成功!  
"); 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中添加插件的定义:


   com.juvenxu.portable-config-maven-plugin
   portable-config-maven-plugin
   1.0.1
     
       
         
           replace-package
         
       
     
     
       src/main/portable/test.xml
     

src/main/portable/test.xml文件的内容就是需要替换的属性集合,下面列出了properties和xml的不同配置,xml替换使用XPATH规则。



  
    test
    test_pwd
  



  
    awesome app
  

当然你可以定义几个不同环境的profile来决定使用哪个替换规则,在打包(mvn package)时该插件会被激活执行替换动作。

有了这款插件对于一些默认的属性可以不使用占位符定义,取而代之的是实际的配置,所以对现有的配置无任何影响,又可以灵活的更改配置。

转载自:

相关 [maven 文件管理 成功] 推荐:

一个基于Maven的配置文件管理成功案例

- - 咖啡兔
配置文件几乎是每个项目中不可或缺的文件类型之一,常用的配置文件类型有xml、properties等,配置文件的好处不言而喻,利用配置文件可以灵活地设置软件的运行参数,并且可以在更改配置文件后在不重启应用的情况下即时生效. 写这篇文章的原因是最近在一个项目中引入了我对配置文件的管理方式,我觉得有必要与大家分享,希望能抛砖引玉.

maven学习之maven与eclipse集成

- - ITeye博客
    在上一篇博文中讲到了maven的配置,今天要讲maven与eclipse的集成. 现在在java开发中,eclipse占据了绝大部分的市场份额. 其他的关于NetBeans,IDEA中的关于maven的配置,读者可参阅相关资料. (注:关于IDEA,据说是款非常强大的集成开发环境,但是据说对svn的支持不是很好,道听途说而已).

maven工程下整合spring+mybatis+freemarker

- - CSDN博客架构设计推荐文章
博客地址:http://zhengyinhui.com/?p=142. 由于工作主要是前端开发,做后端的项目比较少,最近自己做个项目,发觉好多的都忘了,这里写篇博客整理下maven工程下整合spring+mybatis+freemarker相关内容. 新建个Archetype为maven-archetype-webapp的maven项目(安装maven插件:http://download.eclipse.org/technology/m2e/releases),在pom文件添加相关依赖:.

Maven最佳实践:划分模块

- - 企业架构 - ITeye博客
“分天下为三十六郡,郡置守,尉,监” —— 《史记·秦始皇本纪》. 所有用Maven管理的真实的项目都应该是分模块的,每个模块都对应着一个pom.xml. 它们之间通过继承和聚合(也称作多模块,multi- module)相互关联. 我们明明在开发一个项目,划分模块后,导入Eclipse变成了N个项目,这会带来复杂度,给开发带来 不便.

从Maven仓库中导出jar包

- - 企业架构 - ITeye博客
从Maven仓库中导出jar包:进入工程pom.xml 所在的目录下,. 输入以下命令:mvn dependency:copy-dependencies -DoutputDirectory=lib. 更简单的  mvn dependency:copy-dependencies    会导出到target\dependency 下面.

Maven 3 入门 -- 核心概念

- - 开源软件 - ITeye博客
例如上篇helloworld的pom.xml配置文件中的. 其实就是坐标,那么Maven的坐标元素包括groupId、artifactId、version、packaging、classifier. 当一个项目通过依赖,插件或者父项目引用与另外一个项目关联的时候,Maven通过坐标来精确定位一个项目.

简单的Maven模块结构

- - 研发管理 - ITeye博客
简单的Maven模块结构是这样的:. 上述简单示意图中,有一个父项目(app-parent)聚合很多子项目(app-util, app-dao, app-service, app-web). 每个项目,不管是父子,都含有一个pom.xml文件. 而且要注意的是,小括号中标出了每个项目的打包类型. 父项目是pom,也只能是pom.

maven导出项目依赖的jar包

- - 互联网 - ITeye博客
在进行项目部署时,需要将maven项目所依赖的jar导出到指定目录,本文讲解如何导出项目依赖的jar包. 一、导出到默认目录 targed/dependency .     从Maven项目中导出项目依赖的jar包:进入工程pom.xml 所在的目录下,执行如下命令:.        或在eclipse中,选择项目的pom.xml文件,点击右键菜单中的Run As,见下图红框中,在弹出的Configuration窗口中,输入  dependency:copy-dependencies 后,点击运行.

Maven使用archetype生成项目

- -
Archtype指项目的骨架, Maven中的 maven-archetype-plugin插件使用户可以方便的生成所需的项目骨架.. ###1.命令行调用maven插件. 如果不加任何参数,命令窗口显示的会有上千中项目骨架可够选择,我们可以使用 -DarchetypeCatalog, -Dfilter选择过滤项目骨架,参数赋值的格式跟JAVA设置系统属性一样 -D=.

Marlin: Elementary 文件管理器

- 加州旅客 - LinuxTOY
如果您喜好 OS X Finder 风格的文件管理器,Marlin 必然适合您. Marlin 文件管理器作为 Elementary OS 项目的一部分由 ammonkey 开发. Marlin 拥有如同 Finder 的列表视图:. 以及小小的选择 + 号和单击启动模式:. 现在 Marlin 测试版本的 PPA 仓库已经上线,在 Ubuntu 11.04 中安装,请进行:.