关于 SpringCloud 配置,你了解多少?
来源: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 属性。
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 {@AutowiredConfigurableEnvironment env; // 将环境注入到 Bean 内是修改环境的重要前提@PostConstructpublic 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 的基石,框架提供的各种花式功能都是基于它们实现的,对这些知识的掌握,对于理解它表现出来的高级特性很有帮助,之后再查找框架问题也会更有方向。
推荐好文
>>【练手项目】基于SpringBoot的ERP系统,自带进销存+财务+生产功能>>分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
>>能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!
关于 SpringCloud 配置,你了解多少?相关推荐
- SpringCloud配置中心内容加密
转载自 SpringCloud配置中心内容加密 从配置获取的配置默认是明文的,有些像数据源这样的配置需要加密的话,需要对配置中心进行加密处理. 下面使用对称性加密来加密配置,需要配置一个密钥,当然也可 ...
- SpringCloud配置中心客户端读取配置
转载自 SpringCloud配置中心客户端读取配置 微服务连接配置中心来实现外部配置的读取. 引入依赖 <dependencies><dependency><group ...
- Nacos教程_3 整合SpringCloud(配置中心+服务发现)
教程原稿 https://gitee.com/fakerlove/joker-nacos 文章目录 3. 整合SpringCloud(配置中心+服务发现) 3.1 写配置 3.2 创建父工程demo ...
- SpringBoot和SpringCloud配置
SpringBoot和SpringCloud配置 一.springBoot配置 1.1.yml 1.2.pom 1.3.RestTemplate 二.注册中心 Spring Cloud Eureka ...
- SpringCloud配置中心Nacos
SpringCloud配置中心Nacos 一.配置中心介绍 1.Spring Cloud Config 2.Nacos替换Config 二.读取Nacos配置中心的配置文件 1.在Nacos创建统一配 ...
- SpringCloud配置中心-Config
本文主要讨论原理,不涉及使用示例. 一 搭建Config Server SpringCloud Config支持通过git.svn等搭建配置中心.因为目前使用git管理代码比较常见,所以接下来介绍通过 ...
- 【过程记录】springcloud配置使用Eureka作服务发现组件并进行微服务注册
Eureka介绍 工具准备 步骤 编写eureka server 编写微服务应用: 查询指定微服务在Eureka上的实例列表 Eureka的自我保护模式 Eureka介绍 Eureka是Netflix ...
- SpringCloud 配置安全验证、服务消费端处理、无状态 Session 配置、定义公共安全配置程序类
所有的 Rest 服务最终都是暴露在公网上的,也就是说如果你的 Rest 服务属于一些你自己公司的私人业务,这样的结果会直接 导致你信息的泄漏,所以对于 Rest 访问,安全性是首要的因素. 2.1. ...
- springcloud配置动态更新
在实现springcloud的配置中心后,我们需要考虑的就是动态刷新配置.如果考虑只有一个客户端,我们可以在配置文件发生push操作的时候添加webhook,使用webhook发送刷新的post请求到 ...
- 2.springcloud配置ssh
引用:75_Config配置总控中心搭建Spring Cloud 学习笔记(2 / 3)_KISS-CSDN博客Spring Cloud 学习笔记(1 / 3)Spring Cloud 学习笔记(3 ...
最新文章
- laravel5.8的使用
- JAVA游戏编程之二----j2me MIDlet 手机游戏入门开发--贪吃蛇
- go语言笔记——指针,和C用法以及本质一样,但不支持指针的+-运算!
- Objective-C KVC
- Android — 使用 SharedPreferences 本地保存 key-value 数据
- Spring boot的Hello World入门
- 【2012百度之星/初赛上】D:轮子上的度度熊
- mysql 机器复制_MySQL复制在同一台机器上
- jpa使用注解传递对象_注解(下)
- Gulp快速入门教程 1
- python可迭代对象和迭代器的理解
- [知乎] 开放世界游戏中的大地图背后有哪些实现技术?
- 三丰三坐标编程基本步骤_广州电子OEM加工的SMT贴片编程
- 1000瓶毒药里有1瓶有毒,问需要多少只老鼠能试出来哪瓶有毒
- 徐鹤宁语录【销售篇】
- 隐马尔可夫之前向算法
- 九亿少女的梦(python信息处理)
- Label Smoothing 标签平滑 (Label smooth regularization, LSR)
- 拥有数据报表模板,不再愁如何制作数据报表
- kafka中的配额管理(限速)机制