相信大家都知道,每个项目中会有一些配置信息放在一个独立的properties文件中,比如application.properties。这个文件中会放一些常量的配置,比如数据库连接信息、线程池大小、限流参数。

在传统的开发模式下,这种方式很方便,一方面能够对配置进行统一管理,另一方面,我们在维护的时候很方便。

但是随着业务的发展以及架构的升级,在微服务架构中,服务的数量以及每个服务涉及到的配置会越来越多,并且对于配置管理的需求越来越高,比如要求实时性、独立性。

另外,在微服务架构下,会涉及到不同的环境下的配置管理、灰度发布、动态限流、动态降级等需求,包括对于配置内容的安全与权限,所以传统的配置维护方式很难达到需求。

因此,就产生了分布式配置中心。

  • 传统的配置方式不方便维护
  • 配置内容的安全和访问权限,在传统的配置方式中很难实现
  • 更新配置内容时,需要重启

配置中心的工作流程

图11-1

Spring Boot的外部化配置

在本次课程中,我们会Zookeeper集成到Spring Boot的外部化配置中,让用户无感知的使用配置中心上的数据作为数据源,所以我们需要先了解Spring Boot中的外部化配置。

Spring Boot的外部化配置是基于Environment来实现的,它表示Spring Boot应用运行时的环境信息,先来看基本使用

Environment的使用

  • 在spring boot应用中,修改aplication.properties配置

    key=value
  • 创建一个Controller进行测试

    @RestController
    public class EnvironementController { @Autowired
    Environment environment; @GetMapping("/env")
    public String env(){
    return environment.getProperty("key");
    }
    }

@Value注解使用

在properties文件中定义的属性,除了可以通过environment的getProperty方法获取之外,spring还提供了@Value注解,

@RestController
public class EnvironementController { @Value("${env}")
private String env; @GetMapping("/env")
public String env(){
return env;
}
}

spring容器在加载一个bean时,当发现这个Bean中有@Value注解时,那么它可以从Environment中将属性值进行注入,如果Environment中没有这个属性,则会报错。

Environment设计猜想

Spring Boot的外部化配置,不仅仅只是appliation.properties,包括命令行参数、系统属性、操作系统环境变量等,都可以作为Environment的数据来源。

  • @Value("${java.version}") 获取System.getProperties , 获取系统属性
  • 配置command的jvm参数, -Denvtest=command ,然后通过@Value("${envtest}")

图11-2

  • 第一部分是属性定义,这个属性定义可以来自于很多地方,比如application.properties、或者系统环境变量等。
  • 然后根据约定的方式去指定路径或者指定范围去加载这些配置,保存到内存中。
  • 最后,我们可以根据指定的key从缓存中去查找这个值。

扩展Environment

我们可以自己扩展Environment中的数据源,代码如下;

其中,EnvironmentPostProcessor:它可以在spring上下文构建之前可以设置一些系统配置。

CusEnvironmentPostProcessor

