接着上篇博客:
本文将实现上篇博客最后所述的问题:项目启动时加载指定环境的指定路径下的节点信息

因为需要预加载指定路径下的节点信息,所以使用PathChildrenCache来存储节点信息,使用对应的PathChildrenCacheListener来监听节点状态变化。

第一步:配置文件中指定预加载的节点路径

              

zookeeper有管理页面么_分布式

第二步:自定义Zookeeper客户端ZKExtClient.java,和上篇博客定义的客户单ZKClient.java类似,差别在于使用存储节点方式不同,本案例使用的是PathChildrenCache。

package com.learn.zw.zookeeper.client;

import com.learn.zw.zookeeper.constant.ZKConstant;
import com.learn.zw.zookeeper.starter.config.ZKConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.framework.recipes.cache.TreeCacheListener;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName: ZKExtClient
 * @Description: TODO
 * @Author: zw
 * @Date: 2019/3/8 14:52
 * @Version: 1.0
 */
@Slf4j
public class ZKExtClient {

    private CuratorFramework client;

    private Map<String, Object> cache = new ConcurrentHashMap<String, Object>();

    public ZKExtClient(ZKConfig config) {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(
                config.getRetrySleepTime(), config.getRetryCount());
        client = CuratorFrameworkFactory.builder().connectString(config.getConnectionAddr())
                .connectionTimeoutMs(config.getConnectionTimeout())
                .sessionTimeoutMs(config.getSessionTimeout())
                .retryPolicy(retryPolicy)
                .namespace(ZKConstant.root)
                .build();
        client.start();
    }

    public CuratorFramework getClient() {
        return client;
    }

    public PathChildrenCache getPathChildrenCache(String path) {
        return (PathChildrenCache)cache.get(path);
    }

    public void setData(String path, CreateMode mode, byte[] data) {
        try {
            if (!isExist(path)) {
                client.create().withMode(mode).forPath(path, data);
                return;
            }
            client.setData().forPath(path, data);
        } catch (Exception e) {
            log.error("更新属性节点:{},发生异常:{}", path, e);
        }
    }

    public void setData(String path, byte[] data) {
        try {
            if (!isExist(path)) {
                client.create().withMode(CreateMode.PERSISTENT).forPath(path, data);
                return;
            }
            client.setData().forPath(path, data);
        } catch (Exception e) {
            log.error("更新属性节点:{},发生异常:{}", path, e);
        }
    }

    public void setDataParentsIfNeeded(String path, byte[] data) {
        try {
            if (!isExist(path)) {
                client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, data);
                return;
            }
            client.setData().forPath(path, data);
        } catch (Exception e) {
            log.error("更新/新增属性节点:{},发生异常:{}", path, e);
        }
    }



    public byte[] getData(String path) {
        try {
            if (!isExist(path)) {
                return null;
            }
            return client.getData().forPath(path);
        } catch (Exception e) {
            log.error("获取属性节点:{},发生异常:{}", path, e);
            return null;
        }
    }

    public boolean deleteData(String path) {
        try {
            if (isExist(path)) {
                client.delete().deletingChildrenIfNeeded().forPath(path);
            }
        } catch (Exception e) {
            log.error("删除属性节点:{},发生异常:{}", path, e);
            return false;
        }
        return true;
    }

    public boolean isExist(String path) {
        try {
            Stat stat = client.checkExists().forPath(path);
            return stat == null ? false : true;
        } catch (Exception e) {
            log.error("校验属性节点:{},是否存在发生异常:{}", path, e);
            return false;
        }
    }

    /**
     *  设置Path Cache, 监控本节点的子节点被创建,更新或者删除,注意是子节点, 子节点下的子节点不能递归监控
     *  事件类型有3个, 可以根据不同的动作触发不同的动作
     *  @Param path 监控的节点路径, cacheData 是否缓存data
     *  可重入监听
     * */
    public void registerPathChildrenCacheListener(PathChildrenCacheListener listener, String path, boolean cacheData) {
        try {
            PathChildrenCache pathChildrenCache = new PathChildrenCache(client, path, cacheData);
            pathChildrenCache.getListenable().addListener(listener);
            pathChildrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
            cache.put(path, pathChildrenCache);
        } catch (Exception e) {
            log.error("节点:{},注册PathChildrenCacheListener监听发生异常:{}", path, e);
        }
    }

    /**
     *  设置Node Cache, 监控本节点的新增,删除,更新
     *  节点的update可以监控到, 如果删除会自动再次创建空节点
     *  @Param path 监控的节点路径, dataIsCompressed 数据是否压缩
     *  不可重入监听
     * */
    public void registerNodeCacheListener(NodeCacheListener listener, String path, boolean dataIsCompressed) {

    }

    /**
     *  设置Tree Cache, 监控本节点的新增,删除,更新
     *  节点的update可以监控到, 如果删除不会自动再次创建
     *  @Param path 监控的节点路径, dataIsCompressed 数据是否压缩
     *  可重入监听
     * */
    public void registerTreeCacheListener(TreeCacheListener listener, String path) {

    }

    /**
     * 注册zookeeper连接状态监听器
     * @param stateListener
     */
    public void registerConnectionStateListener(ConnectionStateListener stateListener) {
        this.client.getConnectionStateListenable().addListener(stateListener);
    }


}

