SpringBoot项目配置信息大多使用@Value注解或者@ConfigurationProperties注解读取配置信息,线上项目经常需要对某些配置进行调整,如果每次都需要修改配置文件再重新发布服务,难免会导致服务中断。
尤其是在分布式系统中多个服务节点都需要修改配置文件的场景,基于此配置中心也应运而生。
如果我们的项目使用了SpringCloud,那么可选的配置中心有很多,比如Nacos、spring-cloud-starter-config、Apollo等,这些配置中心都需要借助SpringCloud架构才能实现配置刷新。
这里我们的应用没有集成SpringCloud,也不想因为配置中心而让应用架构变重,所以需要基于SpringBoot基础实现一个轻量级的配置动态刷新功能。
远程配置中心
对配置资源进行管理,所有应用连接配置中心读取配置信息,可选用的配置中心包括:
1、配置中心中间件,例如:Nacos;
2、数据库存储;
3、Git仓库;
4、文件系统;
5、其它具备持久化存储及访问功能的中间件。
说点听得懂的实现原理
想要实现配置动态刷新可以从Spring的Bean初始化和属性值注入原理入手,这里我们跳过原理分析阶段,感兴趣的同学自行百度,建议大家可以直接看下Spring的PropertyPlaceholderAutoConfiguration类。
刷新配置信息仅需简单两步:
1、刷新Spring的Environment环境变量;
2、刷新Spring托管的Bean实例的属性值;
刷新Environment
* 刷新环境变量
*
@param
properties
private
MutablePropertySources refreshEnvironment(Properties properties) {
Map
<String, Object> props =
new
HashMap<>
();
properties.stringPropertyNames().stream().forEach(key
->
props.put(key, properties.getProperty(key))
//
获取spring的environment
MutablePropertySources mutablePropertySources =
environment.getPropertySources();
//
添加远程配置信息
mutablePropertySources.addFirst(
new
MapPropertySource("remoteConfig"
, props));
return
mutablePropertySources;
刷新Bean实例
需要注意一下几点:
1、我们不用自己解析@Value的value,通过Spring提供的PropertySourcesPropertyResolver.resolveRequiredPlaceholders即可从环境变量中获取对应的属性值;
2、@Value可以使用EL表达式,注入的属性类型可以是String、List等对象,通过Spring提供的SpelExpressionParser类实现EL表达式解析和运算取值。
* 刷新Bean实例的属性值
*
@param
bean
*
@param
propertyResolver
private
void
refreshBean(Object bean, ConfigurablePropertyResolver propertyResolver) {
//
定义EL表达式解释器
SpelExpressionParser spelExpressionParser;
spelExpressionParser
=
new
SpelExpressionParser();
TemplateParserContext templateParserContext;
templateParserContext
=
new
TemplateParserContext();
String keyResolver, valueResolver
=
null
;
Object parserValue;
//
遍历Bean实例所有属性
for
(Field field : bean.getClass().getDeclaredFields()) {
//
判断field是否含有@Value注解
if
(field.isAnnotationPresent(Value.
class
)) {
//
读取Value注解占位符
keyResolver = field.getAnnotation(Value.
class
).value();
try
{
//
读取属性值
valueResolver =
propertyResolver.resolveRequiredPlaceholders(keyResolver);
//
EL表达式解析
//
兼容形如:@Value("#{'${url}'.split(',')}")含有EL表达式的情况
parserValue =
spelExpressionParser.parseExpression(valueResolver, templateParserContext).getValue(field.getType());
}
catch
(IllegalArgumentException e) {
log.warn(
"{}"
, e.getMessage());
continue
;
//
判断配置项是否存在
if
(Objects.nonNull(valueResolver)) {
field.setAccessible(
true
);
try
{
field.set(bean, parserValue);
continue
;
}
catch
(IllegalAccessException e) {
log.error(
"{}刷新属性值出错, bean: [{}], field: [{}], value: [{}]"
,
TAG, bean.getClass().getName(), field.getName(), valueResolver);