nacos配置中心客户端自动配置原理

2,844 阅读12分钟

版本说明

<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba</artifactId>
<version>2.2.0.RELEASE</version>

1、nacos配置中心客户端使用

1.1 导入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

1.2 配置中心的地址信息

spring:
  application:
    name: nacos-config-example
  profiles:
    active: DEV
---
spring:
  profiles: DEV
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: 789b5be0-0286-4cda-ac0c-e63f5bae3652
        group: DEFAULT_GROUP
        extension-configs:
          - data_id: arch.properties
            group: arch
            refresh: true
          - data_id: jdbc.properties
            group: data
            refresh: false
        shared-configs:
          - data_id: share.properties
            group: DEFAULT_GROUP
            refresh: true

2、 spring.factories

springboot应用在启动时会加载类路径下META-INF/spring.factories文件,将key为 org.springframework.cloud.bootstrap.BootstrapConfiguration(对应spring的bootstrap容器)和org.springframework.boot.autoconfigure.EnableAutoConfiguration(对应spring的Application容器) 对应的value加载到spring容器中。 在spring-cloud-alibaba-nacos-config模块类路径下META-INF/spring.factories文件中,导入了NacosConfigBootstrapConfiguration、NacosConfigAutoConfiguration等配置类。

2.1 NacosConfigBootstrapConfiguration spring bootstrap容器配置

/**
 * 向Bootstrap容器中注入了三个Bean
 *
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {

   // spring.cloud.nacos.config配置
   @Bean
   @ConditionalOnMissingBean
   public NacosConfigProperties nacosConfigProperties() {
      return new NacosConfigProperties();
   }

   // 管理ConfigService 
   //ConfigService是nacos配置中心顶层接口
   @Bean
   @ConditionalOnMissingBean
   public NacosConfigManager nacosConfigManager(
         NacosConfigProperties nacosConfigProperties) {
      return new NacosConfigManager(nacosConfigProperties);
   }

   // PropertySourceBootstrapConfiguration会加载NacosPropertySourceLocator提供的配置
   @Bean
   public NacosPropertySourceLocator nacosPropertySourceLocator(
         NacosConfigManager nacosConfigManager) {
      return new NacosPropertySourceLocator(nacosConfigManager);
   }

}

2.1.1 NacosConfigProperties naocs配置中心的配置信息

@ConfigurationProperties(NacosConfigProperties.PREFIX)
public class NacosConfigProperties {

   /**
    * Prefix of {@link NacosConfigProperties}.
    */
   public static final String PREFIX = "spring.cloud.nacos.config";

   //配置中心地址
   private String serverAddr;

   //配置中心内容编码
   private String encode;

   //分组
   private String group = "DEFAULT_GROUP";


    /**
     * 这块建议结合NacosPropertySourceLocator#locate()代码
     * 1、application配置:对应Nacos的dataId
     * {prefix}-{spring.profiles.active}.{file-extension}。
     *  对于prefix前缀,优先级 prefix > name > spring.application.name。
     * 应用配置内部也有优先级,从低到高:
     * {prefix}-{spring.profiles.active}.{file-extension}
     * {prefix}.{file-extension}
     * {prefix}
     *
     * 2、sharedConfigs:共享配置。 
     * 3、extensionConfigs:扩展配置。
     *
     * 三种配置优先级:share < extension < application
     */

   private String prefix;

   // 配置文件扩展名
   private String fileExtension = "properties";

   /**
    *
    * 命名空间 namespace, separation configuration of different environments.
    */
   private String namespace;

   /**
    * context path for nacos config server.
    */
   private String contextPath;

   /**
    * nacos config cluster name.
    */
   private String clusterName;

   /**
    * nacos config dataId name.
    */
   private String name;

   /**
    *
    * 共享配置 a set of shared configurations .e.g:
    * spring.cloud.nacos.config.shared-configs[0]=xxx .
    */
   private List<Config> sharedConfigs;

   /**
    * 扩展配制 a set of extensional configurations .e.g:
    * spring.cloud.nacos.config.extension-configs[0]=xxx .
    */
   private List<Config> extensionConfigs;

   /**
    * 总控配置是否可以刷新,shareConfigs和extensionConfigs内部可以定义自己的刷新机制。
    */
   private boolean refreshEnabled = true;
   
   
   //省略一些属性
   
   
}

