1 什是分布式配置中心
其实就是把一些配置的信息分离于自身的系统,而这些信息又能被应用实时获取得到。这里用springboot 举例子,我们都知道springboot 启动的时候,会加载resource 目录下面的application.properties 或者 application.yml。 这个时候我们把springboot 启动的时候所需要加载的配置文件 不和工程放在一起,统一管理,这个就是分布式配置中心的核心思想。
1.1 分布式配置中心有哪些组成
1.1.1, 有一个界面能操作配置
1.1.2, 数据能够持久化(防止丢失,服务下线在启动配置还是存在的)
1.1.3, 存在客户端和服务端, 客户端主动去拉去数据或者服务端主动推送数据。 并且刷新本机的配置。(核心)
1.1.4, 一些管理界面的操作日志, 权限系统等。
2,市面上主流的配置中心
2.1 阿里的 nacos
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施. 具体使用请看官网
2.2 nacos 的原理,就是service 监控配置是否发生改变,通过长链接在发送给客户端
架构原理图如下 ,这个是推送模式, 但是是基于长链接
2.3 携程的Apollo
Apollo是携程框架部研发并开源的一款生产级的配置中心产品,它能够集中管理应用在不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。具体使用请看官网
携程的架构原理图也是和 Nacos 的是一样的, 也是长链接轮询, 服务端推送的模式。
2.3 spirgcloud config
springCloudConfig,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git/svn仓库中。
服务启动的时候config Service 会从远程git拉取配置文件,并存入到本地git文件库,当远程git不可用时,会从本地git文件库拉取配置信息. 具体的使用请以官网为准。
2.4 百度disconf
Disconf是百度开源出来的一款基于Zookeeper的分布式配置管理软件。目前很多公司都在使用,包括滴滴、百度、网易、顺丰等公司。通过简单的界面操作就可以动态修改配置属性,还是很方便的。使用Disconf后发现的一大好处是省却应用很多配置,而且配置可以自动load,实时生效。
基本原理图:当然具体的肯定要比这个更加的复杂, 这个只是主要的流程。
3如何实现自己的分布式配置中心
3.1 动态修改本地@Value注解的配置
3.2 在不同的bean 中, 相同的value 怎么同时修改。
4 具体思路
怎么动态修改@Value注解 的配置, 我们就要知道springboot 怎么加载application.ym 或者application.properties 文件的。
我这里用springboot2.1.1 的代码做示范。
首先打开
找到spring.factors 的配置文件
我们看到上面三个就是配置文件的加载器, 这里也显示了为啥properties 的优先级比yaml 的优先级高。 他是从上往下的顺序排列的啊。 但是真正的配置文件执行的还是下面的
我们点击去看一下实现类
ConfigFileApplicationListene
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
//怎么加载资源
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
// 把资源给load 到环境变量里面
new Loader(environment, resourceLoader).load();
}
// 再用 propertySource 解析器给解析
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(
this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoade
: new DefaultResourceLoader();
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
PropertySourceLoader.class, getClass().getClassLoader());
}
那我们也就知道了,也就是我们要是能 EnvironmentPostProcessor ,在重写里面的方法也就可以动态的加载配置文件了。 下面我们就开始代码实现.
4.1 代码实现
那既然是这样我们就可以实现EnvironmentPostProcessor 这个类通过接口动态的加载配置文件了。
那下面就是具体的代码实现
@Autowired
ConfigurableEnvironment configurableEnvironment;
@Autowired
Environment environment;
@Test
public void test() {
String name = environment.getProperty("name");
System.out.printf("动态加载之前" +name);
Map<String,String> map = new HashMap<>();
map.put("name","嘟嘟");
configurableEnvironment.getPropertySources().addLast(
new OriginTrackedMapPropertySource("xxxx.xml", map)
);
String property = environment.getProperty("name");
System.out.printf("动态加载之后" +property);
}
4.1.2 单元测试
最终的结果是
我们现在解决了第一个问题, 怎么动态增加环境变量
第二个问题在@value 注解上使用的怎么动态刷新啊。
那这个使用我们就需要ConfigurablePropertyResolver 这个类,来解析这个key , 在 找到@value 对应的bean 通过反射来刷新
具体代码
4.2 代码实现
public static void refreshBean(Object bean, ConfigurablePropertyResolver propertyResolver) {
// 定义EL表达式解释器
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
TemplateParserContext templateParserContext= new TemplateParserContext();
String keyResolver, valueResolver = null;
Object parserValue;
// 获取真实对象属性
Field[] declaredFields = bean.getClass().getDeclaredFields();
boolean cglib = Arrays.stream(declaredFields).anyMatch(x -> x.getName().contains("CGLIB"));
// 如果是cglib 代理找其父类
if(cglib){
declaredFields = bean.getClass().getSuperclass().getDeclaredFields();
}
// 遍历Bean实例所有属性
for (Field field : declaredFields) {
// 判断field是否含有@Value注解
if (field.isAnnotationPresent(Value.class)) {
// 读取Value注解占位符
keyResolver = field.getAnnotation(Value.class).value();
try {
// 读取属性值
valueResolver = propertyResolver.resolveRequiredPlaceholders(keyResolver);
// EL表达式解析
// 兼容形如:@Value("#{'${codest.five.url}'.split(',')}")含有EL表达式的情况
Expression expression = spelExpressionParser.parseExpression(valueResolver, templateParserContext);
if(field.getType() == Boolean.class){
parserValue =Boolean.valueOf(expression.getValue().toString());
}
else if(field.getType() == Integer.class){
parserValue =Integer.valueOf(expression.getValue().toString());
}
else if(field.getType() == Long.class){
parserValue =Long.valueOf(expression.getValue().toString());
}else {
parserValue = expression.getValue(field.getType());
}
} catch (IllegalArgumentException e) {
continue;
}
// 判断配置项是否存在
if (Objects.nonNull(valueResolver)) {
field.setAccessible(true);
try {
field.set(bean, parserValue);
continue;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
我们在写一个单元测试测试
4.2.1 单元测试
@Autowired
ConfigurableEnvironment configurableEnvironment;
@Autowired
ConfigurablePropertyResolver configurablePropertyResolver;
@Autowired
Person person;
@Test
public void test() {
System.out.printf("动态加载之前" +person.getName());
Map<String,Object> map = new HashMap<>();
map.put("name","嘟嘟");
configurableEnvironment.getPropertySources().forEach( x->{
if (x instanceof OriginTrackedMapPropertySource ) {
Map<String,Object> map1 = (Map<String, Object>) x.getSource();
map1.putAll(map);
}
}
);
refreshBean(person,configurablePropertyResolver);
System.out.printf("动态加载之后" +person.getName());
}
最后结果:
完美, 下一期我们在解决 多个bean, @value 的值一样怎么同时刷新。