zoukankan      html  css  js  c++  java
  • 项目参数外部配置化

    开发一个项目,参数是必不可少的,规模越大参数越多。在不同的测试环境中部署,或者是依赖项目的信息发生了变化,你有没有想跳楼的感觉?如果有,恭喜你,你至少已经不是在开发玩具系统了。

    本文试图列举一些配置参数的方法,希望对你的项目有所帮助。

    一、可用性模式-外部配置

    引用自图书《Java应用架构设计:模块化模式与OSGi》10.2

    “模块应该可以在外部进行配置”

    当把模块部署到运行时环境中时,在使用它之前通常要进行初始化。例如,为了让模块能够访问数据库中的数据,要用必要的用户ID和密码来初始化模块。但是,我们也希望避免将配置信息与模块紧密耦合。如果这样做,将会使模块与单一的上下文环境耦合,这样就限制了模块在其他可选的上下文中进行重用。

    外部配置使得模块可以跨环境上下文配置。下图展现了外部配置,在这里Client类使用一个XML配置文件配置client.jar模块。要注意的是,用来初始化client.jar的配置信息与表示模块行为的Client类分开了。能够配置模块到环境上下文中会增强跨环境重用模块的能力。

    配置文件的位置,有三种处理方式:

    1、配置信息包含在模块中,优势是在模块的默认上下文中很易于使用,不足在于在其他的上下文中不能正常工作。

    clip_image002

    2、配置信息不在模块中,但是在初始化的时候由外部提供给模块。优势是能跨环境重用,不足是每个环境都要配置所有参数。

    clip_image004

    3、更灵活的方案是在模块中提供默认配置文件,但是允许模块外部提供替代的配置文件。下图是图书中的一个例子。

    clip_image006

    这三种方案中,最后一种看起来最有诱惑,能够实现比较灵活的配置方式。后续我们用这种方案进行设计。

    二、默认+替代的配置方案

    考虑一个企业开发中一个相对简单的项目,同时提供WEB界面和API接口。为了方便其他系统调用API,同时提供一个 client jar供调用。

    1、系统设计

    clip_image008

    各个模块的简单介绍:

    • 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对参数进行管理。

    clip_image010

    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进行启用,方法类似,配置界面为:

    clip_image012

    使用maven进行项目打包,也是相同的方法, 在profile处选择testa即可。

    4、在Eclipse中使用Server启动

    在Eclipse中添加Server Runtime Environments后,将项目部署到Server中。在项目上右键,选择“属性”,在弹出的窗口中选择“Maven”,即可输入相应额Profile。

    clip_image014

    四、实现参数实时更新

    之前的实现,已经很好的解决了多环境部署的问题。考虑到生产环境的特殊性,不能随便重启应用。如果某一个关键参数需要修改,按照之前的方案,需要重新打包并部署到生产环境,应用将会重新启动。

    如果项目是关键业务,客户要求不能停机,必须实现参数的实时修改,怎么办?多点环境灰度发布,是一种解决方案;osgi模块化开发部署应该也是一种解决方案。只是这两种方案,很难在已有的项目中实现,我们还是考虑简单一点的处理方式。

    1、提供参数管理功能(DB)

    在系统中实现一个参数设置功能,由管理员将最新的参数值保存在数据库中。系统首先读取数据库中的参数值,如果为空再从properties文件中读取。当需要调整系统参数时,管理员进入管理界面修改并保存即可。

    clip_image015

    可以看出,系统要实现这个定制功能,需要完成:参数数据表、参数封装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。项目结构变为

    clip_image017

    2.2 PropsUtil的修改

    这是在前面PropsUtil的基础之上进行修改,不详述,概要介绍一下需要修改的内容。

    1、增加一个Resource

    读取资源文件的定义为classpath*:conf-disconf.properties。这个配置文件需要记录更新时间。

    2、增加一个Configuration,用于加载新配置文件的内容。这个配置需要检查资源文件的更新时间,如果发现时间有变化,则重新加载内容。

    3、读取配置参数时,首先读取conf-disconf.properties中的内容,如果没有再加载原顺序加载的配置信息。

    这样,当disconf Server中的配置信息发生变化,由disconf-client自动同步到应用系统后,项目中读取参数值时,就能加载到最新的参数值。

  • 相关阅读:
    windows 快捷键
    MySQLorder by用法
    Idea-2020.1.3破解
    Java-FTP上传下载
    Java-byte转换
    Java-执行shell命令
    Java-文件加密传输(摘要+签名)
    AmazonRekognition-Java对接
    Java-File转MultipartFile
    AmazonS3-Java对接
  • 原文地址:https://www.cnblogs.com/codestory/p/5527383.html
Copyright © 2011-2022 走看看