«

Nacos源码学习计划-Day15-配置中心-Nacos客户端如何读取的远端配置

ZealSinger 发布于 阅读:73 技术文档


前面的内容我们讲解了Nacos作为注册中心的相关功能,服务的注册,更新,获取,删除,集群中的同步等内容都有讲解,从现在开始,我们要开始接触Nacos的第二大功能板块——配置中心。

我们知道,我们在使用Nacos的时候,可以在Nacos的管理端处配置对应的配置,然后所有的注册在该Nacos上的服务都会使用该配置的内容,那么我们就可以思考如下几个问题

我们的Nacos配置中心模块的主要学习就是从如下几个方向上出发。本篇解决第一个问题:客户端如何加载和读取Nacos的远端配置

客户端如何加载和读取Nacos的远端配置

我们在使用SpringBoot整合Nacos的时候,当我们需要使用Nacos远端配置,我们知道会需要写一个额外的配置文件bootstrap.yml并且加载Nacos的远端配置都是在服务启动之后就能立马加载且支持热更新

很明显,我们需要和分析“注册服务寻找客户端如何注册到Naocs服务端”时一样,从客户端的自动配置入手,如下,我们在Nacos的依赖文件中可以看到有这么一个类NacosConfigBootstrapConfiguration

image-20251114144352594

可以看到这个配置类中,定义了四个Bean-NacosConfigProperties ;NacosConfigManagerNacosPropertySourceLocator ;ConfigurationPropertiesRebinder

image-20251114152152849

NacosConfigProperties

首先是NacosConfigProperties,从命名来看就知道,这个是Nacos配置的实体类,将一些常用的配置信息封装为了一个类对象,和配置信息相关但和加载读取配置无关

image-20251114152508927

NacosConfigManager

NacosConfigManager主要的有两个成员,ConfigServiceNacosConfigProperties,前者为一个和配置操作相关的接口实现类对象,后者为配置对象。

可以看到这个ConfigService的创建还使用了懒汉式的创建方式NacosConfigManager 是一个配置管理核心组件,主要职责包括: ConfigService提供者: 创建和管理Nacos的 ConfigService 实例,这是与Nacos配置服务器交互的核心接口,主要作用是提供创建/发布/删除配置,而不是指导客户端加载读取配置。

image-20251114152623280

NacosPropertySourceLocator

其实,如果了解SpringCloud配置加载机制的UU,看到这个类的命名的时候,应该由能知道,bootStrap配置文件的加载和读取和这个类八九不离十。

我们来查看一下这个类的源代码,可以看到其内部维护了NacosPropertySourceBuilder ; NacosConfigPropertiesNacosConfigManager,并且该类实现了PropertySourceLocator

后面两个对象我们上面有小分析,我们主要来看其本身的逻辑以及其实现的接口PropertySourceLocator

image-20251114153721138

PropertySourceLocator接口

这个接口如果大家没有接触过的话,我们可以现在来看一下,分析一下他的作用,可以看到这个接口内部,有一个default默认方法和一个static静态方法,default方法的内部其实也就是调用static方法,所以可以认定,这个接口只有一个方法locateCollection()

image-20251114204641949

我们看一下这个defalut的这个方法在哪里被调用了,发现只有一个地方调用了,在SpringFrameWork的一个包中的PropertySourceBootstrapConfiguration类的initialize方法中被调用

image-20251114204845500

我们查看一下这个initialize被调用的地方,可以发现在SpringApplication中被调用了,看到这里其实已经很明显了,这个接口肯定属于整个SpringBoot启动时候的一个拓展点(类似于Bead前处理器,Bean实例化后处理器,属于启动后的流程内的一个钩子方法)

image-20251114205747032

继续往下面跟,在SpringApplication的applyInitializers方法中被调用,该方法又是在同类下的prepareContext方法中被调用,而prepareContext又被同类下的run(String... args)方法调用

image-20251114210633085

看到这里,还不明显么?SpringApplication类下的run(String... args)方法,其实就是我们所有的SpringBoot项目中,都有的启动类中的那个run方法,所以很明显,我们这个操作和项目初始化相关

