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

引言

配置文件几乎是每个项目中不可或缺的文件类型之一,常用的配置文件类型有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)时该插件会被激活执行替换动作。

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

时间: 2024-11-03 20:42:07

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

Ranger – 给命令行用户一个基于文本的文件管理器

图形文件管理器是每个人日常电脑工作的一部分.多数用户都乐于使用默认的文件管理器,并且没有那么多困扰让他们去探索替代的文件管理器.但是,当使用命令行(CLI)的文件管理器,用户在找到一个最好的之前,可能有兴趣尝试各种可用的文件管理器,以适合他们的需求.在这篇文章中,我们将来看看Ranger,一个基于命令行的文件管理器. ranger-main  注释:这篇文章中的所有示例和使用说明在ubuntu13.04上已通过测试. Ranger - 命令行文件管理器 Ranger是一个基于ncurses库的命

云效公有云如何构建一个基于Composer的PHP项目

最近在将公司的持续集成架构做一个系统的调整,调整过程中受到了云效公有云团队大量的帮助,分享这篇内容希望能让更多的人了解和用好这个产品. 我会把我最近3个月的使用体会分成5个部分:使用云效公有云的动机.PHP项目集成.JS项目集成.JAVA项目集成.Docker类项目集成这5个分支来写. 因为近期公有云的迭代比较频繁,所以我的分享会比较的浅,点到为止,仅供参考,目录: 1.云效公有云如何耦合进我们的业务 2.如何构建一个基于Composer的PHP项目 3.如何构建一个基于NodeJS的前后端项目

云效(原RDC)如何构建一个基于Composer的PHP项目

最近在将公司的持续集成架构做一个系统的调整,调整过程中受到了RDC团队大量的帮助,所以利用国庆时间写了几篇RDC的分享,希望能让更多的人了解和用好RDC这个产品. 我会把我最近3个月的使用体会分成5个部分:使用RDC的动机.PHP项目集成.JS项目集成.JAVA项目集成.Docker类项目集成这5个分支来写 因为近期RDC的迭代比较频繁,所以我的分享会比较的浅,点到为止,仅供参考,目录: 1.RDC如何耦合进我们的业务 2.如何构建一个基于Composer的PHP项目 3.如何构建一个基于Nod

云效(原RDC)如何构建一个基于NodeJS的前后端项目

最近在将公司的持续集成架构做一个系统的调整,调整过程中受到了RDC团队大量的帮助,所以利用国庆时间写了几篇RDC的分享,希望能让更多的人了解和用好RDC这个产品. 我会把我最近3个月的使用体会分成5个部分:使用RDC的动机.PHP项目集成.JS项目集成.JAVA项目集成.Docker类项目集成这5个分支来写 因为近期RDC的迭代比较频繁,所以我的分享会比较的浅,点到为止,仅供参考,目录: 1.RDC如何耦合进我们的业务 2.如何构建一个基于Composer的PHP项目 3.如何构建一个基于Nod

一个基于redis和disque实现的轻量级异步任务执行器

简介 horae是一个基于redis和disque实现的轻量级.高性能的异步任务执行器,它的核心是disque提供的任务队列,而队列有先进先出的时序关系,顾得名:horae. horae : 时序女神,希腊神话中司掌季节时间和人间秩序的三女神,又译"荷莱". horae的关注点不是队列服务的实现本身(已经有不少队列服务的实现了),而是希望借助于redis与disque提供的纯内存的高性能的队列机制,实现一个异步任务执行器.它可以自由配置任务来自哪种队列服务,它不关注任务执行的最终状态(

maven安装配置指南

Maven是一个基于项目对象模型(POM)的项目管理工具.更多详情请参考下面的帖子: maven_百度百科   http://baike.baidu.com/view/336103.htm maven官方网站: http://maven.apache.org/index.html Maven的安装配置比较简单,下面简单介绍一下. 1.从官网上下载安装包.http://maven.apache.org/download.cgi 这里我们点击下载:apache-maven-3.0.4-bin.zip

用Kafka和HBase构建一个基于Docker的数据采集器

本文讲的是用Kafka和HBase构建一个基于Docker的数据采集器,[编者的话]本文主要介绍在Docker上,用Kafka和HBase构建一个数据采集器,并用这个采集器用来记录Caltrain Rider这款应用的GPS数据.本文只是一个简单的实践,读者可以将此方法进行拓展,以更好的学习Docker. 不难看出Docker近来发展迅速.分布式计算现在已日益普遍,而适用于分布式环境的开发工具仍在发展之中.一个多平台的应用在开发.测试以及部署方面已经成为一大难题,但好在虚拟机为我们提供了一个非常

设计一个基于CSS的网页模板

css|模板|设计|网页 这是一个教你如何一步一步学习建立基于CSS制作网站的开始,这个教程将由几个部分组成.第一部分是讲述如何在photoshop中制作导航按扭的:第二部分将讲述背景的制作,再下一个是讲述标题(header)和页面的设计规划的,在最后是CSS和XHTML的应用的执行.现在也许有些人想知道为什么在我的教程里要以导航按扭的制作来开始,呵呵,其实我最初的目的是要讲述一段关于这些简单按扭的制作方法的小教程的,但是即然这个想法开始了,为什么不做一个全面的讲解呢!建立一个像玻璃面一样的导航

Spring Batch 2将支持工作划分和基于注解的配置

这一版本的新特性可以分为四类:对Java 5的支持.非顺序执行.增强的可伸缩性以及注解. 对Java 5的支持: Spring Batch 2.0版支持Java 5泛型和参数化类型,以便可以在编译时检查类型安全性.例如,ItemReader接口现在有了一个类型安全的read方法. 非顺序执行: 这其实包括3个新特性--条件.暂停和并行执行.凭借这些特性,各步骤可以按非线性顺序执行.即使工作(Job)中的某个步骤(step)失败,整个工作也依然能够完成.有条件执行(Conditional exec