话说作为实习生刚入职公司,什么业务啊公司框架啊什么都不懂,所以领导在头几天就吩咐我先看看公司的项目架构,先熟悉一下,以便尽快融入团队,所以前几天我一直在公司划水,即一边看代码,一边看书,颇有几分光拿钱不干活的样子。话说前天下午,领导看我这么闲,好吧,那就先打打杂,实现个小需求,我惊,但还是认真听了领导的需求。
原先项目的配置文件有很多类型(但功能是一样的),比如说配置文件有:开发版(dev)、测试版(test)、内测版(beta)、线上版(online)。这些文件原先是由 maven 打包的时候注入到实际的配置文件,如Spring
,数据库配置等,这么多版本的配置文件是为了方便切换环境,对于系统性的开发,是有专门的开发环境、测试环境、线上环境组成,所以有了这些文件后就不需要再去更改 properties 的配置信息,只需要在pom.xml
中切换一下profile
即可。
领导的需求是这样的,原先一些ip
的信息是写在打包之前的properties
文件中,然后由maven
在编译时注入,现在想把这些信息单独提取出来,放置在公共的位置,比如/data/config.ip.properties
中,等到应用程序启动的时候再去加载这些信息,这样就非常方便运维管理,即开发归开发,运维归运维。运维不知道开发的maven
、dev.properties
这些信息,只管自己分内的ip.properties
,并且放在一个公共的位置,开发想怎么加载就怎么加载,与我运维无关。
以下内容需要对maven
、spring
有一定的了解,否则内容可能会引起的身体不适。
项目结构:
配置文件及代码如下:
1.pom.xml
<build>
<resources>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*.xmlinclude>
<include>**/*.propertiesinclude>
includes>
<filtering>truefiltering>
resource>
resources>
build>
<profiles>
<profile>
<activation>
<activeByDefault>trueactiveByDefault>
activation>
<id>devid>
<build>
<filters>
<filter>../dev.propertiesfilter>
filters>
build>
profile>
profiles>
2.dev.properties
# ip.properties 位置,可以放在公共的位置,这里方便测试,写在类路径下
ip.config.path=classpath:ip.properties
#zookeeper address
zookeeper.address=1.1.1.1:2181
#请求地址
comp.gateway.req_url=http://2.2.2.2/comp
comp.online.req_url=http://3.3.3.3/comp
#dubbo config
dubbo.protocol=dubbo
dubbo.protocol.port=20883
#jdbc config
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://3.3.3.3:3306/paygent
jdbc.username=AF3E3930394523504AC5F4BF53328636
jdbc.password=9674806013AE8945DF960D2C2548768A
3.invoke.properties
#zookeeper trans
zookeeper.address.trans=${zookeeper.address}/trans
#dubbo config
dubbo.protocol=${dubbo.protocol}
dubbo.protocol.port=${dubbo.protocol.port}
#回调地址
comp.online.callback=${comp.online.req_url}/callback
4.application-context.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<util:properties id="settings" location="classpath:invoke.properties"/>
<context:component-scan base-package="me.pingcai"/>
<bean id="biz" class="me.pingcai.conf.bean.Biz">bean>
beans>
5.Biz
public class Biz {
@Value("#{settings['comp.online.callback']}")
public String callback;
@Value("#{settings['zookeeper.address.trans']}")
public String zookeeperAddr;
}
6.测试类
public class PropertiesLoadTests {
public static void main(String[] args) {
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("application-context.xml");
app.start();
Biz biz = app.getBean("biz", Biz.class);
System.out.println("通过SpEL注入的回调地址:" + biz.callback);
System.out.println("通过SpEL注入的zookeeper地址:" + biz.zookeeperAddr);
}
}
总的依赖顺序是:
maven
编译时会将dev.properties
注入到类路径下的*.properties
和*.xml
Spring
会将invoke.properties
注入到bean
中。运行结果:
即配置文件成功从dev.properties
加载到invkoke.properties
,再由Spring
将invkoke.properties
注入到 bean
中。
其中Spring
将配置信息注入bean
由以下配置实现:
等价于:
id="settings" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations" value="classpath:invoke.properties"/>
新的实现需要将ip.properties
提取出来,不能由maven
在编译时注入进去,拆分如下(为了方便测试,ip.properties
放在classpath
下,运维可以放在任意目录):
这就有点蛋疼了,invoke.properties
明明是要依赖于ip.properties
的,就是有如下的配置:
ip.properties中有:${zookeeper.address}
invoke.properties中有:zookeeper.address.trans=${zookeeper.address}/trans
这两者不通过maven
,怎么能让它们组合起来呢?(Spring原生没有这个解决方案)
聪明的小伙伴可能想到了上面提到的PropertiesFactoryBean
,没错,既然这个官方有初步的实现,那我只要改写其加载资源文件那一步不就完事了吗?
打开PropertiesFactoryBean
的源代码,发现其继承了PropertiesLoaderSupport
,而PropertiesLoaderSupport
最重要的一个方法就是:
我们只需要重写这个加载规则就可以解决Properties
相互依赖的问题,以下是具体实现(重点是loadProperties
方法的重写规则):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.core.io.Resource;
import java.io.*;
import java.util.Properties;
/**
* Created by pingcai on 2017/7/19.
*/
public class ConfigPropertiesFactoryBean extends PropertiesFactoryBean {
private final static Logger log = LoggerFactory.getLogger(PropertiesFactoryBean.class);
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
private static final String DEFAULT_ENCODING = "UTF-8";
private static final String REPLACE_PATTERN = "${%s}";
private static final String EMPTY_STRING_PATTERN = "\\s*";
private Resource[] locations;
//是否允许空值
private boolean allowNullValue = true;
// 是否忽略不存在的配置文件
private boolean ignoreResourceNotFound = false;
//是否忽略无法解析的带有通配符的value
private boolean ignoreUnresolvablePlaceholders = false;
@Override
protected void loadProperties(Properties props) throws IOException {
if (this.locations != null) {
for (Resource location : this.locations) {
if (location.exists()) {
reload(props, location.getFile(), DEFAULT_ENCODING);
} else {
if (ignoreResourceNotFound) {
logger.info(String.format("资源文件 %s 不存在!", location.getFilename()));
} else {
throw new RuntimeException(String.format("资源文件 %s 不存在!", location.getFilename()));
}
}
}
}
}
public void reload(Properties props, File file, String encoding) {
BufferedReader bufferedReader = null;
InputStreamReader read = null;
try {
read = new InputStreamReader(new FileInputStream(file), encoding);// 考虑到编码格式
bufferedReader = new BufferedReader(read);
String lineTxt = null;
while ((lineTxt = bufferedReader.readLine()) != null) {
buildMap(props, lineTxt);
}
bufferedReader.close();
read.close();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (read != null) {
read.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void buildMap(Properties props, String item) {
if (!isValidItem(item)) {
return;
}
String key = item.substring(0, item.indexOf("=")).trim();// isValidItem 已校验过,不会空指针
String value = item.substring(item.indexOf("=") + 1).trim();
value = resolveValueReferences(props, key, value);//解决Properties相互引用问题
props.put(key, value);
}
/**
* 更新value,如果是确定的值,直接返回
* 如果不是确定的值,则进行替换,
* 先渠道${key}的key,然后在旧的Properties中查找并通过替换到原来的整个value,
* 此时判断替换后的value和旧的value是否相等
* 如果相等,则是循环引用或替换失败
* 如果含表达式${key},则继续解析
* @param props
* @param val
*/
private String resolveValueReferences(Properties props, String key, String val) {
if (isNormalValue(key, val)) { // 正常的值,即没有通配符 ${}
return val;
}
int i = val.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
int j = val.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);
String subKey;//截取子表达式,如:${zookeeper}:${port}/${addr}
String subValue;
StringBuilder sb = new StringBuilder();
while (i > -1 && j > -1 && i < val.length() && j < val.length() && i < j) {
subKey = val.substring(i + 2, j);
subValue = props.getProperty(subKey);
if (subValue == null) {
if (ignoreUnresolvablePlaceholders) {
logger.info(String.format("找不到key为%s的配置项,保留通配符。", subKey));
sb.append(String.format(REPLACE_PATTERN, subKey));
} else {
logger.info(String.format("找不到key为%s的配置项,不保留通配符。", subKey));
}
} else {
sb.append(subValue);
}
i = val.indexOf(DEFAULT_PLACEHOLDER_PREFIX, j);
/**
* 1.没有通配符
* 2.剩下还有通配符,则截取后继续解析
*/
if (i == -1) {
sb.append(val.substring(j + 1));
} else if (i > j + 1) {
sb.append(val.substring(j + 1, i)); //拼接余下的字符串
}
j = val.indexOf(DEFAULT_PLACEHOLDER_SUFFIX, i);
}
return sb.toString();
}
/**
* 是否是正常的properties,
* 不合法类类型:空行,注释,不包含 = 号
* @param item
* @return
*/
private boolean isValidItem(String item) {
if (item == null || item.matches(EMPTY_STRING_PATTERN) || item.startsWith("#") || !item.contains("=")) {
return false;
}
return true;
}
/**
* 检查value是否含有表达式
* @param val
* @return
*/
private boolean isNormalValue(String key, String val) {
if (!allowNullValue && val == null) {
throw new RuntimeException(String.format("Properties Item 不允许为空!当前 key :%s", key));
}
if (val != null) {
int i = val.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
int j = val.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);
if (i >= 0 && j > 0 && j != i) {
return false;
}
}
return true;
}
public void setLocations(Resource[] locations) {
this.locations = locations;
}
@Override
public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) {
this.ignoreResourceNotFound = ignoreResourceNotFound;
}
public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) {
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
}
public void setAllowNullValue(boolean allowNullValue) {
this.allowNullValue = allowNullValue;
}
}
更新后的项目结构及运行结果:
拆除打包后的jar
包,我们可以看到关于ip
的配置项没有被maven
自动注入:
到这一步就基本完成了我们的需求。
有一个细节是在bean
中,我们使用的是SpEL
表达式,如:@Value("#{settings['comp.online.callback']}")
还有一种实现是占位符,如:@Value("${comp.online.callback}")
,但PropertiesFactoryBean
只能解决SpEL
,不能解决占位符,这时候我们再打开源码,发现PropertiesFactoryBean
的父类还有一个实现,就是PropertyPlaceholderConfigurer
,那这个类就是来解决占位符问题的,我们只需要让他们引用同一份Properties
即可,配置如图:
同时重写bean
的表达式,并测试:
完美实现,perfect !
1.资源文件读取和注入由PropertiesLoaderSupport
定义,底下有PropertiesFactoryBean
和PropertyPlaceholderConfigurer
两种实现;
2.在xml
配置中,
的实现就是PropertiesFactoryBean
;
3.PropertiesFactoryBean
解决SpEL
,PropertyPlaceholderConfigurer
解决占位符;
4.路还很长啊,api
要多熟悉,码到用时方恨少这样可不行;
源码(master是最新实现,old分支是旧的实现):https://github.com/pingcai/properties-demo