public class CusEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final Properties properties=new Properties();
private String propertiesFile="custom.properties"; @Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Resource resource=new ClassPathResource(propertiesFile);
environment.getPropertySources().addLast(loadProperties(resource));
} private PropertySource<?> loadProperties(Resource resource){
if(!resource.exists()){
throw new IllegalArgumentException("file:{"+resource+"} not exist");
}
try {
properties.load(resource.getInputStream());
return new PropertiesPropertySource(resource.getFilename(),properties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

custom.properties

在classpath目录下创建custom.properties文件

name=mic
age=18

spring.factories

在META-INF目录下创建spring.factories文件,因为EnvironmentPostProcessor的扩展实现是基于SPI机制完成的。

org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.springbootzookeeper.CusEnvironmentPostProcessor

TestController

创建测试类,演示自定义配置加载的功能。

@RestController
public class TestController { @Value("${name}")
public String val; @GetMapping("/")
public String say(){
return val;
}
}

总结

通过上面的例子我们发现,在Environment中,我们可以通过指定PropertySources来增加Environment外部化配置信息,使得在Spring Boot运行期间自由访问到这些配置。

那么我们要实现动态配置中心,无非就是要在启动的时候,从远程服务器上获取到数据保存到PropertySource中,并且添加到Environment。

下面我们就开始来实现这个过程。

Zookeeper实现配置中心

在本小节中,主要基于Spring的Environment扩展实现自己的动态配置中心,代码结构如图11-3所示。

图11-3

自定义配置中心的相关说明

在本次案例中,我们并没有完全使用EnvironmentPostProcessor这个扩展点,而是基于SpringFactoriesLoader自定义了一个扩展点,主要目的是让大家知道EnvironmentPostProcessor扩展点的工作原理,以及我们以后自己也可以定义扩展点。

代码实现

以下是所有代码的实现过程,按照下面这个步骤去开发即可完成动态配置中心。

ZookeeperApplicationContextInitializer

ApplicationContextInitializer扩展,它是在ConfigurableApplicationContext通过调用refresh函数来初始化Spring容器之前会进行回调的一个扩展方法,我们可以在这个扩展中实现Environment的扩展。

所以这个类的主要作用就是在ApplicationContext完成refresh之前,扩展Environment,增加外部化配置注入。

public class ZookeeperApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
//PropertySourceLocator接口支持扩展自定义配置加载到spring Environment中。
private final List<PropertySourceLocator> propertySourceLocators; public ZookeeperApplicationContextInitializer(){
//基于SPI机制加载所有的外部化属性扩展点
ClassLoader classLoader=ClassUtils.getDefaultClassLoader();
//这部分的代码是SPI机制
propertySourceLocators=new ArrayList<>(SpringFactoriesLoader.loadFactories(PropertySourceLocator.class,classLoader));
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//获取运行的环境上下文
ConfigurableEnvironment environment=applicationContext.getEnvironment();
//MutablePropertySources它包含了一个CopyOnWriteArrayList集合,用来包含多个PropertySource。
MutablePropertySources mutablePropertySources = environment.getPropertySources();
for (PropertySourceLocator locator : this.propertySourceLocators) {
//回调所有实现PropertySourceLocator接口实例的locate方法,收集所有扩展属性配置保存到Environment中
Collection<PropertySource<?>> source = locator.locateCollection(environment,applicationContext);
if (source == null || source.size() == 0) {
continue;
}
//把PropertySource属性源添加到environment中。
for (PropertySource<?> p : source) {
//addFirst或者Last决定了配置的优先级
mutablePropertySources.addFirst(p);
}
}
}
}

创建classpath:/META-INF/spring.factories

org.springframework.context.ApplicationContextInitializer=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperApplicationContextInitializer

PropertySourceLocator

PropertySourceLocator接口支持扩展自定义配置加载到spring Environment中。

public interface PropertySourceLocator {

    PropertySource<?> locate(Environment environment,ConfigurableApplicationContext applicationContext);
//Environment表示环境变量信息
//applicationContext表示应用上下文
default Collection<PropertySource<?>> locateCollection(Environment environment, ConfigurableApplicationContext applicationContext) {
return locateCollection(this, environment,applicationContext);
} static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
Environment environment,ConfigurableApplicationContext applicationContext) {
PropertySource<?> propertySource = locator.locate(environment,applicationContext);
if (propertySource == null) {
return Collections.emptyList();
}
return Arrays.asList(propertySource);
}
}

ZookeeperPropertySourceLocator

ZookeeperPropertySourceLocator用来实现基于Zookeeper属性配置的扩展点,它会访问zookeeper获取远程服务器数据。

public class ZookeeperPropertySourceLocator implements PropertySourceLocator{

    private final CuratorFramework curatorFramework;

    private final String DATA_NODE="/data";  //仅仅为了演示,所以写死目标数据节点

    public ZookeeperPropertySourceLocator() {
curatorFramework= CuratorFrameworkFactory.builder()
.connectString("192.168.221.128:2181")
.sessionTimeoutMs(20000).connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("config").build();
curatorFramework.start();
} @Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
System.out.println("开始加载远程配置到Environment中");
CompositePropertySource composite = new CompositePropertySource("configService");
try {
Map<String,Object> dataMap=getRemoteEnvironment();
//基于Map结构的属性源
MapPropertySource mapPropertySource=new MapPropertySource("configService",dataMap);
composite.addPropertySource(mapPropertySource);
addListener(environment,applicationContext);
} catch (Exception e) {
e.printStackTrace();
}
return composite;
} private Map<String,Object> getRemoteEnvironment() throws Exception {
String data=new String(curatorFramework.getData().forPath(DATA_NODE));
//暂时支持json格式
ObjectMapper objectMapper=new ObjectMapper();
Map<String,Object> map=objectMapper.readValue(data, Map.class);
return map;
}
//添加节点变更事件
private void addListener(Environment environment, ConfigurableApplicationContext applicationContext){
NodeDataCuratorCacheListener curatorCacheListener=new NodeDataCuratorCacheListener(environment,applicationContext);
CuratorCache curatorCache=CuratorCache.build(curatorFramework,DATA_NODE,CuratorCache.Options.SINGLE_NODE_CACHE);
CuratorCacheListener listener=CuratorCacheListener
.builder()
.forChanges(curatorCacheListener).build();
curatorCache.listenable().addListener(listener);
curatorCache.start();
}
}

