部分内容来自:https://bugstack.cn/md/road-map/zookeeper.html
一、环境配置
安装zookeeper
# 命令执行 docker-compose up -d
version: '3.1'
services:
zoo1:
image: zookeeper:3.9.0
container_name: zookeeper
restart: always
hostname: zoo1#设置容器内的主机名为 zoo1
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1#ZooKeeper 节点的唯一 ID,为分布式模式准备
ZOO_SERVERS: server.1=zoo1:2888:3888;2181#定义 ZooKeeper 集群的服务器列表,2888 是节点间通信端口,3888 是选举端口,2181 是客户端连接端口。
二、基本使用
1. 连接 zookeeper
zkCli.sh -server localhost:2181
2. 创建节点:create /path data
[zk: localhost:2181(CONNECTED) 0] create /mynode hi
Created /mynode
类似于建文件夹,用自带的命令只能一个文件夹一个文件夹地建,比如先建/mynode,再建/mynode/config
3. 创建顺序节点:create -s /path data
[zk: localhost:2181(CONNECTED) 1] create -s /mynode hello
Created /mynode0000000005
4. 创建临时顺序节点:create -e -s /path data
[zk: localhost:2181(CONNECTED) 5] create -e -s /esnode esnode-data
Created /esnode0000000007
5. 获取节点数据:get /path
[zk: localhost:2181(CONNECTED) 6] get /mynode
hi
6. 获取节点子节点列表:ls /path
[zk: localhost:2181(CONNECTED) 0] ls /mynode
[]
7. 更新节点数据:set /path data
[zk: localhost:2181(CONNECTED) 1] set /mynode mydata
[zk: localhost:2181(CONNECTED) 2] get /mynode
mydata
8. 删除节点:delete /path
删除时路径下不能有其他节点,类似于删除文件夹时里面不能有文件
9. 删除节点及其子节点:deleteall /path
直接删除完
10. 监听节点变化:get -w /path
[zk: localhost:2181(CONNECTED) 12] create /mynode
Created /mynode
[zk: localhost:2181(CONNECTED) 13] get -w /mynode
null
[zk: localhost:2181(CONNECTED) 14] set /mynode data
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/mynode zxid: 64
[zk: localhost:2181(CONNECTED) 15] get -w /mynode
data
11. 查看节点状态:stat /path
[zk: localhost:2181(CONNECTED) 17] stat /mynode
cZxid = 0x3f
ctime = Thu Nov 28 16:51:07 UTC 2024
mZxid = 0x40
mtime = Thu Nov 28 16:51:58 UTC 2024
pZxid = 0x3f
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
12. 查看节点ACL权限:getAcl /path
[zk: localhost:2181(CONNECTED) 18] getAcl /mynode
'world,'anyone
: cdrwa
三、启动zookeeper服务
代码来自:https://bugstack.cn/md/road-map/zookeeper.html
1. 注入zookeeper客户端
引入依赖,定义一些连接用的参数,完成连接,并往spring ioc 容器中注入zookeeper客户端bean对象
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<version>3.1.4</version>
</dependency>
# zookeeper 自定义链接配置
zookeeper:
sdk:
config:
connect-string: 127.0.0.1:2181
base-sleep-time-ms: 1000
max-retries: 3
session-timeout-ms: 18000
connection-timeout-ms: 30000
@Data
@ConfigurationProperties(prefix = "zookeeper.sdk.config", ignoreInvalidFields = true)
public class ZookeeperClientConfigProperties {
private String connectString;
private int baseSleepTimeMs;
private int maxRetries;
private int sessionTimeoutMs;
private int connectionTimeoutMs;
}
@Configuration
@EnableConfigurationProperties(ZookeeperClientConfigProperties.class)
public class ZooKeeperClientConfig {
@Bean(name = "zookeeperClient")
public CuratorFramework createWithOptions(ZookeeperClientConfigProperties properties) {
ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(properties.getBaseSleepTimeMs(), properties.getMaxRetries());
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(properties.getConnectString())
.retryPolicy(backoffRetry)
.sessionTimeoutMs(properties.getSessionTimeoutMs())
.connectionTimeoutMs(properties.getConnectionTimeoutMs())
.build();
client.start();
return client;
}
}
2. 自定义注解
DCC动态配置中心,后面用来标注哪些属性是归动态配置中心管理的
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
public @interface DCCValue {
String value() default "";
}
3. 实现配置中心
方法 | 作用 |
---|---|
client.checkExists().forPath(BASE_CONFIG_PATH) | 检查是否存在指定节点 |
client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH) | 创建节点,如果存在父节点,一起创建 |
CuratorCache.build(client, BASE_CONFIG_PATH) | 为节点创建缓存对象 |
curatorCache.start() | 启动了 CuratorCache 的监听和缓存功能 |
curatorCache.listenable().addListener() | 添加监听 |
@Slf4j
@Configuration
public class DCCValueBeanFactory implements BeanPostProcessor {
private static final String BASE_CONFIG_PATH = "/paran/config";
private final CuratorFramework client;
private final Map<String, Object> dccObjGroup = new HashMap<>();
public DCCValueBeanFactory(CuratorFramework client) throws Exception {
this.client = client;
// 节点判断
if (null == client.checkExists().forPath(BASE_CONFIG_PATH)) {
client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH);
log.info("DCC 节点监听 base node {} not absent create new done!", BASE_CONFIG_PATH);
}
CuratorCache curatorCache = CuratorCache.build(client, BASE_CONFIG_PATH);
curatorCache.start();
curatorCache.listenable().addListener((type, oldData, data) -> {
switch (type) {
case NODE_CHANGED:
String dccValuePath = data.getPath();
Object objBean = dccObjGroup.get(dccValuePath);
try {
// 1. getDeclaredField 方法用于获取指定类中声明的所有字段,包括私有字段、受保护字段和公共字段。
// 2. getField 方法用于获取指定类中的公共字段,即只能获取到公共访问修饰符(public)的字段。
Field field = objBean.getClass().getDeclaredField(dccValuePath.substring(dccValuePath.lastIndexOf("/") + 1));
field.setAccessible(true);
field.set(objBean, new String(data.getData()));
field.setAccessible(false);
} catch (Exception e) {
throw new RuntimeException(e);
}
break;
default:
break;
}
});
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> beanClass = bean.getClass();
Field[] fields = beanClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(DCCValue.class)) {
DCCValue dccValue = field.getAnnotation(DCCValue.class);
try {
if (null == client.checkExists().forPath(BASE_CONFIG_PATH.concat("/").concat(dccValue.value()))) {
client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH.concat("/").concat(dccValue.value()));
log.info("DCC 节点监听 listener node {} not absent create new done!", BASE_CONFIG_PATH.concat("/").concat(dccValue.value()));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
dccObjGroup.put(BASE_CONFIG_PATH.concat("/").concat(dccValue.value()), bean);
}
}
return bean;
}
}
1. postProcessAfterInitialization
方法 | 作用 |
---|---|
isAnnotationPresent(xxx.class) | 判断某个字段上是否有xxx注解 |
getAnnotation(xxx.class) | 返回xxx注解的实例对象 |
关于反射的知识请看https://blog.csdn.net/fim77/article/details/143796031
实现这个接口,重载postProcessAfterInitialization
方法,即可对spring bean对象初始化完成之后做一些后续处理
public class DCCValueBeanFactory implements BeanPostProcessor
通过反射获取bean的属性字段
Class<?> beanClass = bean.getClass();
Field[] fields = beanClass.getDeclaredFields();
通过isAnnotationPresent()
方法判断属性字段上有没有指定注解
field.isAnnotationPresent(DCCValue.class)
如果有的话,把注解对象拿出来,这样后续就可以用到之前注解中写的值了
DCCValue dccValue = field.getAnnotation(DCCValue.class)
postProcessAfterInitialization
部分的流程如下
2. DCCValueBeanFactory构造函数
四、功能使用
@RestController
public class ConfigController {
@DCCValue("downgradeSwitch")
private String downgradeSwitch;
@DCCValue("userWhiteList")
private String userWhiteList;
@Resource
private CuratorFramework curatorFramework;
/**
* curl http://localhost:8091/getConfig/downgradeSwitch
*/
@RequestMapping("/getConfig/downgradeSwitch")
public String getConfigDowngradeSwitch() {
return downgradeSwitch;
}
/**
* curl http://localhost:8091/getConfig/userWhiteList
*/
@RequestMapping("/getConfig/userWhiteList")
public String getConfigUserWhiteList() {
return userWhiteList;
}
/**
* curl -X PUT "http://localhost:8091/setConfig?downgradeSwitch=true&userWhiteList=paran"
*/
@PutMapping("/setConfig")
public void setConfig(Boolean downgradeSwitch, String userWhiteList) throws Exception {
curatorFramework.setData().forPath("/paran/config/downgradeSwitch", (downgradeSwitch ? "开" : "关").getBytes(StandardCharsets.UTF_8));
curatorFramework.setData().forPath("/paran/config/userWhiteList", userWhiteList.getBytes(StandardCharsets.UTF_8));
}
}
测试结果
C:\Users\Paran>curl http://localhost:8091/getConfig/downgradeSwitch
C:\Users\Paran>curl http://localhost:8091/getConfig/userWhiteList
C:\Users\Paran>curl -X PUT "http://localhost:8091/setConfig?downgradeSwitch=true&userWhiteList=paran"
C:\Users\Paran>curl http://localhost:8091/getConfig/downgradeSwitch
开
C:\Users\Paran>curl http://localhost:8091/getConfig/userWhiteList
paran