springboot 一种分布式动态配置参数的实现方式

背景

分布式动态配置参数,相信是很多公司都要做的,改一处配置,各个地方都可以用,最重要的是大多数情况下都希望能够再应用运行时修改配置参数而无需重启应用:
在这里,作者看了好多解决方案,但都不理想,所以自己来实现,主要如下:

  • 使用百度开源的difconf 缺点:安装复杂依赖多
  • 继承PropertyPlaceholderConfigurer 缺点:springboot已经不支持,或支持度不好
  • 继承PropertySourcesPlaceholderConfigurer 缺点:实现很复杂,看不懂
  • 使用数据库,每次取配置时取获取 缺点:对代码侵入大,而且对一些默认配置不好修改

找了很久,我认为比较简便的实现方式

  1. springboot 会把所有的配置存入到Environment中,然后再给应用运行提供参数,所以我们可以拦截这个Environment,替换其中的值
  2. 关于分布式动态,我们可以采用zookeeper,启动时从zookeeper获取配置,替换Environment的配置参数,同时监听节点,配置改变时,同步修改配置(配置目前实现是存在一个map里面)
  3. 示例使用的application.properties文件,当然你也可以换成其他配置文件,然后配置文件里面必须有loadPropFromRemote=true,来判断是否需要加载远程配置,反之使用本地配置
  4. zookeeper管理工具我用的是:https://github.com/zzhang5/zooinspector,当然github里面也有更好的工具,但很多占用资源多,部署也较麻烦,只需要修改配置,我觉得zooinspector够了

关键代码如下

  • 在启动类里面增加一个bean,如下

    @Autowired
    private PropertyUtils propertyUtils;   

   /**
     * 自动从zookeeper获取配置信息并重写本地配置文件
     *
     */
    @Bean
    public String changeEnvironment(ApplicationContext applicationContext) {
        //支持Environment获取  修改容器里面StandardServletEnvironment

        StandardEnvironment standardEnvironment = applicationContext.getBean(StandardEnvironment.class);
        //根据本地loadPropFromRemote的参数是否为true来确定是否要从远程zookeeper加载配置数据
        if (StringUtils.isNotEmpty(standardEnvironment.getProperty("loadPropFromRemote")) && "true".equals(standardEnvironment.getProperty("loadPropFromRemote"))) {
            Properties properties = propertyUtils.loadProperties(standardEnvironment);
            standardEnvironment.getPropertySources().replace("applicationConfig: [classpath:/application.properties]", new PropertiesPropertySource("applicationConfig: [classpath:/application.properties]", properties));
        }else {
            Properties properties = (Properties)standardEnvironment.getPropertySources().get("applicationConfig: [classpath:/application.properties]").getSource();
            propertyUtils.cacheProperties(properties);

        }

        return null;
    }
  1. PropertyUtils
package com.wos.utils;

import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.logging.log4j.LogManager;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 从zookeeper获取配置信息,监听zookeeper
 * 缓存配置到CACHE_PROPERTIES 供程序使用
 */
@Component
public class PropertyUtils   {
    private static org.apache.logging.log4j.Logger log=LogManager.getLogger(PropertyUtils.class);
    private static NodeCache cache;
    private Properties properties;
    private static final Map CACHE_PROPERTIES = new ConcurrentHashMap<>();
    private static final String ENCODING = "UTF-8";


    public Properties loadProperties(StandardEnvironment standardEnvironment){
        Map props=new HashMap<>(3);
        props.put("zookeeper.address",standardEnvironment.getProperty("zookeeper.address"));
        props.put("app.name",standardEnvironment.getProperty("app.name"));
        props.put("app.version",standardEnvironment.getProperty("app.version"));
        loadPropertiesFromZookeeper(props);
        return properties;
    }