配置扩展点: classpath:/META-INF/spring.factories

com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator

配置动态变更逻辑

NodeDataCuratorCacheListener

NodeDataCuratorCacheListener用来实现持久化订阅机制,当目标节点数据发生变更时,需要收到变更并且应用。

public class NodeDataCuratorCacheListener implements CuratorCacheListenerBuilder.ChangeListener {
private Environment environment;
private ConfigurableApplicationContext applicationContext;
public NodeDataCuratorCacheListener(Environment environment, ConfigurableApplicationContext applicationContext) {
this.environment = environment;
this.applicationContext=applicationContext;
}
@Override
public void event(ChildData oldNode, ChildData node) {
System.out.println("数据发生变更");
String resultData=new String(node.getData());
ObjectMapper objectMapper=new ObjectMapper();
try {
Map<String,Object> map=objectMapper.readValue(resultData, Map.class);
ConfigurableEnvironment cfe=(ConfigurableEnvironment)environment;
MapPropertySource mapPropertySource=new MapPropertySource("configService",map);
cfe.getPropertySources().replace("configService",mapPropertySource);
//发布事件,用来更新@Value注解对应的值(事件机制可以分两步演示)
applicationContext.publishEvent(new EnvironmentChangeEvent(this));
System.out.println("数据更新完成");
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}

EnvironmentChangeEvent

定义一个环境变量变更事件。

public class EnvironmentChangeEvent extends ApplicationEvent {

    public EnvironmentChangeEvent(Object source) {
super(source);
}
}

ConfigurationPropertiesRebinder

ConfigurationPropertiesRebinder接收事件,并重新绑定@Value注解的数据,使得数据能够动态改变

@Component
public class ConfigurationPropertiesRebinder implements ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private Environment environment;
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans,Environment environment) {
this.beans = beans;
this.environment=environment;
} @Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
rebind();
}
public void rebind(){
this.beans.getFieldMapper().forEach((k,v)->{
v.forEach(f->f.resetValue(environment));
});
}
}

ConfigurationPropertiesBeans

ConfigurationPropertiesBeans实现了BeanPostPorocessor接口,该接口我们也叫后置处理器,作用是在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑。注意是Bean实例化完毕后及依赖注入完成后触发的。

我们可以在这个后置处理器的回调方法中,扫描指定注解的bean,收集这些属性,用来触发事件变更。

@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor { private Map<String,List<FieldPair>> fieldMapper=new HashMap<>(); @Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class clz=bean.getClass();
if(clz.isAnnotationPresent(RefreshScope.class)){ //如果某个bean声明了RefreshScope注解,说明需要进行动态更新
for(Field field:clz.getDeclaredFields()){
Value value=field.getAnnotation(Value.class);
List<String> keyList=getPropertyKey(value.value(),0);
for(String key:keyList){
//使用List<FieldPair>存储的目的是,如果在多个bean中存在相同的key,则全部进行替换
fieldMapper.computeIfAbsent(key,(k)->new ArrayList()).add(new FieldPair(bean,field,value.value()));
}
}
}
return bean;
}
//获取key信息,也就是${value}中解析出value这个属性
private List<String> getPropertyKey(String value,int begin){
int start=value.indexOf("${",begin)+2;
if(start<2){
return new ArrayList<>();
}
int middle=value.indexOf(":",start);
int end=value.indexOf("}",start);
String key;
if(middle>0&&middle<end){
key=value.substring(start,middle);
}else{
key=value.substring(start,end);
}
//如果是这种用法,就需要递归,@Value("${swagger2.host:127.0.0.1:${server.port:8080}}")
List<String> keys=getPropertyKey(value,end);
keys.add(key);
return keys;
} public Map<String, List<FieldPair>> getFieldMapper() {
return fieldMapper;
}
}

