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 {

    private final List 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> 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 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 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 configs = Maps.newHashMap();
        setProperties(configs, "", jsonNode);
        return configs;
    }

    /**
     * 递归处理json文件,变成properties  .  的形式
     * @param configMap
     * @param parentPath
     * @param rootNode
     */
    public static void setProperties(Map configMap, String parentPath, JsonNode rootNode) {
        Iterator 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> 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 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 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 keys = getPropertyKey(value, end);
        keys.add(key);
        return keys;
    }

    public Map> 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 {
    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/-

你可能感兴趣的:(Zookeeper作为分布式配置中心小记)