PropertyPlaceholderConfigurer加载配置文件
1.是什么
2.工作场景
3.使用方法
工作场景1的使用方法
工作场景2的使用方法
代码中获取配置属性
4.源码分析
1)加载配置文件到Properties中
2)转化Properties进行重新提交,重写该方法可以对配置项进行处理,例如解密
3)把Properties加载到spring容器中替换BeanDefinition中${}对应的属性
1.是什么
PropertyPlaceholderConfigurer是spring中bean的后置处理器的实现类,也就是BeanFactoryPostProcessor接口的一个实现类
2.工作场景
工程中经常会把配置项放在一个或者多个Properties文件中,项目启动时加载Properties文件到Properties中,然后进行替换Spring容器中BeanDefinition对应属性中${}
当我们配置项中会涉及到数据库密码之类的敏感数据,我们需要加密放置在配置文件中,该场景可以继承PropertyPlaceholderConfigurer方法重写convertProperty方法,在Spring启动加载配置文件到Properties后进行解密,然后进行替换Spring容器中BeanDefinition对应属性中${}
3.使用方法
工作场景1的使用方法
工作场景1的方法比较简单,流程如下,以数据配置文件为例:
编写jdbc.properties文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/data?serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
1
2
3
4
配置PropertyPlaceholderConfigurer加载文件
在Spring配置文件applicationContext.xml中配置,随Spring启动进行加载
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true" />
<property name="locations">
<list>
<value>classpath:config/jdbc.properties</value>
</list>
</property>
</bean>
1
2
3
4
5
6
7
8
如果引用多个文件有两种形式
形式1
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true" />
<property name="locations">
<list>
<value>classpath:config/*.properties</value>
</list>
</property>
</bean>
1
2
3
4
5
6
7
8
形式2
classpath:config/jdbc.properties classpath:conf.properties1
2
3
4
5
6
7
8
配置项使用场景有两种使用方法
1)配置文件中使用
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
1
2
3
4
5
6
2)在代码中通过@Value使用,例如
@Value("${jdbc.username}")
private String username;
1
2
工作场景2的使用方法
当文件中存在加密配置时,这些配置项并不能直接使用,需要进行解密后才能使用
编写jdbc.properties文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/data?serverTimezone=UTC
jdbc.username=root
jdbc.password=mJephukdo4Js82iZSmCu25mSBSO+J5Rs0li8taFtbCs=
1
2
3
4
2.自定义ReadPropertyCfg类继承PropertyPlaceholderConfigurer,重写convertProperty方法
public class ReadPropertyCfg extends PropertyPlaceholderConfigurer implements InitializingBean {
private static Map<String, String> ctxPropertiesMap =
new HashMap<String, String>(16);
/**
* 需要做解密处理的key
/
private static Set encryptSet = new HashSet(16);
static {
// 需要做加解密的key
encryptSet.add(“jdbc.password”);
}
/*
* 配置文件中加密配置属性的转换
*
* @param propertyName 配置的名称
* @param propertyValue 配置的值
* @return 转化结果
/
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if (null != propertyName && (encryptSet.contains(propertyName))) {
String pwdresult = AESCryptor.decryptData(propertyValue, CipherConfig.getWorkCipherKey(“mysql.workkey”));
return pwdresult;
}
return propertyValue;
}
/*
* 在初始化根秘钥
*
* @throws IOException
* @throws DecoderException
*/
@Override
public void afterPropertiesSet() throws SecureUtilCommonException, DecoderException, IOException {
setFileEncoding(“UTF-8”);
CipherConfig.init();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
对上述代码进行简要分析,encryptSet是放置所有需要解密的属性,convertProperty方法把需要解密的数据进行解密,如何加载到Spring容器中等下分析
3. 在Spring配置文件applicationContext.xml中配置ReadPropertyCfg来加载配置文件
<bean id="propertyConfigurer" class="com.huawei.alliance.commons.config.ReadPropertyCfg">
<property name="ignoreResourceNotFound" value="true" />
<property name="locations">
<list>
<value>classpath:config/*.properties</value>
</list>
</property>
</bean>
1
2
3
4
5
6
7
8
配置项使用场景有两种使用方法
1)配置文件中使用
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
1
2
3
4
5
6
2)在代码中通过@Value使用,例如
@Value("${jdbc.password}")
private String password;
1
2
注:spring容器中最多只能定义一个context:property-placeholder,否则会报错:Could not resolve placeholder XXX,但如果想引入多个属性文件怎么办那,可以使用通配符:<context:property-placeholder location=“classpath:*.properties”/>
代码中获取配置属性
1.需要自定义ReadPropertyCfg类继承PropertyPlaceholderConfigurer,重写processProperties方法,通过map来进行承接配置项
public class ReadPropertyCfg extends PropertyPlaceholderConfigurer implements InitializingBean {
private static Map<String, String> ctxPropertiesMap =
new HashMap<String, String>(16);
/**
* 通过重写processProperties方法把Properties属性写入到Map中,然后供后续使用
/
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
throws BeansException {
super.processProperties(beanFactoryToProcess, props);
String keyStr = “”;
String value = “”;
for (Object key : props.keySet()) {
keyStr = key.toString();
if (null != keyStr && null != props.getProperty(keyStr)) {
value = props.getProperty(keyStr);
ctxPropertiesMap.put(keyStr, value);
}
}
}
/*
* 通过获取map中的属性的key获取对应的value
*/
public static String getContextProperty(String name) {
return ctxPropertiesMap.get(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
4.源码分析
那现在疑问就来了,我们自定义PropertyPlaceholderConfigurer或者直接使用PropertyPlaceholderConfigurer,Spring是如何加载配置项的哪?
我们通过自定义的ReadPropertyCfg 反向进行分析Properties加载过程
从反向可以推导出最终是AbstractApplicationContext中的refresh触发的,具体的在这不进行黏贴,可根据上述调用关系在Spring源码中进行查看即可。
下面对重要的代码进行重点分析
PropertyResourceConfigurer.java
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
try {
//1)加载配置文件到Properties中
Properties mergedProps = mergeProperties();
//2)转化Properties进行重新提交,重写该方法可以对配置项进行处理,例如解密
convertProperties(mergedProps);
//3)把Properties加载到spring容器中替换BeanDefinition中${}对应的属性
processProperties(beanFactory, mergedProps);
}
catch (IOException ex) {
throw new BeanInitializationException("Could not load properties", ex);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
由上述代码可知配置文件从加载到最终加载Spring容器中,可分为三步
1)加载配置文件到Properties中
/**
* Return a merged Properties instance containing both the
* loaded properties and properties set on this FactoryBean.
*/
protected Properties mergeProperties() throws IOException {
Properties result = new Properties();
if (this.localOverride) {
// Load properties from file upfront, to let local properties override.
loadProperties(result);
}
if (this.localProperties != null) {
for (Properties localProp : this.localProperties) {
CollectionUtils.mergePropertiesIntoMap(localProp, result);
}
}
if (!this.localOverride) {
// Load properties from file afterwards, to let those properties override.
loadProperties(result);
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2)转化Properties进行重新提交,重写该方法可以对配置项进行处理,例如解密
PropertyResourceConfigurer的convertProperties方法
protected void convertProperties(Properties props) {
Enumeration<?> propertyNames = props.propertyNames();
while (propertyNames.hasMoreElements()) {
String propertyName = (String) propertyNames.nextElement();
String propertyValue = props.getProperty(propertyName);
String convertedValue = convertProperty(propertyName, propertyValue);
if (!ObjectUtils.nullSafeEquals(propertyValue, convertedValue)) {
props.setProperty(propertyName, convertedValue);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
在上述代码中如果没有重写convertProperty方法,propertyValue不发生变化,还是获取原始配置,源码如下
protected String convertProperty(String propertyName, String propertyValue) {
return convertPropertyValue(propertyValue);
}
protected String convertPropertyValue(String originalValue) {
return originalValue;
}
1
2
3
4
5
6
重写convertProperty方法
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if (null != propertyName && (encryptSet.contains(propertyName))) {
String pwdresult = AESCryptor.decryptData(propertyValue, CipherConfig.getWorkCipherKey("mysql.workkey"));
return pwdresult;
}
return propertyValue;
}
1
2
3
4
5
6
7
8
在执行convertProperty代码之前Properties已经有了所有的配置项,只不过是配置项中原生的,重写convertProperty就是为了把需要解密的数据进行解析,把需要解密的数据解密后进行重新设置Properties的对应属性达到解密的目的
3)把Properties加载到spring容器中替换BeanDefinition中${}对应的属性
代码或者配置文件已经被加载Spring容器中对应BeanDefinition的属性,只不过现在为止还是以${}的形式存在,processProperties的过程就是把BeanDefinition的对应属性替换为对应Properties中解密后的value
/**
* Visit each bean definition in the given bean factory and attempt to replace ${...} property
* placeholders with values from the given properties.
*/
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
throws BeansException {
StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props);
doProcessProperties(beanFactoryToProcess, valueResolver);
}