2.1.2 NacosConfigManager 管理ConfigService

public class NacosConfigManager {

   private static final Logger log = LoggerFactory.getLogger(NacosConfigManager.class);

   // 单例ConfigService
   private static ConfigService service = null;

   private NacosConfigProperties nacosConfigProperties;

   public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
      this.nacosConfigProperties = nacosConfigProperties;
      //构造方法中创建ConfigService实例
      createConfigService(nacosConfigProperties);
   }
      
    //单例模式创建ConfigService
    static ConfigService createConfigService(
          NacosConfigProperties nacosConfigProperties) {
       if (Objects.isNull(service)) {
          synchronized (NacosConfigManager.class) {
             try {
                if (Objects.isNull(service)) {
                   service = NacosFactory.createConfigService(
                         nacosConfigProperties.assembleConfigServiceProperties());
                }
             }
             catch (NacosException e) {
                log.error(e.getMessage());
                throw new NacosConnectionFailureException(
                      nacosConfigProperties.getServerAddr(), e.getMessage(), e);
             }
          }
       }
       return service;
    }

}

2.1.3 NacosPropertySourceLocator加载外部配置

NacosPropertySourceLocator实现了spring提供的PropertySourceLocator接口 PropertySourceLocator的作用是提供PropertySource,开发人员需要实现该接口的locate方法, 并返回需要添加到容器中的PropertySource。

public class NacosPropertySourceLocator implements PropertySourceLocator {
}

2.2 NacosConfigAutoConfiguration spring Application容器

public class NacosConfigAutoConfiguration {

   //nacos配置文件
   @Bean
   public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
      // 使用bootstrap父容器中的NacosConfigProperties
      if (context.getParent() != null
            && BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                  context.getParent(), NacosConfigProperties.class).length > 0) {
         return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
               NacosConfigProperties.class);
      }
      return new NacosConfigProperties();
   }

   //已废弃
   @Bean
   public NacosRefreshProperties nacosRefreshProperties() {
      return new NacosRefreshProperties();
   }

   // 基于内存的配置刷新记录
   @Bean
   public NacosRefreshHistory nacosRefreshHistory() {
      return new NacosRefreshHistory();
   }

   // 管理ConfigService,内部使用的ConfigService是Bootstrap容器中的
   @Bean
   public NacosConfigManager nacosConfigManager(
         NacosConfigProperties nacosConfigProperties) {
      return new NacosConfigManager(nacosConfigProperties);
   }

   // 配置刷新
   @Bean
   public NacosContextRefresher nacosContextRefresher(
         NacosConfigManager nacosConfigManager,
         NacosRefreshHistory nacosRefreshHistory) {
      // Consider that it is not necessary to be compatible with the previous
      // configuration
      // and use the new configuration if necessary.
      return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
   }

}

2.2.1 NacosRefreshHistory 基于内存记录配置刷新历史


//在内存中记录最近20次配置刷新历史

public class NacosRefreshHistory {
    private static final int MAX_SIZE = 20;
  

    public void addRefreshRecord(String dataId, String group, String data) {
       records.addFirst(new Record(DATE_FORMAT.get().format(new Date()), dataId, group,
             md5(data), null));
       // 保留最近20条
       if (records.size() > MAX_SIZE) {
          records.removeLast();
       }
    }
}

2.2.2 NacosContextRefresher 上下文刷新

