上一篇博客我们基于Nacos源码中的example模块里的app类学习了NacosNamingService相关的内容:
https://blog.csdn.net/crystonesc/article/details/100530292
接下来我们继续以ConfigExample为入口,学习下Client模块中关于配置相关的类。首先把ConfigExample代码贴出来:
public class ConfigExample {
public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "192.168.171.6";
String dataId = "test";
String group = "DEFAULT_GROUP";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
//从工厂方法获取ConfigService
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println("first:"+content);
//添加配置监听
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("receive:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
//发布配置
boolean isPublishOk = configService.publishConfig(dataId, group, "content");
System.out.println(isPublishOk);
//获取配置
Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
//删除配置
boolean isRemoveOk = configService.removeConfig(dataId, group);
System.out.println(isRemoveOk);
Thread.sleep(3000);
//再次获取配置
content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
Thread.sleep(300000);
}
}
1.首先需要配置Properties,主要设置服务器地址、dataId、group,其中dataId在Nacos中指的是配置集,由一些相关或非相关的配置组成,对应带应用程序里面就是一个配置文件。group则是分组名称,不同的分组名称可以有相同的dataId,这里默认指定的是DEFAULT_GROUP。
2.从NacosFactory获取ConfigService,这里使用的方式和获取NamingService类似,NacosFactory提供了获取ConfigService和NamingService的方法,但是具体的获取办法则是通过NamingFactory和ConfigFactory来实现,如下方代码所示,这里截取了NacosFactory中的部分方法。
public static ConfigService createConfigService(Properties properties) throws NacosException {
return ConfigFactory.createConfigService(properties);
}
public static ConfigService createConfigService(String serverAddr) throws NacosException {
return ConfigFactory.createConfigService(serverAddr);
}
public static NamingService createNamingService(String serverAddr) throws NacosException {
return NamingFactory.createNamingService(serverAddr);
}
public static NamingService createNamingService(Properties properties) throws NacosException {
return NamingFactory.createNamingService(properties);
}
3.我们这里具体看下ConfigFactory的createConfigService方法,如下所示,其通过类名创建了ConfigService的子类,NacosConfigService,并通过properties初始化了ConfigService。接下来我们继续看下NacosConfigService的初始化方法.
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable var4) {
throw new NacosException(-400, var4);
}
}
4.NacosConfigService是在client模块当中,类名为:com.alibaba.nacos.client.config.NacosConfigService,我们先来看看其构造函数,首先获取用户是否配置了编码,如果未配置则采用默认的UTF-8,然后通过initNamespace方法初始化namespace,其初始化方法类似于NamingService中namespace的初始化方法,这里不再赘述。接下来实例化了MetricsHttpAgent类,该类持有ServerHttpAgent类的实例,ServerHttpAgent主要实现与Nacos服务器之间的通信,包括配置发布、删除等等,而MetricsHttpAgent则是对ServerHttpAgent的一次包装,实现了promethues的监控指标的上报。在早期Nacos的版本中,未有MetricsHttpAgent,代码在这里直接实例化了ServerHttpAgent。接着调用了agent的start方法,用于获取服务器端IP地址,如果服务器端进行了扩缩容,这里可以实时通知客户端。实际上start方法类是由ServerListManager来进行服务器地址的管控,后面会详细分析。最后实例化了ClientWorker来实现longpolling的配置更新任务,如果客户端订阅了某个配置,就会有ClientWorker来获取最新的配置更新,并通知响应的监听类。
完成构造函数的分析,我们可以继续看下NacosConfigService还提供了哪些方法。
public NacosConfigService(Properties properties) throws NacosException {
//获取编码配置
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
encode = Constants.ENCODE;
} else {
encode = encodeTmp.trim();
}
//初始化Namespace
initNamespace(properties);
//包装了ServerHttpAgent实现监控和Nacos服务器的操作,例如发布、删除配置等
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
//start方法适用于获取Nacos服务器列表的
agent.start();
//实现longpolling的配置更新任务
worker = new ClientWorker(agent, configFilterChainManager, properties);
}
5.NacosConfigService提供的其它方法,下面在方法上面备注了下方法的大体作用,这里不针对每个方法进行详细叙述,而是通过流程贯穿中来带出每个方法。
//初始化Namespace
private void initNamespace(Properties properties) {
String namespaceTmp = null;
String isUseCloudNamespaceParsing =
properties.getProperty(PropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
System.getProperty(SystemPropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
String.valueOf(Constants.DEFAULT_USE_CLOUD_NAMESPACE_PARSING)));
if (Boolean.valueOf(isUseCloudNamespaceParsing)) {
namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, new Callable() {
@Override
public String call() {
return TenantUtil.getUserTenantForAcm();
}
});
namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, new Callable() {
@Override
public String call() {
String namespace = System.getenv(PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_NAMESPACE);
return StringUtils.isNotBlank(namespace) ? namespace : EMPTY;
}
});
}
if (StringUtils.isBlank(namespaceTmp)) {
namespaceTmp = properties.getProperty(PropertyKeyConst.NAMESPACE);
}
namespace = StringUtils.isNotBlank(namespaceTmp) ? namespaceTmp.trim() : EMPTY;
properties.put(PropertyKeyConst.NAMESPACE, namespace);
}
//获取配置
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
//获取配置并注册监听器
@Override
public String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener) throws NacosException {
String content = getConfig(dataId, group, timeoutMs);
worker.addTenantListenersWithContent(dataId, group, content, Arrays.asList(listener));
return content;
}
//向Worker注册监听器
@Override
public void addListener(String dataId, String group, Listener listener) throws NacosException {
worker.addTenantListeners(dataId, group, Arrays.asList(listener));
}
//发布配置
@Override
public boolean publishConfig(String dataId, String group, String content) throws NacosException {
return publishConfigInner(namespace, dataId, group, null, null, null, content);
}
//移除发布的配置
@Override
public boolean removeConfig(String dataId, String group) throws NacosException {
return removeConfigInner(namespace, dataId, group, null);
}
//移除Worker的监听器
@Override
public void removeListener(String dataId, String group, Listener listener) {
worker.removeTenantListener(dataId, group, listener);
}
//获取配置
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = null2defaultGroup(group);
//校验dataId和group
ParamUtils.checkKeyParam(dataId, group);
//Config响应体
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 优先使用本地配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
//通过worker去获取配置
try {
content = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(content);
//进行过滤
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
} catch (NacosException ioe) {
if (NacosException.NO_RIGHT == ioe.getErrCode()) {
throw ioe;
}
LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
agent.getName(), dataId, group, tenant, ioe.toString());
}
//获取快照?
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
private String null2defaultGroup(String group) {
return (null == group) ? Constants.DEFAULT_GROUP : group.trim();
}
//移除配置服务
private boolean removeConfigInner(String tenant, String dataId, String group, String tag) throws NacosException {
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
String url = Constants.CONFIG_CONTROLLER_PATH;
List params = new ArrayList();
params.add("dataId");
params.add(dataId);
params.add("group");
params.add(group);
if (StringUtils.isNotEmpty(tenant)) {
params.add("tenant");
params.add(tenant);
}
if (StringUtils.isNotEmpty(tag)) {
params.add("tag");
params.add(tag);
}
HttpResult result = null;
try {
result = agent.httpDelete(url, null, params, encode, POST_TIMEOUT);
} catch (IOException ioe) {
LOGGER.warn("[remove] error, " + dataId + ", " + group + ", " + tenant + ", msg: " + ioe.toString());
return false;
}
if (HttpURLConnection.HTTP_OK == result.code) {
LOGGER.info("[{}] [remove] ok, dataId={}, group={}, tenant={}", agent.getName(), dataId, group, tenant);
return true;
} else if (HttpURLConnection.HTTP_FORBIDDEN == result.code) {
LOGGER.warn("[{}] [remove] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(), dataId,
group, tenant, result.code, result.content);
throw new NacosException(result.code, result.content);
} else {
LOGGER.warn("[{}] [remove] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(), dataId,
group, tenant, result.code, result.content);
return false;
}
}
//发布配置服务内部方法
private boolean publishConfigInner(String tenant, String dataId, String group, String tag, String appName,
String betaIps, String content) throws NacosException {
//检查是否设置了分组
group = null2defaultGroup(group);
//检查dataId,group,content的合法性
ParamUtils.checkParam(dataId, group, content);
ConfigRequest cr = new ConfigRequest();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
cr.setContent(content);
//对content的内容进行过滤,过滤的后的内容重新写入cr的content
configFilterChainManager.doFilter(cr, null);
content = cr.getContent();
String url = Constants.CONFIG_CONTROLLER_PATH;
List params = new ArrayList();
params.add("dataId");
params.add(dataId);
params.add("group");
params.add(group);
params.add("content");
params.add(content);
if (StringUtils.isNotEmpty(tenant)) {
params.add("tenant");
params.add(tenant);
}
if (StringUtils.isNotEmpty(appName)) {
params.add("appName");
params.add(appName);
}
if (StringUtils.isNotEmpty(tag)) {
params.add("tag");
params.add(tag);
}
List headers = new ArrayList();
if (StringUtils.isNotEmpty(betaIps)) {
headers.add("betaIps");
headers.add(betaIps);
}
//调用agent的httpPost请求服务器进行配置发布
HttpResult result = null;
try {
result = agent.httpPost(url, headers, params, encode, POST_TIMEOUT);
} catch (IOException ioe) {
LOGGER.warn("[{}] [publish-single] exception, dataId={}, group={}, msg={}", agent.getName(), dataId,
group, ioe.toString());
return false;
}
if (HttpURLConnection.HTTP_OK == result.code) {
LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}, config={}", agent.getName(), dataId,
group, tenant, ContentUtils.truncateContent(content));
return true;
} else if (HttpURLConnection.HTTP_FORBIDDEN == result.code) {
LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
dataId, group, tenant, result.code, result.content);
throw new NacosException(result.code, result.content);
} else {
LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
dataId, group, tenant, result.code, result.content);
return false;
}
}
@Override
public String getServerStatus() {
if (worker.isHealthServer()) {
return "UP";
} else {
return "DOWN";
}
}
6.现在回到最开始的ConfigExample中,configService.getConfig(dataId, group, 5000),这里表示获取dataId和groupe的配置,同时超时为5秒。这里会调用NacosConfigService中getConfig方法,getConfig方法中,会通过调用对象内部的getConfigInner方法来执行实际的操作,getConfigInner会带上NacosConfigService中配置的namespace,这样用户就不用每次都指定namespace了。
//获取配置
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
7.getConfigInner方法内容如下,首先其会做一些参数校验,并构建通过ConfigResponse构建响应体,响应的内容包括dataId,tenant,group以及content(配置内容),tenant则就是用户的namespace,接着Nacos会优先从本地获取配置,获取的方法是LocalConfigInfoProcessor.getFailover
,这样的做法是为了在无法和服务器通信的情况下获取本地配置数据,若本地没有配置文件,则通过ClientWorker的getServerConfig方法去服务器侧获取配置。
//获取配置
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = null2defaultGroup(group);
//校验dataId和group
ParamUtils.checkKeyParam(dataId, group);
//Config响应体
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 优先使用本地配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
//通过worker去获取配置
try {
content = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(content);
//进行过滤
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
} catch (NacosException ioe) {
if (NacosException.NO_RIGHT == ioe.getErrCode()) {
throw ioe;
}
LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
agent.getName(), dataId, group, tenant, ioe.toString());
}
//获取快照
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
8.接下来我们再继续查看ConfigExample后面的内容,这里通过configService添加了一个监听器,用于在配置发生改变后进行响应的操作。下面我们看下configService.addListener都做了什么。
//添加配置监听
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("receive:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
9.NacosConfigService的addListener方法直接调用了ClientWorker中的addTenantListeners方法,我们再来看下addTenantListeners方法,这个方法主要完成两件事,第一件事是向ClientWorker中的CacheData添加信息,其次是将Listener添加到cache当中,下面我们稍微分析下CacheData这个数据结构.
//向Worker注册监听器
@Override
public void addListener(String dataId, String group, Listener listener) throws NacosException {
worker.addTenantListeners(dataId, group, Arrays.asList(listener));
}
//添加租户的监听者
public void addTenantListeners(String dataId, String group, List extends Listener> listeners) throws NacosException {
group = null2defaultGroup(group);
String tenant = agent.getTenant();
CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
for (Listener listener : listeners) {
cache.addListener(listener);
}
}
10.CacheData数据结构:
CacheData主要包含name,dataId,group,tenant,listeners,configFilterChainManager,md5属性,可见一个CacheData主要对应的是一个配置集(配置文件),其中listeners这里主要使用ManagerListenerWrap,这个CacheData的内部类,主要为listeners附加了MD5的属性,当配置更新后,Nacos会把配置最新的MD5值覆盖上一次的MD5值,同时当发现Listener的MD5值与CacheData本生MD5值不同使,则会触发配置更新的操作,下面使上述逻辑的代码.
//查看Listeners的MD5值与CacheData的MD5值是否相同,不同的化,触发通知
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, md5, wrap);
}
}
}
11.好了,我们继续查看ConfigExample后面的内容,这里使发布配置操作,直接调用NacosConfigService的publish方法,如下面方法所示,最后在publishConfigInner方法中通过MetricHttpAgent完成方法的发布。
//发布配置
boolean isPublishOk = configService.publishConfig(dataId, group, "content");
System.out.println(isPublishOk);
//发布配置
@Override
public boolean publishConfig(String dataId, String group, String content) throws NacosException {
return publishConfigInner(namespace, dataId, group, null, null, null, content);
}
//发布配置服务内部方法
private boolean publishConfigInner(String tenant, String dataId, String group, String tag, String appName,
String betaIps, String content) throws NacosException {
//检查是否设置了分组
group = null2defaultGroup(group);
//检查dataId,group,content的合法性
ParamUtils.checkParam(dataId, group, content);
ConfigRequest cr = new ConfigRequest();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
cr.setContent(content);
//对content的内容进行过滤,过滤的后的内容重新写入cr的content
configFilterChainManager.doFilter(cr, null);
content = cr.getContent();
String url = Constants.CONFIG_CONTROLLER_PATH;
List params = new ArrayList();
params.add("dataId");
params.add(dataId);
params.add("group");
params.add(group);
params.add("content");
params.add(content);
if (StringUtils.isNotEmpty(tenant)) {
params.add("tenant");
params.add(tenant);
}
if (StringUtils.isNotEmpty(appName)) {
params.add("appName");
params.add(appName);
}
if (StringUtils.isNotEmpty(tag)) {
params.add("tag");
params.add(tag);
}
List headers = new ArrayList();
if (StringUtils.isNotEmpty(betaIps)) {
headers.add("betaIps");
headers.add(betaIps);
}
//调用agent的httpPost请求服务器进行配置发布
HttpResult result = null;
try {
result = agent.httpPost(url, headers, params, encode, POST_TIMEOUT);
} catch (IOException ioe) {
LOGGER.warn("[{}] [publish-single] exception, dataId={}, group={}, msg={}", agent.getName(), dataId,
group, ioe.toString());
return false;
}
if (HttpURLConnection.HTTP_OK == result.code) {
LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}, config={}", agent.getName(), dataId,
group, tenant, ContentUtils.truncateContent(content));
return true;
} else if (HttpURLConnection.HTTP_FORBIDDEN == result.code) {
LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
dataId, group, tenant, result.code, result.content);
throw new NacosException(result.code, result.content);
} else {
LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
dataId, group, tenant, result.code, result.content);
return false;
}
}
12.最后,ConfigExample中还对配置进行了获取和删除,其分析方法和发布配置类似,这里不再赘述。
小结:
本篇内容主要通过ConfigExample为切入,分析Client模块中关于配置发布、管理相关的类和方法,我们了解到NacosConfigService以及MetricHttpAgent和ClientWorker的作用和其中一些方法,后面一篇我们将继续分析Nacos的配置管理,来研究下ClientWorker是怎么实现配置更新的.