起源:https://zhenbianshu.github.io
需要
人不知; 鬼不觉,web 开发曾经进入“微服务”、”分布式”的时代,致力于提供通用 Java 开发解决方案的 Spring 天然不甘人后,提出了 Spring Cloud 来扩充 Spring 在微服务方面的影响,也获得了市场的认可,在咱们的业务中也有利用。
前些天,我在一个需要中也遇到了 spring cloud 的相干问题。咱们在用的是 Spring Cloud 的 config 模块,它是用来反对分布式配置的,原来单机配置在应用了 Spring Cloud 之后,能够反对第三方存储配置和配置的动静批改和从新加载,本人在业务代码里实现配置的从新加载,Spring Cloud 将整个流程抽离为框架,并很好的融入到 Spring 原有的配置和 Bean 模块内。
尽管在解决需要问题时走了些弯路,但也借此机会理解了 Spring Cloud 的一部分,抽空总结一下问题和在查问问题中理解到的常识,分享进去让再遇到此问题的同学少踩坑吧。
本文基于 Spring 5.0.5、Spring Boot 2.0.1 和 Spring Cloud 2.0.2。
背景和问题
咱们的服务原来有一批单机的配置,因为同一 key 的配置太长,于是将其配置为数组的模式,并应用 Spring Boot 的 @ConfigurationProperties
和 @Value
注解来解析为 Bean 属性。
Spring Boot 基础教程就不介绍了,不会的看这里的示例教程和源码:https://github.com/javastacks…
properties 文件配置像:
test.config.elements[0]=value1
test.config.elements[1]=value2
test.config.elements[2]=value3
在应用时:
@ConfigurationProperties(prefix="test.config")
Class Test{@Value("${#elements}")
private String[] elements;}
这样,Spring 会对 Test 类主动注入,将数组 [value1,value2,value3] 注入到 elements 属性内。
而咱们应用 Spring Cloud 主动加载配置的姿态是这样:
@RefreshScope
class Test{@Value("${test.config.elements}")
private String[] elements;}
应用 @RefreshScope
注解的类,在环境变量有变动后会主动从新加载,将最新的属性注入到类属性内,但它却不反对数组的主动注入。
而我的指标是能找到一种形式,使其即反对注入数组类型的属性,又能应用 Spring Cloud 的主动刷新配置的个性。
环境和属性
无论 Spring Cloud 的个性如何优良,在 Spring 的地盘,还是要入乡随俗,和 Spring 的根底组件打成一片。所以为了理解整个流程,咱们就要先理解 Spring 的根底。
Spring 是一个大容器,它不光存储 Bean 和其中的依赖,还存储着整个利用内的配置,绝对于 BeanFactory 存储着各种 Bean,Spring 治理环境配置的容器就是 Environment
,从 Environment 内,咱们能依据 key 获取所有配置,还能依据不同的场景(Profile,如 dev,test,prod)来切换配置。
但 Spring 治理配置的最小单位并不是属性,而是 PropertySource
(属性源),咱们能够了解 PropertySource 是一个文件,或是某张配置数据表,Spring 在 Environment 内保护一个 PropertySourceList,当咱们获取配置时,Spring 从这些 PropertySource 内查找到对应的值,并应用 ConversionService
将值转换为对应的类型返回。
Spring Cloud 配置刷新机制
分布式配置
Spring Cloud 内提供了 PropertySourceLocator
接口来对接 Spring 的 PropertySource 体系,通过 PropertySourceLocator,咱们就拿到一个”自定义”的 PropertySource,Spring Cloud 里还有一个实现 ConfigServicePropertySourceLocator
,通过它,咱们能够定义一个近程的 ConfigService,通过专用这个 ConfigService 来实现分布式的配置服务。
从 ConfigClientProperties
这个配置类咱们能够看得出来,它也为近程配置预设了用户名明码等安全控制选项,还有 label 用来辨别服务池等配置。
scope 配置刷新
近程配置有了,接下来就是对变动的监测和基于配置变动的刷新。
Spring Cloud 提供了 ContextRefresher
来帮忙咱们实现环境的刷新,其次要逻辑在 refreshEnvironment
办法和 scope.refreshAll()
办法,咱们离开来看。
咱们先来看 spring cloud 反对的 scope.refreshAll 办法。
public void refreshAll() {super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
scope.refreshAll 则更”横蛮”一些,间接销毁了 scope,并公布了一个 RefreshScopeRefreshedEvent 事件,scope 的销毁会导致 scope 内(被 RefreshScope 注解)所有的 bean 都会被销毁。而这些被强制设置为 lazyInit 的 bean 再次创立时,也就实现了新配置的从新加载。
ConfigurationProperties 配置刷新
而后再回过头来看 refreshEnvironment 办法。
Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
return keys;
它读取了环境内所有 PropertySource 内的配置后,从新创立了一个 SpringApplication 以刷新配置,再次读取所有配置项并失去与后面保留的配置项的比照,最初将前后配置差公布了一个 EnvironmentChangeEvent
事件。而 EnvironmentChangeEvent 的监听器是由 ConfigurationPropertiesRebinder 实现的,其次要逻辑在 rebind
办法。
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
return true;
能够看到它的解决逻辑,就是把其外部存储的 ConfigurationPropertiesBeans
顺次执行销毁逻辑,再执行初始化逻辑实现属性的从新绑定。
这里能够晓得,Spring Cloud 在进行配置刷新时是思考过 ConfigurationProperties 的,通过测试,在 ContextRefresher 刷新上下文后,ConfigurationProperties 注解类的属性是会进行动静刷新的。
测试一次就解决的事件,感觉有些白忙活了。。
不过既然查到这里了,就再往下深刻一些。
Bean 的创立与环境
接着咱们再来看一下,环境里的属性都是怎么在 Bean 创立时被应用的。
咱们晓得,Spring 的 Bean 都是在 BeanFactory 内创立的,创立逻辑的入口在 AbstractBeanFactory.doGetBean(name, requiredType, args, false)
办法,而具体实现在 AbstractAutowireCapableBeanFactory.doCreateBean
办法内,在这个办法里,实现了 Bean 实例的创立、属性填充、初始化办法调用等逻辑。
在这里,有一个非常复杂的步骤就是调用全局的 BeanPostProcessor
,这个接口是 Spring 为 Bean 创立筹备的勾子接口,实现这个接口的类能够对 Bean 创立时的操作进行批改。它是一个十分重要的接口,是咱们能干预 Spring Bean 创立流程的重要入口。
咱们要说的是它的一种具体实现 ConfigurationPropertiesBindingPostProcessor
,它通过调用链 ConfigurationPropertiesBinder.bind() --> Binder.bindObject() --> Binder.findProperty()
办法查找环境内的属性。
private ConfigurationProperty findProperty(ConfigurationPropertyName name,
Context context) {if (name.isEmpty()) {return null;}
return context.streamSources()
.map((source) -> source.getConfigurationProperty(name))
.filter(Objects::nonNull).findFirst().orElse(null);
}
找到对应的属性后,再应用 converter 将属性转换为对应的类型注入到 Bean 骨。
private <T> Object bindProperty(Bindable<T> target, Context context,
ConfigurationProperty property) {context.setConfigurationProperty(property);
Object result = property.getValue();
result = this.placeholdersResolver.resolvePlaceholders(result);
result = context.getConverter().convert(result, target);
return result;
}
一种 trick 形式
由下面能够看到,Spring 是反对 @ConfigurationProperties 属性的动静批改的,但在查问流程时,我也找到了一种比拟 trick 的形式。
咱们先来整顿动静属性注入的关键点,再从这些关键点里找可批改点。
- PropertySourceLocator 将 PropertySource 从近程数据源引入,如果这时咱们能批改数据源的后果就能达到目标,可是 Spring Cloud 的近程资源定位器 ConfigServicePropertySourceLocator 和 近程调用工具 RestTemplate 都是实现类,如果僵硬地对其继承并批改,代码很不优雅。
- Bean 创立时会顺次应用 BeanPostProcessor 对上下文进行操作。这时增加一个 BeanPostProcessor,能够手动实现对 Bean 属性的批改。但这种形式 实现起来很简单,而且因为每一个 BeanPostProcessor 在所有 Bean 创立时都会调用,可能会有平安问题。
- Spring 会在解决类属性注入时,应用 PropertyResolver 将配置项解析为类属性指定的类型。这时候增加属性解析器 PropertyResolver 或类型转换器 ConversionService 能够插手属性的操作。但它们都只负责解决一个属性,因为我的指标是”多个”属性变成一个属性,它们也无能为力。
我这里能想到的形式是借用 Spring 主动注入的能力,把 Environment Bean 注入到某个类中,而后在类的初始化办法里对 Environment 内的 PropertySource 里进行批改,也能够达成目标,这里贴一下伪代码。
@Component
@RefreshScope // 借用 Spring Cloud 实现此 Bean 的刷新
public class ListSupportPropertyResolver {
@Autowired
ConfigurableEnvironment env; // 将环境注入到 Bean 内是批改环境的重要前提
@PostConstruct
public void init() {
// 将属性键值对从环境内取出
Map<String, Object> properties = extract(env.getPropertySources());
// 解析环境里的数组,抽取出其中的数组配置
Map<String, List<String>> listProperties = collectListProperties(properties)
Map<String, Object> propertiesMap = new HashMap<>(listProperties);
MutablePropertySources propertySources = env.getPropertySources();
// 把数组配置生成一个 PropertySource 并放到环境的 PropertySourceList 内
propertySources.addFirst(new MapPropertySource("modifiedProperties", propertiesMap));
}
}
这样,在创立 Bean 时,就能第一优先级应用咱们批改过的 PropertySource 了。
当然了,有了比拟”正规”的形式后,咱们不必要对 PropertySource 进行批改,毕竟全局批改等于未知危险或埋坑。
小结
查找答案的过程中,我更粗浅地了解到 Environment、BeanFactory 这些才是 Spring 的基石,框架提供的各种花式性能都是基于它们实现的,对这些常识的把握,对于了解它体现进去的高级个性很有帮忙,之后再查找框架问题也会更有方向。
近期热文举荐:
1.600+ 道 Java 面试题及答案整顿 (2021 最新版)
2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!
3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!