//实现了ApplicationListener接口 监听ApplicationReadyEvent事件
public class NacosContextRefresher
      implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
    
    public void onApplicationEvent(ApplicationReadyEvent event) {
       // many Spring context
       if (this.ready.compareAndSet(false, true)) {
          // 注册监听
          this.registerNacosListenersForApplications();
       }
    }
    
    
    private void registerNacosListenersForApplications() {
       // spring.cloud.nacos.config.isRefreshEnabled总控开关默认开启
       if (isRefreshEnabled()) {
          // 从缓存中获取所有Nacos配置
          for (NacosPropertySource propertySource : NacosPropertySourceRepository
                .getAll()) {
             if (!propertySource.isRefreshable()) {
                continue;
             }
             String dataId = propertySource.getDataId();
             //注册监听
             registerNacosListener(propertySource.getGroup(), dataId);
          }
       }
    }

    private void registerNacosListener(final String groupKey, final String dataKey) {
       String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
       //定义事件触发逻辑
       Listener listener = listenerMap.computeIfAbsent(key,
             lst -> new AbstractSharedListener() {
                @Override
                public void innerReceive(String dataId, String group,
                      String configInfo) {
                   refreshCountIncrement();
                   //记录配置刷新
                   nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);

                   /**
                    *  发布RefreshEvent事件 RefreshEventListener 监听该事件
                    * spring-cloud-starter-alibaba-nacos-config必须配合RefreshScope一同使用才能实现配置动态更新,
                    * 并且一个配置文件的更新会导致所有RefreshScope里的Bean重新实例化,暂时还不支持单个配置文件的刷新。
                    */

                   // todo feature: support single refresh for listening
                   applicationContext.publishEvent(
                         new RefreshEvent(this, null, "Refresh Nacos config"));
                   if (log.isDebugEnabled()) {
                      log.debug(String.format(
                            "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
                            group, dataId, configInfo));
                   }
                }
             });
       try {
          /**
           * 向nacos注册监听,注意 :此处并不是向spring注册监听 nacos有自己的一套事件监听组件
          */
          configService.addListener(dataKey, groupKey, listener);
       }
       catch (NacosException e) {
          log.warn(String.format(
                "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
                groupKey), e);
       }
    }
}

3、配置注入

3.1 spring容器的配置注入

PropertySourceBootstrapConfigurationApplicationContextInitializer类型的bean。容器启动过程中会回调ApplicationContextInitializer.initialize方法。具体PropertySourceBootstrapConfiguration.initialize方法是何时调用的本文不做赘述(跟了一下代码,非常复杂...)

public class PropertySourceBootstrapConfiguration implements
      ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
     
    @Autowired(required = false)
    private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
    
    public void initialize(ConfigurableApplicationContext applicationContext) {
       List<PropertySource<?>> composite = new ArrayList<>();
       AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
       boolean empty = true;
       ConfigurableEnvironment environment = applicationContext.getEnvironment();
       //遍历所有的PropertySourceLocator 执行locate方法(locateCollection方法中会执行loacte方法),将返回的PropertySource加到集合中
       for (PropertySourceLocator locator : this.propertySourceLocators) {
          Collection<PropertySource<?>> source = locator.locateCollection(environment);
          if (source == null || source.size() == 0) {
             continue;
          }
          List<PropertySource<?>> sourceList = new ArrayList<>();
          for (PropertySource<?> p : source) {
             if (p instanceof EnumerablePropertySource) {
                EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
                sourceList.add(new BootstrapPropertySource<>(enumerable));
             }
             else {
                sourceList.add(new SimpleBootstrapPropertySource(p));
             }
          }
          logger.info("Located property source: " + sourceList);
          composite.addAll(sourceList);
          empty = false;
       }
       if (!empty) {
          MutablePropertySources propertySources = environment.getPropertySources();
          String logConfig = environment.resolvePlaceholders("${logging.config:}");
          LogFile logFile = LogFile.get(environment);
          for (PropertySource<?> p : environment.getPropertySources()) {
             if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
                propertySources.remove(p.getName());
             }
          }
          
          //将收集到的composite,添加到environment中的中
          insertPropertySources(propertySources, composite);
          reinitializeLoggingSystem(environment, logConfig, logFile);
          setLogLevels(applicationContext, environment);
          handleIncludedProfiles(environment);
       }
    }
}

3.2 NacosPropertySourceLocator

public class NacosPropertySourceLocator implements PropertySourceLocator {
   
    public PropertySource<?> locate(Environment env) {
       nacosConfigProperties.setEnvironment(env);
       // 1. 获取Nacos配置管理核心API ConfigService
       ConfigService configService = nacosConfigManager.getConfigService();

       if (null == configService) {
          log.warn("no instance of config service found, can't load config from nacos");
          return null;
       }
       long timeout = nacosConfigProperties.getTimeout();
       // 2. 构造Nacos配置Builder 真正读取配置
       nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
             timeout);
       String name = nacosConfigProperties.getName();

