项目参数外部配置化
开发一个项目,参数是必不可少的,规模越大参数越多。在不同的测试环境中部署,或者是依赖项目的信息发生了变化,你有没有想跳楼的感觉?如果有,恭喜你,你至少已经不是在开发玩具系统了。
本文试图列举一些配置参数的方法,希望对你的项目有所帮助。
一、可用性模式-外部配置
引用自图书《Java应用架构设计:模块化模式与OSGi》10.2
“模块应该可以在外部进行配置”
当把模块部署到运行时环境中时,在使用它之前通常要进行初始化。例如,为了让模块能够访问数据库中的数据,要用必要的用户ID和密码来初始化模块。但是,我们也希望避免将配置信息与模块紧密耦合。如果这样做,将会使模块与单一的上下文环境耦合,这样就限制了模块在其他可选的上下文中进行重用。
外部配置使得模块可以跨环境上下文配置。下图展现了外部配置,在这里Client类使用一个XML配置文件配置client.jar模块。要注意的是,用来初始化client.jar的配置信息与表示模块行为的Client类分开了。能够配置模块到环境上下文中会增强跨环境重用模块的能力。
配置文件的位置,有三种处理方式:
1、配置信息包含在模块中,优势是在模块的默认上下文中很易于使用,不足在于在其他的上下文中不能正常工作。
2、配置信息不在模块中,但是在初始化的时候由外部提供给模块。优势是能跨环境重用,不足是每个环境都要配置所有参数。
3、更灵活的方案是在模块中提供默认配置文件,但是允许模块外部提供替代的配置文件。下图是图书中的一个例子。
这三种方案中,最后一种看起来最有诱惑,能够实现比较灵活的配置方式。后续我们用这种方案进行设计。
二、默认+替代的配置方案
考虑一个企业开发中一个相对简单的项目,同时提供WEB界面和API接口。为了方便其他系统调用API,同时提供一个 client jar供调用。
1、系统设计
各个模块的简单介绍:
- 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对参数进行管理。
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进行启用,方法类似,配置界面为:
使用maven进行项目打包,也是相同的方法, 在profile处选择testa即可。
4、在Eclipse中使用Server启动
在Eclipse中添加Server Runtime Environments后,将项目部署到Server中。在项目上右键,选择“属性”,在弹出的窗口中选择“Maven”,即可输入相应额Profile。
四、实现参数实时更新
之前的实现,已经很好的解决了多环境部署的问题。考虑到生产环境的特殊性,不能随便重启应用。如果某一个关键参数需要修改,按照之前的方案,需要重新打包并部署到生产环境,应用将会重新启动。
如果项目是关键业务,客户要求不能停机,必须实现参数的实时修改,怎么办?多点环境灰度发布,是一种解决方案;osgi模块化开发部署应该也是一种解决方案。只是这两种方案,很难在已有的项目中实现,我们还是考虑简单一点的处理方式。
1、提供参数管理功能(DB)
在系统中实现一个参数设置功能,由管理员将最新的参数值保存在数据库中。系统首先读取数据库中的参数值,如果为空再从properties文件中读取。当需要调整系统参数时,管理员进入管理界面修改并保存即可。
可以看出,系统要实现这个定制功能,需要完成:参数数据表、参数封装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。项目结构变为
2.2 PropsUtil的修改
这是在前面PropsUtil的基础之上进行修改,不详述,概要介绍一下需要修改的内容。
1、增加一个Resource
读取资源文件的定义为classpath*:conf-disconf.properties。这个配置文件需要记录更新时间。
2、增加一个Configuration,用于加载新配置文件的内容。这个配置需要检查资源文件的更新时间,如果发现时间有变化,则重新加载内容。
3、读取配置参数时,首先读取conf-disconf.properties中的内容,如果没有再加载原顺序加载的配置信息。
这样,当disconf Server中的配置信息发生变化,由disconf-client自动同步到应用系统后,项目中读取参数值时,就能加载到最新的参数值。