RefreshScope

定义注解来实现指定需要动态刷新类的识别。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshScope { }

FieldPair

这个类中主要通过PropertyPlaceholderHelper将字符串里的占位符内容,用我们配置的properties里的替换。

public class FieldPair {
private static PropertyPlaceholderHelper propertyPlaceholderHelper=new PropertyPlaceholderHelper("${","}",":",true);
private Object bean;
private Field field;
private String value; public FieldPair(Object bean, Field field, String value) {
this.bean = bean;
this.field = field;
this.value = value;
} public void resetValue(Environment environment){
boolean access=field.isAccessible();
if(!access){
field.setAccessible(true);
}
//从新从environment中将占位符替换为新的值
String resetValue=propertyPlaceholderHelper.replacePlaceholders(value,((ConfigurableEnvironment) environment)::getProperty);
try {
//通过反射更新
field.set(bean,resetValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

访问测试ConfigController

@RefreshScope
@RestController
public class ConfigController { @Value("${name}")
private String name; @Value("${job}")
private String job; @GetMapping
public String get(){
return name+":"+job;
}
}

基于自定义PropertySourceLocator扩展

由于在上述代码中,我们创建了一个PropertySourceLocator接口,并且在整个配置加载过程中,我们都是基于PropertySourceLocator扩展点来进行加载的,所以也就是意味着除了上述使用的Zookeeper作为远程配置装载以外,我们还可以通过扩展PropertySourceLocator来实现其他的扩展,具体实现如下

CustomPropertySourceLocator

创建一个MapPropertySource作为Environment的属性源。

public class CustomPropertySourceLocator implements PropertySourceLocator{

    @Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
Map<String, Object> source = new HashMap<>();
source.put("age","18");
MapPropertySource propertiesPropertySource = new MapPropertySource("configCenter",source);
return propertiesPropertySource;
}
}

spring.factories

由于CustomPropertySourceLocator是自定义扩展点,所以我们需要在spring.factories文件中定义它的扩展实现,修改如下

com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator,\
com.gupaoedu.example.zookeepercuratordemo.config.CustomPropertySourceLocator

ConfigController

接下来,我们通过下面的代码进行测试,从结果可以看到,我们自己定义的propertySource被加载到Environment中了。

@RefreshScope
@RestController
public class ConfigController { @Value("${name}")
private String name; @Value("${job}")
private String job; @Value("${age}")
private String age; @GetMapping
public String get(){
return name+":"+job+":"+age;
}
}

关注[跟着Mic学架构]公众号,获取更多精品原创

基于Apache Zookeeper手写实现动态配置中心(纯代码实践)的更多相关文章

  1. 代理模式精讲(手写JDK动态代理)

    代理模式是一种架构型模式,表现出来就是一个类代表另一个类的功能,一般用在想对访问一个类的时候做一些控制,同时又不想影响正常的业务,这种代理模式在现实的生活中应用的也非常的广泛,我用穷举法给举几个好理解 ...

  2. 使用java语言基于SMTP协议手写邮件客户端

    使用java语言基于SMTP协议手写邮件客户端 1. 说明 电子邮件是互联网上常见的应用,他是互联网早期的产品,直至今日依然受到广大用户的喜爱(在中国可能因为文化背景不同,电子邮件只在办公的时候常用) ...

  3. 基于MATLAB的手写公式识别(9)

    基于MATLAB的手写公式识别(9) 1.2图像的二值化 close all; clear all; Img=imread('drink.jpg'); %灰度化 Img_Gray=rgb2gray(I ...

  4. 基于MATLAB的手写公式识别(6)

    基于MATLAB的手写公式识别 2021-03-29 10:24:51 走通了程序,可以识别"心脑血管这几个字",还有很多不懂的地方. 2021-03-29 12:20:01 tw ...

  5. 基于MATLAB的手写公式识别(5)

    基于MATLAB的手写公式识别 总结一下昨天一天的工作成果: 获得了大致的识别过程. 一个图像从生肉到可以被处理需要经过预处理(灰质化.增加对比度.中值过滤.膨胀或腐蚀.闭环运算). 掌握了相关函数的 ...

  6. 基于MATLAB的手写公式识别(3)

    基于MATLAB的手写公式识别 图像的膨胀化,获取边缘(思考是否需要做这种处理,初始参考样本相对简单) %膨胀 imdilate(dilate=膨胀/扩大) clc clear A1=imread(' ...

  7. 基于MATLAB的手写公式识别(2)

    基于MATLAB的手写公式识别 图像的预处理(除去噪声.得到后续定位分割所需的信息.) 预处理其本质就是去除不需要的噪声信息,得到后续定位分割所需要的图像信息.图像信息在采集的过程中由于天气环境的影响 ...

  8. 基于MATLAB的手写公式识别(1)

    基于MATLAB的手写公式识别 reason:课程要求以及对MATLAB强大生命力的探索欲望: plan date:2021/3/28-2021/4/12 plan: 进行材料搜集和思路整理: 在已知 ...

  9. Dubbo配置完全外部化实践,使用动态配置中心的注意事项

    问题描述 近期开发项目,将Dubbo的配置全部外部化到动态配置中心.这里配置中心我使用的是Apollo. @Configuration public class DubboConfig { @Bean ...

随机推荐

  1. SpringBoot详解(一)——

    https://www.cnblogs.com/lifullmoon/p/14957771.html https://www.cnblogs.com/lifullmoon/p/14957751.htm ...

  2. .Net性能调优-MemoryPool

    简单用法 //获取MemoryPool实例,实际返回了一个ArrayMemoryPool<T> MemoryPool<char> Pool = MemoryPool<ch ...

  3. Spring5(六)——AspectJ(xml)

    一.AspectJ 1.介绍 AspectJ是一个面向切面的框架,它扩展了Java语言.AspectJ定义了AOP语法,也可以说 AspectJ 是一个基于 Java 语言的 AOP 框架.通常我们在 ...

  4. jdbc核心技术-宋红康

    视频地址 JDBC核心技术 第1章:JDBC概述 1.1 数据的持久化 持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用.大多数情况下,特别是企业级应用,数据持久化意味着将 ...

  5. CodeForce-810B Summer sell-off (结构体排序)

    http://codeforces.com/problemset/problem/810/B 已知n天里,已知第i天的供货量和需求量,给定一个f,可以在n天之中选f天促销使得供货量翻倍. 问选择其中f ...

  6. Django学习day07随堂笔记

    今日考题 """ 今日考题 1.必知必会N条都有哪些,每个都是干啥使的 2.简述神奇的双下划线查询都有哪些方法,作用是什么 3.针对多对多外键字段的增删改查方法有哪些,各 ...

  7. Markdown公式用法大全

    目录 基本语法 两种代码引用方式 插入链接并描述 插入图片 有序列表 无序列表 分割线 表格 如何插入公式 如何输入上下标 如何输入括号和分隔符 如何输入分数 如何输入开方 如何输入省略号 如何输入矢 ...

  8. 深入学习Composer原理(四)

    本系列第四篇文章,也是最后一篇 首先,我们先看看Composer的源码从哪里看起.当然,请您先准备好源码. composer init或者直接install之后,自动生成了一个vendor目录,这时您 ...

  9. Shell系列(31)- 双分支if语句简介

    双分支if条件语句 if [ 条件判断式 ] then 条件成立,执行的程序 else 条件不成立,执行的程序 fi 需求 根据用户输入的目录名,判断是否存在 脚本: #!/bin/bash #使用r ...

  10. Kafka 3.0新特性

    1.概述 Kafka是一个分布表示实时数据流平台,可独立部署在单台服务器上,也可部署在多台服务器上构成集群.它提供了发布与订阅的功能,用户可以发送数据到Kafka集群中,也可以从Kafka集群中读取数 ...