       // 3. dataId前缀按照优先级获取
       // prefix > name > spring.application.name
       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;
    }
    //加载共享配置
    private void loadSharedConfiguration(
          CompositePropertySource compositePropertySource) {
       List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
             .getSharedConfigs();
       if (!CollectionUtils.isEmpty(sharedConfigs)) {
          checkConfiguration(sharedConfigs, "shared-configs");
          //从nacos server加载共享配置
          loadNacosConfiguration(compositePropertySource, sharedConfigs);
       }
    }
    
    //加载配置
    private void loadNacosConfiguration(final CompositePropertySource composite,
          List<NacosConfigProperties.Config> configs) {
       for (NacosConfigProperties.Config config : configs) {
          String dataId = config.getDataId();
          String fileExtension = dataId.substring(dataId.lastIndexOf(DOT) + 1);       //重点
          loadNacosDataIfPresent(composite, dataId, config.getGroup(), fileExtension,
                config.isRefresh());
       }
    }

    

    //加载扩展配置
    private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
       List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
             .getExtensionConfigs();
       if (!CollectionUtils.isEmpty(extConfigs)) {
          checkConfiguration(extConfigs, "extension-configs");
          loadNacosConfiguration(compositePropertySource, extConfigs);
       }
    }

    //加载application配制
    private void loadApplicationConfiguration(
          CompositePropertySource compositePropertySource, String dataIdPrefix,
          NacosConfigProperties properties, Environment environment) {
       String fileExtension = properties.getFileExtension();
       String nacosGroup = properties.getGroup();
       // load directly once by default
       // 第一次加载 : 默认情况下直接加载一次
       loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
             fileExtension, true);
       // load with suffix, which have a higher priority than the default
       // 第二次加载 : 加载带有后缀,具有比默认值更高的优先级
       loadNacosDataIfPresent(compositePropertySource,
             dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
       // Loaded with profile, which have a higher priority than the suffix
       // 第三次加载 :用配置文件 Profile 加载,它比后缀具有更高的优先级
       // 此处循环所有的 Profiles , 分别进行处理
       for (String profile : environment.getActiveProfiles()) {
          String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
          loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
                fileExtension, true);
       }

    }

}

无论加载共享和扩展配置,还是加载application配置,最终都调用到了loadNacosDataIfPresent方法

public class NacosPropertySourceLocator implements PropertySourceLocator {

    private void loadNacosDataIfPresent(final CompositePropertySource composite,
          final String dataId, final String group, String fileExtension,
          boolean isRefreshable) {
       if (null == dataId || dataId.trim().length() < 1) {
          return;
       }
       if (null == group || group.trim().length() < 1) {
          return;
       }
       // 加载
       NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
             fileExtension, isRefreshable);
       this.addFirstPropertySource(composite, propertySource, false);
    }

    private NacosPropertySource loadNacosPropertySource(final String dataId,
          final String group, String fileExtension, boolean isRefreshable) {
       if (NacosContextRefresher.getRefreshCount() != 0) {
          if (!isRefreshable) {
             return NacosPropertySourceRepository.getNacosPropertySource(dataId,
                   group);
          }
       }
       // 加载
       return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
             isRefreshable);
    }

com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#build

public class NacosPropertySourceBuilder {
    //配置中心顶层接口
    private ConfigService configService;
    
    NacosPropertySource build(String dataId, String group, String fileExtension,
          boolean isRefreshable) {
       // 1. 调用ConfigService读取远程配置
       Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
       // 2. 封装为NacosPropertySource
       NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
             p, new Date(), isRefreshable);
       // 3. 将配置缓存到NacosPropertySourceRepository
       NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
       // 4. 返回给外部,加入Environment
       return nacosPropertySource;
    }

    private Map<String, Object> loadNacosData(String dataId, String group,
          String fileExtension) {
       String data = null;
       try {
          // 1、获取远程配置信息(向Config Serevr发http请求)
          data = configService.getConfig(dataId, group, timeout);
          if (StringUtils.isEmpty(data)) {
             log.warn(
                   "Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
                   dataId, group);
             return EMPTY_MAP;
          }
          if (log.isDebugEnabled()) {
             log.debug(String.format(
                   "Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
                   group, data));
          }
          // 2、解析远程配置信息
          Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
                .parseNacosData(data, fileExtension);
          return dataMap == null ? EMPTY_MAP : dataMap;
       }
       catch (NacosException e) {
          log.error("get data from Nacos error,dataId:{}, ", dataId, e);
       }
       catch (Exception e) {
          log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
       }
       // 此处省略了异常处理逻辑 , 出现异常不会抛出 ,而是返回空集合
       // PS : 这里也导致部分错误不好从日志判断原因
       return EMPTY_MAP;
    }
}