我们现在再会过来看看,这个部分的到底是在干啥,看到applyInitializers,通过 for 循环遍历调用初始化器集合,会有一个 PropertySourceBootstrapConfiguration初始化器,在 for 循环当中,会调用这个类的 initialize 方法来初始化配置文件。

protected void applyInitializers(ConfigurableApplicationContext context) {
  // 循环调用初始化器
  for (ApplicationContextInitializer initializer : getInitializers()) {
     Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
           ApplicationContextInitializer.class);
     Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
     initializer.initialize(context);
  }
}

而在 initialize 方法中,这里就会使用到刚刚上文所提到的 PropertySourceLocator 扩展接口,SpringBoot 会拿到所有实现该接口的实现类,然后又是一个 for 循环遍历调用,而 Nacos 配置中心是有实现该接口

image-20251114223737081

在 for 循环中,会调用 locator.locateCollection() 收集方法,在这里要绕一下,因为 PropertySourceLocator 这个扩展接口它虽然是个 interface,但是它有默认的方法实现,也就是我们最开始看到的接口中的defalut方法

image-20251114223840356

在上图的static方法中,第一行中就会调用PropertySourceLocator接口实现类的locate方法,那么因为Nacos中NacosPropertySourceLocator实现了这个接口并且在客户端的依赖中早已通过NacosConfigBootstrapConfiguration这个配置类已经生成了 NacosPropertySourceLocator这个Bean对象,自然也就是执行其中的locate方法

locate方法

这里我们直接跟核心主线任务,主要是看如何加载原创配置的。在上文源码的最后,一共加载了三个不同类型的配置文件,有些小伙伴可能没使用过这些不同类型的配置文件,这里不着急,在下一个章节中我会详细详解不同类型的配置文件的使用方式,以及配置文件的优先顺序。

@Override
public PropertySource<?> locate(Environment env) {
  nacosConfigProperties.setEnvironment(env);
  // 获取 configService 对象
  ConfigService configService = nacosConfigManager.getConfigService();

  if (null == configService) {
     log.warn("no instance of config service found, can't load config from nacos");
     return null;
  }
 
  // 获取 yml 配置信息
  long timeout = nacosConfigProperties.getTimeout();
  nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
        timeout);
  String name = nacosConfigProperties.getName();

  String dataIdPrefix = nacosConfigProperties.getPrefix();
  if (StringUtils.isEmpty(dataIdPrefix)) {
     dataIdPrefix = name;
  }

  if (StringUtils.isEmpty(dataIdPrefix)) {
     dataIdPrefix = env.getProperty("spring.application.name");
  }

  CompositePropertySource composite = new CompositePropertySource(
        NACOS_PROPERTY_SOURCE_NAME);

  // 加载共享配置文件
  loadSharedConfiguration(composite);
  // 加载额外的配置文件
  loadExtConfiguration(composite);
  // 加载自身应用配置文件
  loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
  return composite;
}

那这三个加载配置方法中,最终都会调用到 loadNacosDataIfPresent() 这个方法(这里不放图了 自己点进去看一下立马可以看到),那我们一起来看下这个方法的源码部分,代码如下:

private void loadNacosDataIfPresent(final CompositePropertySource composite,
     final String dataId, final String group, String fileExtension,
     boolean isRefreshable) {
  // 校验参数 dataId、group
  if (null == dataId || dataId.trim().length() < 1) {
     return;
  }
  if (null == group || group.trim().length() < 1) {
     return;
  }
  // 在这里加载 Nacos 配置文件
  NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
        fileExtension, isRefreshable);
  // 把 Nacos 读取的配置添加到 Spring 容器当中
  this.addFirstPropertySource(composite, propertySource, false);
}

上面代码中有使用到loadNacosPropertySource()方法,我们进入这个方法往下去,会依次调用 build()loadNacosData(); configService.getConfig方法,getConfig的底层是getConfigInner其源码如下