    private void loadPropertiesFromZookeeper(Map props) {
        log.info("开始从zookeeper获取配置信息");
        String zookeeperAddress = props.get("zookeeper.address");
        String appName = props.get("app.name");
        String appVersion = props.get("app.version");
        if (StringUtils.isBlank(zookeeperAddress) || StringUtils.isBlank(appName)
                || StringUtils.isBlank(appVersion)) {
            throw new RuntimeException("配置项[zookeeper.address, app.name, app.version]不能为空");
        }
        try {
            CuratorFramework client = ZookeeperUtil.getClient(zookeeperAddress);
            String path = "/configuration/" + appName + "/" + appVersion + "/config";
            byte[] data = client.getData().forPath(path);
            properties = loadRemoteProperties(data);
            cacheProperties(properties);
            log.info("获取远程配置信息成功");

            cache = new NodeCache(client, path);
            cache.start(true);
            cache.getListenable().addListener(() -> {
                ChildData data1 = cache.getCurrentData();
                log.info("远程配置信息变更,重新加载远程配置信息");
                properties = loadRemoteProperties(data1.getData());
                cacheProperties(properties);
            });
        } catch (Exception e) {
            throw new RuntimeException("从zookeeper获取配置信息失败", e);
        }
        log.info("配置文件初始化完成");
    }



    private  Properties loadRemoteProperties(byte[] data) throws IOException {

        String config = new String(data, ENCODING);
        Properties p = new Properties();
        p.load(new StringReader(config));
        return p;
    }

    /**
     * 缓存配置信息
     *
     * @param p 具体配置
     */
    public   void cacheProperties(Properties p) {
        for (Object key : p.keySet()) {
            if (key instanceof String) {
                Object value = p.get(key);
                if (value instanceof String) {
                    PropertyUtils.set((String) key, (String) value);
                }
            }
        }
    }
    public static String get(String key) {
        return CACHE_PROPERTIES.get(key);
    }

    protected static Map getProperties() {
        return CACHE_PROPERTIES;
    }

    private static void set(String key, String value) {
        CACHE_PROPERTIES.put(key, value);
    }




}
  • ZookeeperUtil
package com.wos.utils;


import org.apache.commons.lang.StringUtils;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.data.Stat;
import org.springframework.beans.factory.annotation.Value;


/**
 * zookeeper工具类
 */
public class ZookeeperUtil {

    private static CuratorFramework client;

    public static final String TYPE_CONFIGURATION = "configuration";

    @Value("$(zookeeper.address)")
    public static String ADDRESS;
    /**
     * 缓存client
     * @param c
     */
    protected synchronized static void cache(CuratorFramework c){
        client = c;
    }
    public static CuratorFramework getClient(){
        if(client == null){
            return getClient(ADDRESS);
        }
        return client;
    }
    public static synchronized CuratorFramework getClient(String address){
        if(client == null){
            if(StringUtils.isEmpty(address)){
                throw new RuntimeException("zookeeper地址不能为空");
            }
            //解决 zookeeper地址解析异常
            address = address.replace("zookeeper://", "");
            address = address.replace("?backup=", ",");
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
            client = CuratorFrameworkFactory.newClient(address, retryPolicy);
            client.start();
        }
        return client;
    }

    public static String getStringData(Stat stat, String path, String encode){
        try {
            byte[]  data = getClient().getData().storingStatIn(stat).forPath(path);
            return new String(data, encode);
        }catch(NoNodeException noNode){
            return null;
        }
        catch (Exception e) {
            throw new RuntimeException("获取信息失败", e);
        }
    }
    public static String getStringData(String path, String encode){
        try {
            byte[]  data = getClient().getData().forPath(path);
            return new String(data, encode);
        }catch(NoNodeException noNode){
            return null;
        }
        catch (Exception e) {
            throw new RuntimeException("获取信息失败", e);
        }
    }

    public static void saveOrUpdate(CuratorFramework client, String path, byte[] data) throws Exception{
        Stat stat = client.checkExists().forPath(path);
        if(stat != null){
            client.setData().forPath(path, data);
        }else{
            client.create().creatingParentsIfNeeded().forPath(path, data);
        }
    }
}
  • maven 依赖
        <dependency>
            <groupId>commons-langgroupId>
            <artifactId>commons-langartifactId>
            <version>2.5version>
        dependency>

        
        <dependency>
            <groupId>org.apache.curatorgroupId>
            <artifactId>curator-recipesartifactId>
            <version>2.10.0version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeepergroupId>
                    <artifactId>zookeeperartifactId>
                exclusion>
            exclusions>
        dependency>

        
        <dependency>
            <groupId>org.apache.zookeepergroupId>
            <artifactId>zookeeperartifactId>
            <version>3.4.5version>
        dependency>
        <dependency>
            <groupId>com.101tecgroupId>
            <artifactId>zkclientartifactId>
            <version>0.3version>
        dependency>
        

你可能感兴趣的:(java)