com.alibaba.nacos.client.config.NacosConfigService#getConfig是nacos对外提供的服务发现接口。

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默认设置为DEFAULT_GROUP
    group = null2defaultGroup(group);
    ParamUtils.checkKeyParam(dataId, group);
    ConfigResponse cr = new ConfigResponse();

    cr.setDataId(dataId);
    cr.setTenant(tenant);
    cr.setGroup(group);

    /**
     * failover文件在Nacos里是优先级最高的,如果failover文件存在则不会使用nacos服务端的配置,永远会使用failover文件,
     * 即使服务端的配置发生了变化,类似于Apollo中-Denv=LOCAL时只会使用本地配置文件。
     * 需要注意的是,Nacos的failover文件内容没有更新的入口,也就是说这个文件只能在文件系统中修改生效,生效时机在长轮询过程
     * failover文件的路径:
     *      默认namespace:/{user.home}/{agentName}_nacos/data/config-data/{group}/{dataId}
     *      指定namespace:/{user.home}/{agentName}_nacos/data/config-data-tenant/{namespace}/{group}/{dataId}
     */

    // LEVEL1 : 使用本地文件系统的failover配置
    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;
    }
    //一般情况下,failover文件不会存在,那么都会走ClientWorker.getServerConfig方法
    // LEVEL2 : 读取config-server实时配置,并将snapshot保存到本地文件系统
    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;
        }
        // 非403错误进入LEVEL3
        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));
    // LEVEL3 : 如果读取config-server发生非403Forbidden错误,使用本地snapshot
    /**
     * snapshot文件的路径:
     *
     * 默认namespace:/{user.home}/{agentName}_nacos/snapshot/{group}/{dataId}
     * 指定namespace:/{user.home}/{agentName}_nacos/snapshot-tenant/{namespace}/{group}/{dataId}
     */
    content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
    cr.setContent(content);
    configFilterChainManager.doFilter(null, cr);
    content = cr.getContent();
    return content;
}

com.alibaba.cloud.nacos.parser.NacosDataParserHandler#parseNacosData解析配置,决定了nacos支持的配置文件格式

public final class NacosDataParserHandler {
  
    public Map<String, Object> parseNacosData(String data, String extension)
          throws IOException {
       if (null == parser) {
          // 创建解析器链
          parser = this.createParser();
       }
       return parser.parseNacosData(data, extension);
    }
    
    //创建解析器 可以看出nacos支持properties格式和yaml格式
    private AbstractNacosDataParser createParser() {
       return new NacosDataPropertiesParser().addNextParser(new NacosDataYamlParser())
             .addNextParser(new NacosDataXmlParser())
             .addNextParser(new NacosDataJsonParser());
    }
}

com.alibaba.cloud.nacos.parser.AbstractNacosDataParser#parseNacosData 真正的解析配置

public abstract class AbstractNacosDataParser {
    
    public final Map<String, Object> parseNacosData(String data, String extension)
          throws IOException {
       if (extension == null || extension.length() < 1) {
          throw new IllegalStateException("The file extension cannot be empty");
       }
       // 判断解析器是否支持解析当前格式的配置
       if (this.isLegal(extension.toLowerCase())) {
          return this.doParse(data);
       }
       if (this.nextParser == null) {
          throw new IllegalStateException(getTips(extension));
       }
       // 调用下一个解析器
       return this.nextParser.parseNacosData(data, extension);
    }
}

在新版本中,nacos的数据解析器整合了spring的SPI机制(spring.factories),可以让用户实现PropertySourceLoader用于处理不同扩展名的配置文件,只需要用户在自己的spring.factories中添加如下配置:

org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
public final class NacosDataParserHandler {

   private static List<PropertySourceLoader> propertySourceLoaders;

   private NacosDataParserHandler() {
      propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());
   }
   public List<PropertySource<?>> parseNacosData(String configName, String configValue,String extension) throws IOException {
         // ...
        for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {
            // 找到可以处理 扩展名配置文件的对应PropertySourceLoader
            if (!canLoadFileExtension(propertySourceLoader, extension)) {
                    continue;
            }
           // ...
        }
        return Collections.emptyList();
    }
 
}

4、配置监听

4.1 构造NacosConfidService对象

