本文基于nacos-2.0.3版本
nacos服务端源码是使用spring boot编写的,初看源码,完全找不到头绪,不知道从哪看起。于是我改变了思路,从客户端开始看,然后从客户端调用的服务逐步深入到服务端。那么本文就来介绍一下客户端是如何从服务端拉取配置。
一、注解@EnableNacosConfig
要想使用nacos,客户端必须配置注解@EnableNacosConfig。
一般的使用EnableNacosConfig指定服务端的地址,比如:
@EnableNacosConfig(globalProperties = @NacosProperties(serverAddr = "127.0.0.1:8848"))
该注解引入了一个非常重要的类:NacosConfigBeanDefinitionRegistrar。下面看一下这个类registerBeanDefinitions()方法。
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//获得EnableNacosConfig属性globalProperties的值
AnnotationAttributes attributes = fromMap(
metadata.getAnnotationAttributes(EnableNacosConfig.class.getName()));
//将globalProperties作为bean对象注册到spring容器中,bean名字为“globalNacosProperties$config”
registerGlobalNacosProperties(attributes, registry, environment,
CONFIG_GLOBAL_NACOS_PROPERTIES_BEAN_NAME);
//下面两个方法向spring容器中注册一些bean,
//包括了处理注解NacosInjected和注解NacosValue的后处理器,接下来将要用到的后处理器NacosPropertySourcePostProcessor
//还注册了构建ConfigService的构造器ConfigServiceBeanBuilder
// Register Nacos Common Beans
registerNacosCommonBeans(registry);
// Register Nacos Config Beans
registerNacosConfigBeans(registry, environment, beanFactory);
// Invoke NacosPropertySourcePostProcessor immediately
// in order to enhance the precedence of @NacosPropertySource process
//下面方法调用了后处理器NacosPropertySourcePostProcessor.postProcessBeanFactory()
invokeNacosPropertySourcePostProcessor(beanFactory);
}
NacosPropertySourcePostProcessor类的主要作用是:
- 找到服务器地址信息;
- 创建ConfigService对象
- 创建与服务器的连接
下面分别介绍这三个作用是如何实现的。
1、找到服务器地址信息
首先nacos遍历spring容器中所有的beanDefinition,从beanDefinition里面查找注解@NacosPropertySources或者@NacosPropertySource。
注解NacosPropertySource用于设置dataId和groupId,这两个值指定了配置在服务器上位置
nacos使用如下代码从注解里面找到dataId和groupId:
String name = (String) runtimeAttributes.get(NAME_ATTRIBUTE_NAME);
//下面两个值表示配置在服务器上存放的位置
String dataId = (String) runtimeAttributes.get(DATA_ID_ATTRIBUTE_NAME);
String groupId = (String) runtimeAttributes.get(GROUP_ID_ATTRIBUTE_NAME);
之后调用ConfigFactory.createConfigService()方法创建ConfigService对象。
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 e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
createConfigService()方法通过反射调用了NacosConfigService的构造方法。
2、创建ConfigService对象
ConfigService提供了查看、删除、增加服务端配置的方法,并且还可以创建监听服务端配置的监听器。ConfigService相当于客户端与服务端之间的接口,通过它可以方便的控制服务端的配置数据。
我们可以通过@NacosInjected直接在程序中自动注入ConfigService对象。nacos内部也是通过ConfigService完成对服务端配置数据的增删改查。
下面介绍ConfigService的构造方法:
public NacosConfigService(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
//设置请求的编码格式
//客户端与服务端使用http协议交互,encode属性是设置http协议报文头的字段Accept-Charset的值
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
this.encode = Constants.ENCODE;//默认UTF-8
} else {
this.encode = encodeTmp.trim();
}
initNamespace(properties);//初始化命名空间,默认是空
//ServerHttpAgent表示连接服务端的代理对象
//MetricsHttpAgent在ServerHttpAgent的基础上,增加了统计功能,可以统计每次连接的耗时
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
//初始化一些配置,并且创建定时任务
//其中一个定时任务可以定时访问服务器地址,更新服务器地址
this.agent.start();
//如果在NacosPropertySource里面配置了serverAddr属性,
//那么创建ClientWorker时会创建与服务端的连接,否则等到需要读取配置的时候才会建立连接
//ClientWorker的作用是访问服务器,读取配置信息,并在本地缓存
//同时还创建一个定时任务,定义检查服务器配置信息是否发生变化
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
创建ClientWorker对象时,会创建一个定时任务,定时任务执行checkConfigInfo(),该方法的作用是检查服务配置是否发生变化,如果发生变化就更改本地缓存,同时发布事件,通知响应的对象更改其属性值,定时任务的代码如下:
this.executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
配置定时更改的原理以后文章做介绍。
3、创建与服务器的连接
创建完ConfigService对象之后,nacos便可以通过ConfigService访问服务器读取dataId和groupId下的配置数据,那么这就需要建立与服务器之间的连接了。
连接的建立并且读取配置是通过ClientWorker.getServerConfig()完成的,下面只展示了ClientWorker.getServerConfig()方法的关键代码:
//tenant表示命名空间
//readTimeout表示超时时间
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
//省略部分代码
//入参params里面保存了dataId、group、tenant
//agent是MetricsHttpAgent对象
//result是HttpRestResult<String>类型的,里面保存了从服务器获取的配置数据
//CONFIG_CONTROLLER_PATH指定了请求URL的路径
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
} catch (Exception ex) {
//代码省略
}
switch (result.getCode()) {
case HttpURLConnection.HTTP_OK:
//在本地保存一份,保存到文件中
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
ct[0] = result.getData();//result.getData()返回服务器的配置数据
//CONFIG_TYPE指定了配置数据的展现方式,类型可以有JSON、XML、properties
//其中properties类型就是字符串表示,所有的属性和属性值组合成一个字符串
if (result.getHeader().getValue(CONFIG_TYPE) != null) {
ct[1] = result.getHeader().getValue(CONFIG_TYPE);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
//其他case省略
}
}
上面代码使用了MetricsHttpAgent.httpGet()方法,MetricsHttpAgent底层调用了ServerHttpAgent.httpGet()方法:
public HttpRestResult<String> httpGet(String path, Map<String, String> headers, Map<String, String> paramValues,
String encode, long readTimeoutMs) throws Exception {
//代码省略
//创建HTTP请求的报文头
//encode表示Accept-Charset的值
Header newHeaders = getSpasHeaders(paramValues, encode);
//添加请求参数,请求参数包括了dataId,groupId等
Query query = Query.newInstance().initParams(paramValues);
//下面的代码建立与服务端的连接,并且访问指定的URL得到服务器返回值
//默认情况下,nacos使用jdk的URLConnection创建与服务端的连接
HttpRestResult<String> result = NACOS_RESTTEMPLATE
//path指定了请求路径,path是默认值:/v1/cs/configs
.get(getUrl(currentServerAddr, path), httpConfig, newHeaders, query, String.class);
//代码省略
}
httpGet()方法的上述代码在一个循环里面,只要没有超过超时时间,它可以重试多次。
默认情况下,nacos使用jdk的URLConnection创建与服务端的连接,并且使用HTTP协议将请求发送到服务器。为了后面分析服务器行为方便,这里需要指出客户端请求的连接:
http://127.0.0.1:8848/nacos/v1/cs/configs
ConfigService找到了配置数据之后,nacos将这些数据组装到NacosPropertySource对象中,一个(dataId,groupId)对应一个NacosPropertySource对象。之后nacos将这些对象添加到spring的ConfigurableEnvironment对象的propertySources属性中,这样在应用程序中就可以像使用本地配置一样使用服务器上的配置数据了,包括占位符也可以直接处理。
如果在NacosPropertySource里面配置自动刷新,还会创建定时任务,这个在后面的文章中介绍。