第三步:自定义属性操作类PropertyExtCache.java,新增属性栏目用来指定预加载的节点路径

package com.learn.zw.zookeeper.cache;

import com.learn.zw.zookeeper.client.ZKExtClient;
import com.learn.zw.zookeeper.constant.ZKConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;

/**
 * @ClassName: PropertyExtCacheClient
 * @Description: TODO
 * @Author: zw
 * @Date: 2019/3/7 16:22
 * @Version: 1.0
 */
@Slf4j
public class PropertyExtCache {

    private ZKExtClient zkClient;
    /**
     * 环境变量
     */
    private String profile;
    /**
     * 属性栏目
     */
    private String[] modules;

    public PropertyExtCache(ZKExtClient client, String profile, String[] modules) {
        log.info("属性预加载环境:{}", profile);
        this.profile = profile;
        this.zkClient = client;
        this.modules = modules;
        if (modules != null && modules.length > 0) {
            for (int i = 0; i < modules.length; i++) {
                String module = modules[i];
                log.info("已订阅栏目路径:{}", module);
                this.zkClient.registerPathChildrenCacheListener(new CacheListener1(ZKConstant.separator + profile + module),
                        ZKConstant.separator + profile + module, true);
            }
        } else {
            log.warn("未发现待订阅栏目路径");
        }
    }

    /**
     * 根据属性节点路径从缓存中获取属性值
     *
     * @param path
     * @return
     * @throws Exception
     */
    public String getPropertyWithCache(String path) throws Exception {
        String key = path.substring(0, path.lastIndexOf("/"));
        PathChildrenCache pathChildrenCache = zkClient.getPathChildrenCache(getPrefixPath() + key);
        if (pathChildrenCache == null) {
            log.warn("预加载环境未发现路径为:{}的属性节点", getPrefixPath() + key);
            return null;
        }
        final ChildData currentData = pathChildrenCache.getCurrentData(getPrefixPath() + path);
        if (currentData == null) {
            log.warn("未发现路径为:{}的属性节点", path);
            return null;
        }
        byte[] bytes = currentData.getData();
        String data = new String(bytes, "utf-8");
        log.info("获取当前属性路径:" + path + ",属性值:" + (data.length() > 15 ? data.substring(0, 15) : data));
        return data;
    }

    /**
     * 删除属性节点,级联删除字节点
     *
     * @param path
     */
    public void deleteProperty(String path) {
        zkClient.deleteData(getPrefixPath() + path);
    }

    /**
     * 添加属性节点,父路径必须已存在
     *
     * @param path
     * @param value
     */
    public void addProperty(String path, String value) {
        zkClient.setData(getPrefixPath() + path, value.getBytes());
    }

    /**
     * 添加属性节点,父路径可不存在
     *
     * @param path
     * @param value
     */
    public void addPropertyParentsIfNeeded(String path, String value) {
        zkClient.setDataParentsIfNeeded(getPrefixPath() + path, value.getBytes());
    }

    /**
     * 获取当前环境下根节点路径,如:/dev,/prd
     *
     * @return
     */
    public String getPrefixPath() {
        return ZKConstant.separator + profile;
    }

    /**
     * 自定义节点状态监听器
     */
    class CacheListener1 implements PathChildrenCacheListener {

        private String path;

        public CacheListener1(String path) {
            this.path = path;
        }

        @Override
        public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
            switch (event.getType()) {
                case CHILD_ADDED:
                    log.info("监听路径:{},新增属性节点CHILD_ADDED,属性路径:{},属性值:{}", path,
                            event.getData().getPath(), new String(event.getData().getData(), "utf-8"));
                    break;
                case CHILD_UPDATED:
                    log.info("监听路径:{},更新属性节点CHILD_UPDATED,属性路径;{},属性值:{}", path,
                            event.getData().getPath(), new String(event.getData().getData(), "utf-8"));
                    break;
                case CHILD_REMOVED:
                    log.info("监听路径:{},移除属性节点NODE_REMOVED,属性路径;{},属性值:{}", path,
                            event.getData().getPath(), new String(event.getData().getData(), "utf-8"));
                    break;
                case CONNECTION_LOST:
                    log.warn("监听路径:{},连接丢失:CONNECTION_LOST", path);
                    break;
                case CONNECTION_SUSPENDED:
                    log.warn("监听路径:{},连接挂起:CONNECTION_SUSPENDED", path);
                    break;
                case CONNECTION_RECONNECTED:
                    log.warn("监听路径:{},重新连接:CONNECTION_RECONNECTED", path);
                    break;
                case INITIALIZED:
                    log.warn("监听路径:{},连接初始化:INITIALIZED", path);
                    break;
                default:
                    log.warn("监听到未定义状态变化类型");
                    break;
            }
        }
    }

}

