你的位置:首页 > Java教程

[Java教程]项目参数外部配置化


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

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

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

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

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

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

外部配置使得模块可以跨环境上下文配置。下图展现了外部配置,在这里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.

假设系统的部署有四套环,分别是

  • dev: 开发环境
  • testa: 第一轮测试
  • testb: 第二轮测试
  • product: 生产环境

那么,修改pom.

<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.

如果是在命令行使用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的应用有两种方案:注解式分布式配置使用方式和

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自动同步到应用系统后,项目中读取参数值时,就能加载到最新的参数值。