「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
一、前言
nacos如何去拉取配置?
大胆猜想的步骤如下:
- 容器启动加载
nacos-client(这个发生在容器初始化Bean之前) nacos-client读取本地配置(例如:服务器地址server-addr)nacos-client发送HTTP请求到nacos-server,请求拉取配置- 将配置缓存在内存和本地文件中
先了解下 nacos 的数据模型:
Nacos数据模型Key由三元组唯一确定,Namespace默认是空串,公共命名空间(public),分组默认是DEFAULT_GROUP。
namespace: 命名空间group:群service/dataId:数据
数据模型,如图:
bootstrap.properties 文件,该文件配置了 Nacos 配置中心的地址,这个文件会让 Spring Cloud 启动 Bootstrap 阶段在 Nacos 配置中心获取配置。
Spring Cloud 的 Bootstrap 阶段的概念:
在
Bootstrap阶段会构造ApplicationContext,这个ApplicationContext加载配置的过程会基于bootstrap.properties或bootstrap.yaml文件(spring.config.name为bootstrap)去加载文件。 在加载文件的过程中,Spring Cloud有一套机制(PropertySourceLocator接口的定义)来u构造数据源PropertySource,其余跟Spring Boot一致的。
BootStrap 阶段构造的 ApplicationContext 会作为正常阶段的 ApplicationContext 的父类(parent),有了这一层父子关系之后,如果不能从子 ApplicationContext 获取配置,就会从父 ApplicationContext 获取。
Spring Cloud Bootstrap 阶段优先级高,会先读取配置中心的配置,这些配置在下一次正常的 ApplicationContext 启动时作用。
spring-cloud-context 模块内部的 META-INF/spring.factories 添加了一个 Bootstrap-ApplicationListener (实现了 ApplicationListener 接口),用于监听 ApplicationEnvironmentPreparedEvent 事件(Environment 刚创建,ApplicationContext 未创建时会触发事件),收到该事件后进入 Bootstrap 阶段,从配置中心加载配置。
二、源码分析
Nacos版本:1.4.2。 代码来自nacos源码工程下的example工程。
从这个 demo 入手,可以看到分为几步:
- 创建配置服务和获取配置
- 推送配置
- 删除配置
样例代码如下:
public class ConfigExample {
public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "localhost:8848";
String dataId = "my-provider";
String group = "DEFAULT_GROUP";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
// 创建配置服务
ConfigService configService = NacosFactory.createConfigService(properties);
// 获取配置
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
// 注册事件监听
// 监听 nacos 的事件
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)获取配置
从 demo 入手:
public class ConfigExample {
public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "localhost";
String dataId = "my-provider";
String group = "DEFAULT_GROUP";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
// 主要通过反射创建 ConfigService,实际 NacosConfigService
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
}
}
运行后,输出:
跟实际配置一致:
1)创建 ConfigService,实际子类 NacosConfigService
- 通过反射创建
ConfigService,实现类NacosConfigService
public class ConfigFactory {
// 参数包括:配置中心 serverAddr 地址、dataId 和 group
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);
}
}
}
- 深入
ConfigService, 即NacosConfigService
主要研究对象。
public class NacosConfigService implements ConfigService {
......
public NacosConfigService(Properties properties) throws NacosException {
// 1. 检查配置中的 `contextPath`
ValidatorUtils.checkInitParam(properties);
// 2. 从配置中获取配置编码,默认 UTF-8
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE); // encode
if (StringUtils.isBlank(encodeTmp)) {
this.encode = Constants.ENCODE; // UTF-8
} else {
this.encode = encodeTmp.trim();
}
// 3. 设置命名空间
initNamespace(properties);
// 4. 创建网络连接代理,构造函数传入了ServerHttpAgent, 采用装饰器模式
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
// 5. 去拉取服务配置
// 实际上调用 ServerHttpAgent.start() -> ServerListManager.start()
this.agent.start();
// 6. 客户端工作线程池取获取配置中心的配置
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
......
}
2)获取服务器中配置
从这行代码入手:
String content = configService.getConfig(dataId, group, 5000);
还是看源码:NacosConfigService
public class NacosConfigService implements ConfigService {
// ... ...
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 1. 优先使用本地配置
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;
}
// 2. 从远端服务器从拉取配置
try {
String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
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());
}
// 3. 从快照中获取配置
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
// ... ...
return content;
}
// ... ...
}
小结,获取配置步骤:
- 本地配置:对应目录例如:
/home/donald/nacos/config/fixed-localhost_8848_nacos/data/config-data - 远端配置
- 本地快照配置 快照配置对应目录例如:
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot$ ll
total 12
drwxrwxr-x 3 donald donald 4096 Jan 5 10:59 ./
drwxrwxr-x 3 donald donald 4096 Jan 5 10:59 ../
drwxrwxr-x 2 donald donald 4096 Jan 5 10:59 DEFAULT_GROUP/
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot$ cd DEFAULT_GROUP/
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot/DEFAULT_GROUP$ ll
total 12
drwxrwxr-x 2 donald donald 4096 Jan 5 10:59 ./
drwxrwxr-x 3 donald donald 4096 Jan 5 10:59 ../
-rw-rw-r-- 1 donald donald 39 Jan 5 11:22 my-provider
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot/DEFAULT_GROUP$ cat my-provider
book.author=haha
(2)推送配置
这块只是封装请求,然后发送 HTTP 请求到 nacos。
略。
(3)移除配置
这块只是封装请求,然后发送 HTTP 请求到 nacos。
略。
三、Spring 装配 Nacos 配置
若使用 Alibaba Nacos 配置中心,那么 spring-cloud-alibaba-nacos-config 模块中对应的 spring.factories 会加载 key 为 org.springframework.cloud.bootstrap.BootstrapConfiguration 的 NacosConfigBootstrapConfiguration 配置类。
该配置类内部会构造 NacosPropertySourceLocator 这个 PropertySourceLocator Bean。
NacosPropertySourceLocator 的作用就是从 Nacos 配置中心获取配置,定义如下:
@Order(0)
public class NacosPropertySourceLocator implements PropertySourceLocator {
... ...
public PropertySource<?> locate(Environment env) {
this.nacosConfigProperties.setEnvironment(env);
// 1. 得到 ConfigService, 用于从 Nacos 配置中心获取配置。
ConfigService configService = this.nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
} else {
long timeout = (long)this.nacosConfigProperties.getTimeout();
this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
String name = this.nacosConfigProperties.getName();
String dataIdPrefix = this.nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
// 2. 构造 CompositePropertySource,
// 后续 Nacos 配置中心加载的配置都会构造成 PropertySource
// 并添加到该 CompositePropertySource
CompositePropertySource composite = new CompositePropertySource("NACOS");
// 加载共享配置
this.loadSharedConfiguration(composite);
// 加载扩展配置
this.loadExtConfiguration(composite);
// 加载应用配置
this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);
return composite;
}
}
... ...
}