【Spring】资源注入整合 及 Properties相互依赖的解决方案
来源:互联网 发布:js 浏览器窗口大小 编辑:程序博客网 时间:2024/05/21 07:15
前言
话说作为实习生刚入职公司,什么业务啊公司框架啊什么都不懂,所以领导在头几天就吩咐我先看看公司的项目架构,先熟悉一下,以便尽快融入团队,所以前几天我一直在公司划水,即一边看代码,一边看书,颇有几分光拿钱不干活的样子。话说前天下午,领导看我这么闲,好吧,那就先打打杂,实现个小需求,我惊,但还是认真听了领导的需求。
原先项目的配置文件有很多类型(但功能是一样的),比如说配置文件有:开发版(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/resources</directory> <includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes> <!--让 maven 在编译时为上面的配置文件注入配置,即 ${} 的配置--> <filtering>true</filtering> </resource> </resources></build><profiles> <profile> <!-- 默认激活开发配制 --> <activation> <activeByDefault>true</activeByDefault> </activation> <id>dev</id> <build> <filters> <filter>../dev.properties</filter> </filters> </build> </profile></profiles>
2.dev.properties
# ip.properties 位置,可以放在公共的位置,这里方便测试,写在类路径下ip.config.path=classpath:ip.properties#zookeeper addresszookeeper.address=1.1.1.1:2181#请求地址comp.gateway.req_url=http://2.2.2.2/compcomp.online.req_url=http://3.3.3.3/comp#dubbo configdubbo.protocol=dubbodubbo.protocol.port=20883#jdbc configjdbc.driverClassName=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://3.3.3.3:3306/paygentjdbc.username=AF3E3930394523504AC5F4BF53328636jdbc.password=9674806013AE8945DF960D2C2548768A
3.invoke.properties
#zookeeper transzookeeper.address.trans=${zookeeper.address}/trans#dubbo configdubbo.protocol=${dubbo.protocol}dubbo.protocol.port=${dubbo.protocol.port}#回调地址comp.online.callback=${comp.online.req_url}/callback
4.application-context.xml
<?xml version="1.0" encoding="UTF-8"?><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
由以下配置实现:
<util:properties id="settings" location="classpath:invoke.properties"/>
等价于:
<bean id="settings" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="locations" value="classpath:invoke.properties"/> </bean>
新的实现
新的实现需要将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
配置中,<util:properties id="settings" location="classpath:invoke.properties"/>
的实现就是PropertiesFactoryBean
;
3.PropertiesFactoryBean
解决SpEL
,PropertyPlaceholderConfigurer
解决占位符;
4.路还很长啊,api
要多熟悉,码到用时方恨少这样可不行;
引用
源码(master是最新实现,old分支是旧的实现):https://github.com/pingcai/properties-demo
- 【Spring】资源注入整合 及 Properties相互依赖的解决方案
- SSH整合,SPRING注入失败解决方案
- spring注入properties类型的配置文件信息
- Spring注入.properties文件
- Spring 注入properties文件
- Spring 注入 Properties
- Spring - 资源文件properties的配置
- Spring 资源文件 properties 的配置
- Spring - 资源文件properties的配置
- Spring - 资源文件properties的配置
- Spring - 资源文件properties的配置
- Spring 读取 properties 资料的解决方案
- Spring整合XFire的注入问题
- Spring整合XFire的注入问题
- WPF中当资源之间相互依赖(xaml资源与类对象资源相互依赖)时,常见无法加载的问题
- spring注入properties属性配置
- Spring自动注入properties文件
- Spring自动注入properties文件
- MyEclipse常用快捷键
- Error:Can't load library: C:\Users\Administrator.gradle\native\23\windows-i386\native-platform.dll
- mysql 如何按照索引横向拼接两张表的字段, 利用create table as, join on
- 拷贝构造和赋值语句
- 网络基础
- 【Spring】资源注入整合 及 Properties相互依赖的解决方案
- 冒泡法(nodejs)
- 连接DB2报错:ERRORCODE=-4499, SQLSTATE=08004
- 感染者(并查集)
- ssh 文件传输
- 函数的内部属性2
- SAEA,SoketAsyncEventArgs写的处理函数在高负载时出莫名其妙的错误
- 33 款主宰 2017 iOS 开发的开源库
- HTML课堂讲义(4)