第四步:定义bean的加载类ZKBoot.java

package com.learn.zw.zookeeper.starter;

import com.learn.zw.zookeeper.cache.PropertyCache;
import com.learn.zw.zookeeper.cache.PropertyExtCache;
import com.learn.zw.zookeeper.client.ZKExtClient;
import com.learn.zw.zookeeper.starter.config.ZKConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ZKBoot {

    @Value("${learn.zk.profile}")
    private String profile;

    @Value("${learn.zk.modules}")
    private String[] modules;

    /**
     * 此bean用来操作PathChildrenCache节点,可加载多个不同的路径下的一级子节点
     * 注:PathChildrenCache特性
     * @param config
     * @return
     */
    @Bean
    public ZKExtClient zkExtClient(@Qualifier("zkConfig") ZKConfig config) {
        return  new ZKExtClient(config);
    }

    @Bean
    public PropertyExtCache propertyExtCache(ZKExtClient zkExtClient) {
        return new PropertyExtCache(zkExtClient, profile, modules);
    }

    /**
     * 此bean用来操作TreeCache类型节点,加载指定路径下所有节点
     * 注:TreeCahce特性
     * @param config
     * @return
     */
    //@Bean
    public PropertyCache propertyCache(@Qualifier("zkConfig") ZKConfig config) {
        return new PropertyCache(config, profile);
    }

}

第五步:通过ZookeeperController.java定义的请求接口进行测试

package com.learn.zw.zookeeper.controller;

import com.learn.zw.zookeeper.cache.PropertyCache;
import com.learn.zw.zookeeper.cache.PropertyExtCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName: TestController
 * @Description: TODO
 * @Author: zw
 * @Date: 2019/3/7 18:23
 * @Version: 1.0
 */
@RestController
public class ZookeeperController {

    //@Autowired
    private PropertyCache propertyCache;

    @Autowired
    private PropertyExtCache propertyExtCache;


    @RequestMapping(value = "/test/get")
    public String test(String path) {
        try {
            //String property = propertyCache.getProperty(path);
            String property = propertyExtCache.getPropertyWithCache(path);
            return property;
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }

    @RequestMapping(value = "/test/delete")
    public String delete(String path) {
        try {
            //propertyCache.deleteProperty(path);
            propertyExtCache.deleteProperty(path);
            return "success";
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }

    @RequestMapping(value = "/test/add")
    public String add(String path, String value) {
        try {
            propertyExtCache.addPropertyParentsIfNeeded(path, value);
            //propertyCache.addPropertyParentsIfNeeded(path, value);
            return value;
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }
}

第六步:测试结果分析

1)通过yml配置文件中的modules可以知道预加载时只加载了/user/base、/user/center、/member/base路径下节点信息,对于其他路径下的节点信息是获取不到的,也监听不到其他节点的状态变化。通过zookeeper命令,查看一下目前zookeeper中共存储了哪些节点,如图:

     

zookeeper有管理页面么_zookeeper有管理页面么_02

获取未预加载路径下的节点信息时为空

zookeeper有管理页面么_zookeeper_03

获取已预加载路径下的节点信息可以获取到

zookeeper有管理页面么_zookeeper有管理页面么_04

2)新增节点

zookeeper有管理页面么_zookeeper_05

3)删除节点

zookeeper有管理页面么_分布式配置管理_06

4)后台日志

2019-04-06 12:02:04.955  WARN 3626 --- [nio-8090-exec-5] c.l.zw.zookeeper.cache.PropertyExtCache  : 预加载环境未发现路径为:/dev/level1的属性节点
2019-04-06 12:05:31.810  INFO 3626 --- [nio-8090-exec-8] c.l.zw.zookeeper.cache.PropertyExtCache  : 获取当前属性路径:/user/base/addrcode,属性值:address coce
2019-04-06 12:08:25.399  INFO 3626 --- [ChildrenCache-2] c.l.zw.zookeeper.cache.PropertyExtCache  : 监听路径:/dev/member/base,新增属性节点CHILD_ADDED,属性路径:/dev/member/base/addr,属性值:chengyuandedizhixinxi
2019-04-06 12:09:26.731  INFO 3626 --- [ChildrenCache-2] c.l.zw.zookeeper.cache.PropertyExtCache  : 监听路径:/dev/member/base,移除属性节点NODE_REMOVED,属性路径;/dev/member/base/addr,属性值:chengyuandedizhixinxi

总结

以上就是zookeeper实现配置管理的全部内容了,基本上都是代码实现,想理解zookeeper客户端以及一些关于zookeeper操作的高级API的可以在网上查阅相关资料学习,先知道它们是干什么的然后再根据个人情况和能力去深究其原理,不懂可以多看几遍但一定一定不要放弃,还有可别忘了动手实现,不动手玩玩那都是白瞎。

一起学习,一起进步!