「这是我参与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;
}
}
... ...
}