1. 选用版本
- zookeeper版本 3.6.3, zookeeper3.5以后,需要再在bin包才是可执行运行的包。tar.gz为源码包
- curator-framework版本5.1.0
版本关系对应不上可能导致连接zookeeper报错
2. 灵魂-清晰的脉络
3. 关键逻辑处理
- 如何获取到需要变更对应配置的对象?
- 如何服务初始化启动时加载配置信息到spring的环境变量中?
ApplicationContextInitializer
ApplicationContextInitializer接口是spring容器刷新之前执行的一个回调函数,通常用于需要对应用程序上下文进行编程初始化,比如注册相关属性配置等。可以通过在启动main函数中,sringApplication.addInitializers()方法加入,也可通过spring.factories中指定加入。
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.zookeeperconfig.CustomEnvironmentPostProcessor
org.springframework.context.ApplicationContextInitializer=\
com.example.zookeeperconfig.config.ConfigCenterApplicationContextInitializer
com.example.zookeeperconfig.env.PropertySourceLocator=\
com.example.zookeeperconfig.env.ZookeeperPropertySourceLocator
解决第二个问题
BeanPostProcessor
后置处理器,spring提供的一个扩展接口。在对象实例化后,初始化前后进行相关逻辑处理。
public interface BeanPostProcessor {
//bean初始化方法调用前被调用
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
//bean初始化方法调用后被调用
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
解决第一个问题,获取到类实例化后的对象,根据对应的标记(注解)得到需要更是配置的对象。
核心代码
初始化加载配置
package com.example.zookeeperconfig.config;
import com.example.zookeeperconfig.env.PropertySourceLocator;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @Author: zhenghang.xiong
* @Date: 2021/9/18
* ApplicationContextInitializer接口是spring容器刷新之前执行的一个回调函数
* 通常用于需要对应用程序上下文进行编程初始化,比如注册相关属性配置等
* 可以通过在启动main函数中,sringApplication.addInitializers()方法加入
* 也可通过spring.factories中指定加入
*/
public class ConfigCenterApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private final List<PropertySourceLocator> propertySourceLocators;
public ConfigCenterApplicationContextInitializer() {
ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
// 加载propertySourceLocator的所有扩展实现,SPI
propertySourceLocators = new ArrayList<>(SpringFactoriesLoader.loadFactories(PropertySourceLocator.class, classLoader));
}
/**
* 动态加载自定义配置或者中心话配置到spring Enviroment中
* @param configurableApplicationContext
*/
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment();
MutablePropertySources mutablePropertySources = environment.getPropertySources();
for (PropertySourceLocator locator : this.propertySourceLocators) {
Collection<PropertySource<?>> sources = locator.locateCollection(environment, configurableApplicationContext);
if (sources == null || sources.size() == 0) {
continue;
}
for (PropertySource<?> p : sources) {
mutablePropertySources.addLast(p);
}
}
}
}
package com.example.zookeeperconfig.env;
import com.example.zookeeperconfig.config.NodeDataChangeListener;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.apache.curator.framework.CuratorFramework;
import java.util.Iterator;
import java.util.Map;
/**
* @Author: zhenghang.xiong
* @Date: 2021/9/18
*/
@Slf4j
public class ZookeeperPropertySourceLocator implements PropertySourceLocator{
private final CuratorFramework curatorFramework;
private final String CONFIG_NODE= "/custom-config";
public ZookeeperPropertySourceLocator() {
curatorFramework = CuratorFrameworkFactory.builder()
.connectString("192.168.182.132:2181")
.connectionTimeoutMs(60000)
.sessionTimeoutMs(60000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.namespace("config")
.build();
curatorFramework.start();
}
@Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
log.info("开始加载zookeeper中配置信息");
CompositePropertySource compositePropertySource = new CompositePropertySource("customService");
try {
Map<String, Object> dataMap = getRemoteEnvironment();
MapPropertySource mapPropertySource = new MapPropertySource("customService", dataMap);
compositePropertySource.addPropertySource(mapPropertySource);
// 增加节点修改监听器
addListener(environment, applicationContext);
} catch (Exception e) {
log.error("加载配置失败", e);
}
return compositePropertySource;
}
private void addListener(Environment environment, ConfigurableApplicationContext applicationContext) {
NodeDataChangeListener nodeDataChangeListener = new NodeDataChangeListener(environment, applicationContext);
CuratorCache curatorCache = CuratorCache.build(curatorFramework, CONFIG_NODE, CuratorCache.Options.SINGLE_NODE_CACHE);
CuratorCacheListener curatorCacheListener = CuratorCacheListener.builder().forChanges(nodeDataChangeListener).build();
curatorCache.listenable().addListener(curatorCacheListener);
curatorCache.start();
}
/**
* 获取节点配置信息
* @return
* @throws Exception
*/
private Map<String, Object> getRemoteEnvironment() throws Exception {
String data = new String(curatorFramework.getData().forPath(CONFIG_NODE));
ObjectMapper objectMapper = new ObjectMapper();
// 将多层json配置,转成zookeeper.name.xxx的key的形式,最后保存到配置中
JsonNode jsonNode = objectMapper.readTree(data);
// 递归拼接成 . 的形式
Map<String, Object> configs = Maps.newHashMap();
setProperties(configs, "", jsonNode);
return configs;
}
/**
* 递归处理json文件,变成properties . 的形式
* @param configMap
* @param parentPath
* @param rootNode
*/
public static void setProperties(Map<String, Object> configMap, String parentPath, JsonNode rootNode) {
Iterator<String> stringIterator = rootNode.fieldNames();
// 多个key的循环处理
while (stringIterator.hasNext()) {
String key = stringIterator.next();
String tempKey = StringUtils.isEmpty(parentPath)? key : parentPath + "." + key;
JsonNode secondNode = rootNode.get(key);
if (secondNode.isArray()) {
// 数组情况暂不处理
continue;
} else if(secondNode.isValueNode()){
configMap.put(tempKey, secondNode.asText());
} else if(secondNode.isObject()) {
setProperties(configMap, tempKey, secondNode);
}
}
}
}
保存配置信息与实例化bean的关系
package com.example.zookeeperconfig.config;
import com.example.zookeeperconfig.annotation.RefreshScope;
import com.example.zookeeperconfig.model.FieldPair;
import com.google.common.collect.Lists;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author: zhenghang.xiong
* @Date: 2021/9/22
*
*/
@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<?> beanClass = bean.getClass();
// 如果当前类被RefreshScope注解标记, 表明需要动态更新配置
if (beanClass.isAnnotationPresent(RefreshScope.class)) {
for (Field field : beanClass.getDeclaredFields()) {
Value value = field.getAnnotation(Value.class);
RefreshScope refreshScope = field.getAnnotation(RefreshScope.class);
if (value == null || refreshScope == null) {
// 未加@Value注解的字段忽略
continue;
}
List<String> keyList = getPropertyKey(value.value(), 0);
for(String key : keyList) {
fieldMapper.computeIfAbsent(key, (k) -> new ArrayList<>())
.add(new FieldPair(bean, field, value.value()));
}
}
}
// bean返回到IOC容器
return bean;
}
/**
* value="${xxx.xxx:yyyy}"
* @param value
* @param begin
* @return
*/
private List<String> getPropertyKey(String value, int begin) {
int start = value.indexOf("${", begin) + 2;
if (start < 2) {
// 未找到得到情况下
return Lists.newArrayList();
}
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);
}
List<String> keys = getPropertyKey(value, end);
keys.add(key);
return keys;
}
public Map<String, List<FieldPair>> getFieldMapper() {
return fieldMapper;
}
}
加载本地化配置
package com.example.zookeeperconfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.Properties;
/**
* @Author: zhenghang.xiong
* @Date: 2021/9/16
* Allows for customization of the application's {@link Environment} prior to the application context being refreshed.
* 允许定制应用的上下文的应用环境优于应用的上下文之前被刷新。(意思就是在spring上下文构建之前可以设置一些系统配置)
* EnvironmentPostProcessor为spring提供的一个初始化配置的接口,可以在spring上下文刷新之前进行相关环境变量的配置
* 使用它必须在spring.factories文件中指定类的全路径
*/
public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final Properties properties = new Properties();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Resource resource = new ClassPathResource("custom.properties");
environment.getPropertySources().addLast(loadProperties(resource));
}
private PropertySource<?> loadProperties(Resource resource) {
if (!resource.exists()) {
throw new RuntimeException("file not exists");
}
try {
properties.load(resource.getInputStream());
return new PropertiesPropertySource(resource.getFilename(), properties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
反射更改对象的属性值
package com.example.zookeeperconfig.config;
import com.example.zookeeperconfig.model.EnvironmentChangeEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* @Author: zhenghang.xiong
* @Date: 2021/9/22
* 接收配置更改的通知事件
*/
@Slf4j
@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 environmentChangeEvent) {
log.info("收到enviroment变更事件");
rebind();
}
private void rebind() {
this.beans.getFieldMapper().forEach((k, v) -> {
v.forEach(f -> f.resetValue(environment));
});
}
}
package com.example.zookeeperconfig.model;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.util.PropertyPlaceholderHelper;
import java.lang.reflect.Field;
/**
* @Author: zhenghang.xiong
* @Date: 2021/9/22
*/
@Slf4j
public class FieldPair {
private PropertyPlaceholderHelper propertyPlaceholderHelper =
new PropertyPlaceholderHelper("${", "}", ":", true);
private Object bean;
private Field field;
/**
* 注解的value值
*/
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);
}
String resetValue = propertyPlaceholderHelper.replacePlaceholders(value, environment::getProperty);
try {
field.set(bean, resetValue);
} catch (IllegalAccessException e) {
log.error("属性设置失败: ", e);
}
}
}