NacosConfigBootstrapConfiguration 自动配置类中,向容器中注入了NacosConfigManager对象。

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {

   // spring.cloud.nacos.config配置
   @Bean
   @ConditionalOnMissingBean
   public NacosConfigProperties nacosConfigProperties() {
      return new NacosConfigProperties();
   }

   // 管理ConfigService
   @Bean
   @ConditionalOnMissingBean
   public NacosConfigManager nacosConfigManager(
         NacosConfigProperties nacosConfigProperties) {
      return new NacosConfigManager(nacosConfigProperties);
   }
}

在NacosConfigManager的构造方法中,初始化了NacosConfigService,ConfigService是Nacos暴露给客户端的配置服务接口。

public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
   this.nacosConfigProperties = nacosConfigProperties;
   // Compatible with older code in NacosConfigProperties,It will be deleted in the
   // future.
   createConfigService(nacosConfigProperties);
}

/**
 * Compatible with old design,It will be perfected in the future.
 */
static ConfigService createConfigService(
      NacosConfigProperties nacosConfigProperties) {
   if (Objects.isNull(service)) {
      synchronized (NacosConfigManager.class) {
         try {
            if (Objects.isNull(service)) {
               //构造NacosConfigService
               service = NacosFactory.createConfigService(
                     nacosConfigProperties.assembleConfigServiceProperties());
            }
         }
         catch (NacosException e) {
            log.error(e.getMessage());
            throw new NacosConnectionFailureException(
                  nacosConfigProperties.getServerAddr(), e.getMessage(), e);
         }
      }
   }
   return service;
}

NacosFactory#createConfigService()

public class NacosFactory {

    /**
     * Create config service
     *
     * @param properties init param
     * @return config
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        return ConfigFactory.createConfigService(properties);
    }
}

public class ConfigFactory {

    /**
     * Create Config.
     *
     * @param properties init param
     * @return ConfigService
     * @throws NacosException Exception
     */
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            //为什么要通过反射创建NacosConfigService实现类?主要是为了将api层单独拆分出来。
            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);
        }
    }

}

4.2 ClientWorker

在NacosCofigService的构造方法中,创建了ClientWorker对象。

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
                    final Properties properties) {
    this.agent = agent;
    this.configFilterChainManager = configFilterChainManager;

    // Initialize the timeout parameter
    // 初始化一些参数,如:timeout
    init(properties);
    // 单线程执行器
    this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
            t.setDaemon(true);
            return t;
        }
    });
    // 执行LongPollingRunnable的执行器,固定线程数=核数
    this.executorService = Executors
        .newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });


    this.executor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            try {
                /**  10ms执行一次
                 * 负责检测当前情况(cacheMap大小及当前已经提交的长轮询任务数),是否需要提交新的长轮询任务到executorService中,固定线程数=1。
                 */
                checkConfigInfo();
            } catch (Throwable e) {
                LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
            }
        }
    }, 1L, 10L, TimeUnit.MILLISECONDS);
}

ClientWorker主要的任务就是执行长轮询。

public void checkConfigInfo() {
    // Dispatch taskes.
    // cacheMap大小
    int listenerSize = cacheMap.size();
    // Round up the longingTaskCount.
    // cacheMap大小 / 3000 向上取整
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());

    /**
     * 一个长轮询任务处理3000个listener,listener监听的是某个group的dataid
     * 每个listener在添加到cacheMap之前会计算所属的taskId
     */
    // 计算longingTaskCount 大于 当前实际长轮询任务数量
    if (longingTaskCount > currentLongingTaskCount) {

        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // The task list is no order.So it maybe has issues when changing.
            // 开启新的长轮询任务
            /**
             * 所以开启长轮询任务的时机,一般是注册监听之后创建了CacheData,checkConfigInfo定时任务扫描到需要开启新的长轮询任务时,触发长轮询任务提交
             */
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}

checkConfigInfo每10ms执行一次,负责检测当前情况(cacheMap大小及当前已经提交的长轮询任务数),是否需要提交新的长轮询任务到executorService中,固定线程数=1。

4.3 LongPollingRunnable

class LongPollingRunnable implements Runnable {

    private final int taskId;

    public LongPollingRunnable(int taskId) {
        this.taskId = taskId;
    }

