在应用程序开发中,配置文件是一个重要但反而总是被忽略掉的问题,因为它的解决方案很简单,而且在项目中往往被架构师一次性就写好了,其他的开发人员只是用就可以了,根本不用关心。但是初学者在从头搭建项目脚手架时,往往被这貌似简单的问题绊住手脚,浪费时间。今天花点时间说下我所理解的配置化
单独说配置文件其实非常简单,无非就是把一些程序中会用到的参数放置到文本文件中,方便之后修改,但是往往这些配置项是程序的关键参数,比如数据库连接地址、接口地址,连接池定义、程序的logo等。这些参数往往都与环境相关,在本地开发时、测试环境、灰度环境、生产环境都不一样,于是需要根据不同的环境用不同的配置文件,这就是我们的需求的来源
随便列举几种应用场景:
1、中间件地址不同。我们的应用都依赖于一个队列集群,不同环境下的队列集群地址不一样。
2、日志打印的位置不一样。在本地调试时,我的日志打印到一个目录下,但是在测试环境的主机上,我希望日志打印到另外一个目录下
3、希望根据不同的环境做不同的业务处理。业务需求说每天给xxx@email.com发送邮件,但是我只想生产环境发送真实的邮件,测试环境我希望只是返回一个成功字符串即可。
使用spring.profile.active
从以上的问题就可以看出,定义出能区分环境的变量非常关键,于是自然而然想到,在启动程序时传入一个参数,告诉程序目前是什么环境,程序根据不同的环境来加载不同的配置项。
于是,在spring的世界里,开发者们定义了spring.profile.active这个变量,并互相约定当在xml或者Bean定义中见到profile
时,把它当做环境的标识。比如:只有在当前模式为spring.profile.active=test的时候,才会加载profile=test
的配置项。这在一些Spring团队出品的软件上工作的不错。随着时间的推移,spring成为实际意义上的j2ee标准,于是这一做法获得广泛的应用,举一个配置文件的例子:
<!-- 开发环境配置文件 -->
<beans profile="test">
<context:property-placeholder location="/WEB-INF/test-orm.properties" />
</beans>
<!-- 本地环境配置文件 -->
<beans profile="local">
<context:property-placeholder location="/WEB-INF/local-orm.properties" />
</beans>
在程序启动时jvm参数传入-Dspring.profile.active=test
打包工具和占位符
上述spring.profile.active操作方式解决了不少的问题,但是这还是遗留了一些问题:
问题一:如何传入这个参数带来了一些新的工作量,尽管spring提供了各种各样的方式设置这个参数,比如:设置在操作系统环境变量里、设置在tomcat的jvm参数等,但是这些方法都需要在程序的目标主机上做些操作,尽管这工作量并不大,但是程序不再是随处迁移的,必须要在相应的主机上进行设置才行。
问题二:在一些并不是spring团队出品或者没有遵循spring.profile.active设计的一些软件上时,这种方式并不好用的。当然还是可以通过一些修改,把配置迁移到spring中以遵循这个设计,但是成本还是有点高。
问题三:分布式应用越来越流行,相同的应用越来越多,每一份配置文件都要在程序中放一份,当修改了一个配置项时,所有的相关程序都要重新部署打包。
针对问题一和二,工程师们想出了另外的一种解决思路,就是利用maven或者gradle等打包工具,把环境参数的定义放在打包部署的环节,在编译打包的时候,传入相应的环境参数,来决定将什么样的配置项打包部署到程序包中去,这样就不需要在运行时做处理,而且也不依赖spring.profile.active设计。
以gradle下spring boot为例,配置文件通常是放置到src/main/resources
中的,比如:
开发环境下
bootstrap.properties定义了队列集群的地址:
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=aaa
spring.rabbitmq.password=bbb
log4j2.xml定义了日志文件的路径及名称:
<RollingFile name="radalog" fileName="../aaa.log" filePattern="../$${date:yyyy-MM}/aaa-%d{yyyy-MM-dd}-%i.log.gz">
....
</RollingFile>
测试环境下
bootstrap.properties定义了队列集群的地址:
spring.rabbitmq.host=111.111.111.111
spring.rabbitmq.port=5672
spring.rabbitmq.username=test
spring.rabbitmq.password=test
log4j2.xml定义了日志文件的路径及名称:
<RollingFile name="radalog" fileName="/home/tomcat/logs/aaa.log" filePattern="/home/tomcat/logs/$${date:yyyy-MM}/aaa-%d{yyyy-MM-dd}-%i.log.gz">
....
</RollingFile>
如何实现上述的效果呢,解决方案就是占位符
将bootstrap.properties中的内容替换为
spring.rabbitmq.host=@rabbitmq.host@
spring.rabbitmq.port=@rabbitmq.port@
spring.rabbitmq.username=@rabbitmq.username@
spring.rabbitmq.password=@rabbitmq.password@
将log4j2.xml中的内容替换为:
<RollingFile name="radalog" fileName="@log4j2.dirLogName@/@projectName@.log" filePattern="@log4j2.dirLogName@/$${date:yyyy-MM}/@projectName@-%d{yyyy-MM-dd}-%i.log.gz">
....
</RollingFile>
再声明一个新的配置文件config.groovy
environments {
//开发环境
dev {
log4j2 {
dirLogName = '../logs'
}
rabbitmq{
host='127.0.0.1'
port='5672'
username='aaa'
password='bbb'
}
}
//测试环境
test {
log4j2 {
dirLogName = '/home/tomcat/logs'
}
rabbitmq{
host='111.111.111.111'
port='5672'
username='test'
password='test'
}
}
}
看明白了吗,我们的目标就是将config.groovy中的内容根据不同的环境(dev or test),替换不同的配置项到配置文件中去,如何做到这一点呢,利用gradle的一个内置任务processResources
修改build.gradle文件(gradle的打包配置文件,同maven的pom文件,不懂得去百度)
//获取传入参数
def getProfile(){
def profile = System.getProperty("profileMode") ?: "dev";
return profile;
}
//替换配置文件中的@@
def loadGroovyConfig(){
def configFile = file('src/main/resources/config.groovy')
new ConfigSlurper(getProfile()).parse(configFile.toURL()).toProperties()
}
processResources {
from(sourceSets.main.resources.srcDirs) {
println project.name+'进入了processResources方法'
println '打包模式::'+getProfile()
def mode =getProfile();
filter(org.apache.tools.ant.filters.ReplaceTokens,tokens:[systemMode:mode])
filter(org.apache.tools.ant.filters.ReplaceTokens,tokens:[projectName:project.name])
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: loadGroovyConfig())
}
}
定义好了的话,当你使用gradle bootRun -DprofileMode=test
的时候,程序中配置文件的@@占位符最终都会被替换成config.groovy的test下的内容。
当然,不使用占位符的话,使用不同的文件夹也可以作区分比如test文件都放到test文件夹下,在打包命令gradle bootRun -DprofileMode=test
时,使用test文件夹的内容替换src/main/resources的内容,但是我认为这种方式处理起来还是不如占位符更加清晰明了。
分布式配置中心
第一二个问题解决了,第三个问题怎么办?答案是分布式配置中心。简而言之,就是把配置项按照环境不同统一放到一个叫做配置中心的程序进程里,所有的程序都与这个配置中心进行远程通信(通常使用restful接口)而实现配置统一管理的目的。目前比较流行的是spring cloud的config server和百度disconf分布式配置中心。至于配置中心的选型并不在本文的讨论范围之内,感兴趣自行百度下即可,容易得很。
通常一个配置中心里包含的内容:
app-base-sample-dev.properties
app-base-sample-prod.properties
app-base-sample-dev.properties
app-base-sample.properties
当然这个问题,其实还有另外的解决方案就是数据库,在高可用及性能要求都没有特别高的情况下,数据库一直是分布式配置好用的简单解决方案。
需要注意的是,当使用到分布式配置中心时,所有的环境的配置都放到一个统一的配置中心的时候,你可能会以为在应用程序内部配置文件就已经消失了,但是事实上并不是,因为最起码你还得定义一个配置文件放配置中心的url路径不是。
其他
还有一个问题,在编写一些业务代码的时候,当前运行环境是一个有用的参数,比如说文章一开始说的生产环境发邮件,但是测试环境不发送邮件的场景。因此最佳的做法在配置文件中定义一个运行模式。我通常的做法就是定义system.mode=@systemMode@
,@systemMode@的替换查看上面的build.gradle的代码
而在java中定义一个类
public class SystemMode {
/**
* 获取当前系统运行模式,默认是测试模式
*/
public static String getMode(){
String mode = PropertyUtils.getProperty("system.mode");
logger.debug("服务器模式:"+mode);
//默认为开发模式
if(mode==null||"".equals(mode)){
mode="dev";
logger.info("没有设置,默认服务器模式:"+mode);
}
return mode;
}
public static boolean isTestMode(){
if("test".equals(getMode().toLowerCase())){
return true;
}
return false;
}
public static boolean isProdMode(){
if("prod".equals(getMode().toLowerCase())){
return true;
}
return false;
}
}
这样就可以愉快的使用了!