平常工作中经常使用公司的配置中心修改特定服务的配置,修改配置后,部署在不同机房不同机器的服务都能够及时跟随配置的变化。这个功能就是通过zookeeper实现的,同时结合了spring、java自定义注解,便于业务根据线上情况灵活调整。
本文介绍通过zookeeper的api实现简单的服务监听配置功能。
1. 代码
代码包括如下三个部分:配置客户端类ConfigClient、应用服务类Service、本地配置类MyConfig,关系如下:
ConfigClient通过与zookeeper服务器交互,修改MyConfig;
Service在业务逻辑中直接使用MyConfig;
Service启动时,完成ConfigClient的启动、初始化。
package config;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* 实际生产中,配置中心的客户端以java注解的形式使用
* 通过maven引入依赖,随着服务的启动,spring容器自动加载
*/
public class ConfigClient {
private static final Logger log = LoggerFactory.getLogger(ConfigClient.class);
private ZooKeeper zkClient;
private String servicePath;
private Map<String, String> localConfigs;
/**
* 服务启动时调用,将服务节点记录到/config-center节点下
*/
public void init(String appkey) throws IOException {
zkClient = new ZooKeeper("127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183", 15000,
event -> log.info("event:{}", event));
this.servicePath = "/config-center/" + appkey;
createNode(servicePath, new byte[0]);
localConfigs = new HashMap<>(10);
}
/**
* 注意,调用create时,data还被当做上下文参数ctx的值;在异步回调时,如果发生connectionLoss,ctx可以作为data再次调用create方法
*/
private void createNode(String path, byte[] data) {
zkClient.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() {
@Override
public void processResult(int rc, String path, Object ctx, String name) {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
createNode(path, (byte[]) ctx);
break;
case OK:
log.info("{} create successfully!", path);
break;
case NODEEXISTS:
log.info("{} exists!", path);
break;
default:
log.error("Something went wrong: ", KeeperException.create(KeeperException.Code.get(rc), path));
}
}
}, data);
}
/**
* 服务启动时调用,将本地的每个配置项作为节点,记录到服务节点下
*/
public void registerConfig(String fieldName, Object configValue) {
byte[] data = configValue == null ? new byte[0] : configValue.toString().getBytes();
String configPath = this.servicePath + "/" + fieldName;
createNode(configPath, data);
localConfigs.put(configPath, fieldName);
reloadConfig(configPath);
}
/**
* DataCallback:异步方式查询数据,如果连接丢失,重新执行reloadConfig
* Watcher:监听节点数据变更,即监听配置的值发生变更
*/
public void reloadConfig(String configPath) {
zkClient.getData(configPath, new Watcher() {
@Override
public void process(WatchedEvent event) {
switch (event.getType()) {
case NodeDataChanged:
log.info("数据修改事件发生:{}", event.getPath());
reloadConfig(event.getPath());
}
}
}, new AsyncCallback.DataCallback() {
@Override
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
reloadConfig(path);
case OK:
String fieldName = localConfigs.get(path);
String strValue = new String(data);
reloadValue(fieldName, strValue);
}
}
}, new Stat());
}
/**
* 通过反射的方式,将配置的变更set到应用服务的MyConfig类的对应field中
* 实际生产中,依托配置中心的客户端field级注解:不必将所有配置统一写到特定的类中;配置项名称不必与字段名称fieldName相同
*/
private void reloadValue(String fieldName, String strValue) {
try {
Field field = MyConfig.class.getDeclaredField(fieldName);
field.setAccessible(true);
if (field.getType() == String.class) {
field.set(MyConfig.getInstance(), strValue);
} else if (field.getType() == Integer.class) {
field.set(MyConfig.getInstance(), Integer.valueOf(strValue));
}
} catch (IllegalAccessException e) {
log.error("reload zk config to local config fail:", e);
} catch (NoSuchFieldException e) {
log.error("reload zk config to local config fail:", e);
}
}
}
package config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Scanner;
public class Service {
private static final Logger log = LoggerFactory.getLogger(ConfigClient.class);
/**
* 服务唯一标识
*/
private String appkey = "service-1";
private ConfigClient configClient;
/**
* 应用服务初始化
* 在实际生产中,通过spring完成configClient的初始化、依赖注入
*/
public void init() throws IOException {
configClient = new ConfigClient();
configClient.init(appkey);
configClient.registerConfig("whiteList", MyConfig.getInstance().whiteList);
configClient.registerConfig("limit", MyConfig.getInstance().limit);
}
public static void main(String[] args) {
Service service = new Service();
try {
service.init();
Thread.sleep(2000);
} catch (Exception e) {
log.error("start fail....");
System.exit(-1);
}
Scanner sc = new Scanner(System.in);
while (true) {
log.info("回车查看当前配置:");
sc.nextLine();
log.info("service config whiteList: {}, limit:{}", MyConfig.getInstance().getWhiteList(),
MyConfig.getInstance().limit);
}
}
}
package config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 实际生产中:
* ① myConfig可以作为单例bean托管在spring容器中
* ② 利用java field级注解,不必将每一项配置统一写到特定类中,
*/
public class MyConfig {
private volatile static MyConfig instance;
private MyConfig() {
}
/**
* 单例模式:延迟加载与双重检查锁定
*/
public static MyConfig getInstance() {
if (instance == null) {
synchronized (MyConfig.class) {
if (instance == null) {
instance = new MyConfig();
}
}
}
return instance;
}
/**
* 配置1:白名单列表
*/
public String whiteList = "lileim,hanmeimei";
public List<String> getWhiteList() {
try {
return new ArrayList(Arrays.asList(whiteList.split(",")));
} catch (Exception e) {
return new ArrayList<>();
}
}
/**
* 配置2:限制
*/
public Integer limit = 10;
}
2. 运行
(1)命令行启动zookeeper服务器,指令如下:
zkServer.sh start zoo-1.cfg
zkServer.sh start zoo-2.cfg
zkServer.sh start zoo-3.cfg
(2)命令行启动zookeeper客户端,新建配置中心节点/config-center
admindeMacBook-Pro-36:~ yeleits$ zkCli.sh -server 127.0.0.1:2181
[zk: 127.0.0.1:2181(CONNECTED) 0] create /config-center
(3)运行Service的main方法,控制台日志如下:
服务启动,第一次加载服务节点/config-center/service-1、配置节点/config-center/service-1/whiteList、/config-center/service-1/limit,配置节点的值是MyConfig类中对应字段的初始值。
(4)zookeeper客户端执行如下指令,并观察Service的控制台日志:
[zk: 127.0.0.1:2181(CONNECTED) 3] get /config-center/service-1/whiteList
lileim,hanmeimei
[zk: 127.0.0.1:2181(CONNECTED) 5] set /config-center/service-1/whiteList "zhangsan,lisi"
zookeeper客户端修改配置,服务成功监听到变化,修改了本地的配置值。
(5)停止Service后重启,观察Service的控制台日志:
服务重启后,提示服务节点、配置节点都已经存在了,而配置值不是代码中的默认初值。