    /**
     * 1、处理failover配置:判断当前CacheData是否使用failover配置(ClientWorker.checkLocalConfig),
     *  如果使用failover配置,则校验本地配置文件内容是否发生变化,发生变化则触发监听器(CacheData.checkListenerMd5)。
     *  这一步其实和长轮询无关。
     * 2、对于所有非failover配置,执行长轮询( /v1/cs/configs/listener  服务端会hold住请求),返回发生改变的groupKey(ClientWorker.checkUpdateDataIds)。
     * 3、根据返回的groupKey,查询服务端实时配置并保存snapshot(ClientWorker.getServerConfig)
     * 4、更新内存CacheData的配置content。
     * 5、校验配置是否发生变更,通知监听器(CacheData.checkListenerMd5)。
     * 6、如果正常执行本次长轮询,立即提交长轮询任务,执行下一次长轮询;发生异常,延迟2s提交长轮询任务。
     */
    @Override
    public void run() {
        // 当前长轮询任务负责的CacheData集合
        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        // 正在初始化的CacheData 即刚构建的CacheData,内部的content仍然是snapshot版本
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            // 1. 对于failover配置文件的处理
            // check failover config
            for (CacheData cacheData : cacheMap.values()) {
                //当前长轮询任务负责的CacheData
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    try {
                        // 判断cacheData是否需要使用failover配置,设置isUseLocalConfigInfo
                        // 如果需要则更新内存中的配置
                        checkLocalConfig(cacheData);
                        // 使用failover配置则检测content内容是否发生变化,如果变化则通知监听器
                        if (cacheData.isUseLocalConfigInfo()) {
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // 2. 对于所有非failover配置,执行长轮询( /v1/cs/configs/listener),返回发生改变的groupKey
            // check server config
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
            }

            //每个元素代表一个发生配置变化的groupKey。
            for (String groupKey : changedGroupKeys) {
                String[] key = GroupKey.parseKey(groupKey);
                String dataId = key[0];
                String group = key[1];
                String tenant = null;
                if (key.length == 3) {
                    tenant = key[2];
                }
                try {
                    // 3. 对于发生改变的配置,查询实时配置并保存snapshot
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    // 4. 更新内存中的配置
                    CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                    cache.setContent(ct[0]);
                    if (null != ct[1]) {
                        cache.setType(ct[1]);
                    }
                    LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                            agent.getName(), dataId, group, tenant, cache.getMd5(),
                            ContentUtils.truncateContent(ct[0]), ct[1]);
                } catch (NacosException ioe) {
                    String message = String
                            .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                    agent.getName(), dataId, group, tenant);
                    LOGGER.error(message, ioe);
                }
            }
            // 5. 对于非failover配置,触发监听器
            for (CacheData cacheData : cacheDatas) {
                // 排除failover文件
                if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                    /**
                     * 校验md5是否发生变化,如果发生变化通知listener
                     */
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();
            // 6-1. 都执行完成以后,再次提交长轮询任务
            executorService.execute(this);

        } catch (Throwable e) {

            // If the rotation training task is abnormal, the next execution time of the task will be punished
            LOGGER.error("longPolling error : ", e);
            // 6-2. 如果长轮询执行发生异常,延迟2s执行下一次长轮询
            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
        }
    }
}

当文件系统指定路径下的failover配置文件存在时,就会优先使用failover配置文件

private void checkLocalConfig(CacheData cacheData) {
    final String dataId = cacheData.dataId;
    final String group = cacheData.group;
    final String tenant = cacheData.tenant;

   // 当文件系统指定路径下的failover配置文件存在时,就会优先使用failover配置文件;当failover配置文件被删除时,又会切换为使用server端配置
    File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);
    // 当isUseLocalConfigInfo=false(使用failover配置文件) 且 failover配置文件存在时,使用failover配置文件,并更新内存中的配置
    if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);

        LOGGER.warn(
                "[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        return;
    }
    // 当isUseLocalConfigInfo=true 且 failover配置文件不存在时,不使用failover配置文件
    // If use local config info, then it doesn't notify business listener and notify after getting from server.
    if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
        cacheData.setUseLocalConfigInfo(false);
        LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
                dataId, group, tenant);
        return;
    }

    // When it changed.
    // 当isUseLocalConfigInfo=true 且 failover配置文件存在时 并且 记录failover配置文件的上次更新时间戳不等于当前failover配置文件的时间,使用failover配置文件并更新内存中的配置
    if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
            .lastModified()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        cacheData.setUseLocalConfigInfo(true);
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);
        LOGGER.warn(
                "[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
    }
}