Zookeeper作为分布式配置中心小记

1. 选用版本

  • zookeeper版本 3.6.3, zookeeper3.5以后,需要再在bin包才是可执行运行的包。tar.gz为源码包
  • curator-framework版本5.1.0
    版本关系对应不上可能导致连接zookeeper报错

2. 灵魂-清晰的脉络

image.png

3. 关键逻辑处理

  1. 如何获取到需要变更对应配置的对象?
  2. 如何服务初始化启动时加载配置信息到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;
}
image.png

解决第一个问题,获取到类实例化后的对象,根据对应的标记(注解)得到需要更是配置的对象。

核心代码

初始化加载配置
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);
        }
    }
}

zookeeper配置信息

image.png

完整项目地址

https://github.com/loveqingbaobao/-

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容