private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
   group = blank2defaultGroup(group);
   ParamUtils.checkKeyParam(dataId, group);
   ConfigResponse cr = new ConfigResponse();
   
   cr.setDataId(dataId);
   cr.setTenant(tenant);
   cr.setGroup(group);
   
   // We first try to use local failover content if exists.
   // A config content for failover is not created by client program automatically,
   // but is maintained by user.
   // This is designed for certain scenario like client emergency reboot,
   // changing config needed in the same time, while nacos server is down.
   // 优先使用本地配置 主要是人为预设,最高优先级的本地配置
   String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);
   if (content != null) {
       LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}",
               worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
       cr.setContent(content);
       String encryptedDataKey = LocalEncryptedDataKeyProcessor
              .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
       cr.setEncryptedDataKey(encryptedDataKey);
       configFilterChainManager.doFilter(null, cr);
       content = cr.getContent();
       return content;
  }
   
   // 当本地配置没有的时候
   try {
       // 通过worker类来读取远程配置文件 1.4.x的版本底层就是用HTTP请求进行获取,2.x版本就是通过gRPC,通过RpcClient进行会哦去
       ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
       cr.setContent(response.getContent());
       cr.setEncryptedDataKey(response.getEncryptedDataKey());
       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={}",
               worker.getAgentName(), dataId, group, tenant, ioe.toString());
  }
   // 远端如何获取失败的话 再次尝试本地获取 这里获取的是客户都安自动缓存的内容 仅仅在远端失败时回退上次保存的成功配置
   content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);
   if (content != null) {
       LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}",
               worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
  }
   cr.setContent(content);
   String encryptedDataKey = LocalEncryptedDataKeyProcessor
          .getEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant);
   cr.setEncryptedDataKey(encryptedDataKey);
   configFilterChainManager.doFilter(null, cr);
   content = cr.getContent();
   return content;
}

总结

Nacos 客户端通过 Spring Boot 启动阶段的扩展机制,在应用初始化时自动从 Nacos Server 拉取配置,并依据预设的优先级策略(本地 → 远端 → 快照)确保配置可靠加载。整个过程透明、可扩展,为后续的配置热更新动态刷新奠定了坚实基础。

mermaid-diagram-2025-11-15-162158

 

 

当 NacosPropertySourceLocator 的 locate 方法被调用时,它大致会做以下几件事情:
(1)获取 Nacos 配置信息:
它会从已经部分初始化的 Environment 中读取 Nacos 客户端的配置,比如 spring.cloud.nacos.config.server-addr、spring.cloud.nacos.config.namespace、spring.cloud.nacos.config.group 等。
它还会读取需要加载的配置文件名(data-id),这可以是一个列表,支持加载多个配置文件。
(2)创建 Nacos 配置服务客户端 (ConfigService):
NacosPropertySourceLocator 会使用上面获取到的连接信息,通过 NacosFactory.createConfigService(properties) 来创建一个 ConfigService 的实例。ConfigService 是 Nacos 客户端 SDK 的核心,负责与 Nacos Server 进行网络通信。
(3)拉取配置:
NacosPropertySourceLocator 会遍历需要加载的 data-id 列表。
对于每一个 data-id,它会调用 ConfigService.getConfig(dataId, group, timeout) 方法从 Nacos Server 拉取配置内容。
Nacos 支持多种配置格式,如 properties、yaml、json 等。拉取到原始字符串后,Nacos 会根据配置的 file-extension(或文件名后缀)来选择合适的解析器(PropertySourceLoader),将其解析成一个 PropertySource 对象(例如 PropertiesPropertySource 或 YamlPropertySource)。
(4)组合并返回 PropertySource:
如果只拉取了一个配置文件,它会直接返回这个解析好的 PropertySource。
如果拉取了多个配置文件,它会将这些 PropertySource 封装到一个 CompositePropertySource(组合属性源)中,然后返回这个 CompositePropertySource。
(5)(可选 不在NacosPropertySourceLocator的相关逻辑下)注册配置监听器
如果启用了配置动态刷新功能(默认开启),NacosPropertySourceLocator 还会为每个拉取到的配置注册一个 Listener。
这个监听器会监听 Nacos Server 上对应配置的变化。当配置发生变更时,Nacos Server 会推送变更通知给客户端,监听器收到通知后会重新拉取配置,并更新 Environment 中对应的 PropertySource。
结合 @RefreshScope 注解,就可以实现 Bean 的动态刷新。

编程 Java 项目