Nacos源码(六)Nacos与SpringCloud前言

前言

本章学习spring-cloud-starter-alibaba-nacos-config如何集成nacos。

  • 配置如何注入Spring容器
  • 配置如何动态更新

注:截至目前官方spring-cloud-starter-alibaba-nacos-config仅支持nacos-client版本到1.4.x。

一、注入了哪些Bean

从spring.factories看,spring-cloud-starter-alibaba-nacos-config主要加载了两个重要的自动配置:

  • NacosConfigBootstrapConfiguration:Bootstrap容器自动配置。
  • NacosConfigAutoConfiguration:Application容器自动配置。
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer
org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
复制代码

Configuration.png

Boostrap容器

NacosConfigBootstrapConfiguration在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
   @Bean
   @ConditionalOnMissingBean
   public NacosConfigManager nacosConfigManager(
         NacosConfigProperties nacosConfigProperties) {
      return new NacosConfigManager(nacosConfigProperties);
   }

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

1、NacosConfigProperties

使用nacos时,bootstrap.yml配置如下:

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
复制代码

NacosConfigProperties存储了上述配置。

@ConfigurationProperties(NacosConfigProperties.PREFIX)
public class NacosConfigProperties {
   public static final String PREFIX = "spring.cloud.nacos.config";

   private String serverAddr;
   // 命名空间
   private String namespace;
   // 分组
   private String group = "DEFAULT_GROUP";
   // dataId前缀 name > prefix > spring.application.name
   private String prefix;
   private String name;
   // 配置文件扩展名
   private String fileExtension = "properties";

   /**
    * 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;

   /**
    * the master switch for refresh configuration, it default opened(true).
    */
   private boolean refreshEnabled = true;

   public static class Config {
      private String dataId;
      private String group = "DEFAULT_GROUP";
      private boolean refresh = false;
   }
}
复制代码
  • application配置:对应Nacos的dataId = {prefix}-{spring.profiles.active}.{file-extension}。对于prefix前缀,优先级name > prefix > spring.application.name。应用配置内部也有优先级,从低到高:
    • {prefix}
    • {prefix}-{spring.profiles.active}
    • {prefix}-{spring.profiles.active}.{file-extension}
  • sharedConfigs:共享配置。
  • extensionConfigs:扩展配置。
  • 三种配置优先级:share < extension < application
  • 配置刷新:NacosConfigProperties.refreshEnabled总控配置是否可以刷新,shareConfigs和extensionConfigs内部可以定义自己的刷新机制。

2、NacosConfigManager

NacosConfigManager负责管理ConfigService,Bootstrap和Application容器共享一个ConfigService(单例)。

public class NacosConfigManager {
   // 单例ConfigService
   private static ConfigService service = null;

   private NacosConfigProperties nacosConfigProperties;

   public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
      this.nacosConfigProperties = nacosConfigProperties;
      createConfigService(nacosConfigProperties);
   }

   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) {
               throw new NacosConnectionFailureException(
                     nacosConfigProperties.getServerAddr(), e.getMessage(), e);
            }
         }
      }
      return service;
   }

   public ConfigService getConfigService() {
      if (Objects.isNull(service)) {
         createConfigService(this.nacosConfigProperties);
      }
      return service;
   }

   public NacosConfigProperties getNacosConfigProperties() {
      return nacosConfigProperties;
   }

}
复制代码

3、NacosPropertySourceLocator

NacosPropertySourceLocator实现了Spring的PropertySourceLocator接口。

public class NacosPropertySourceLocator implements PropertySourceLocator {
}
复制代码

PropertySourceLocator的作用是提供PropertySource,需要用户实现locate方法。

public interface PropertySourceLocator {
   PropertySource<?> locate(Environment environment);

   default Collection<PropertySource<?>> locateCollection(Environment environment) {
      return locateCollection(this, environment);
   }

   static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
         Environment environment) {
      PropertySource<?> propertySource = locator.locate(environment);
      if (propertySource == null) {
         return Collections.emptyList();
      }
      if (CompositePropertySource.class.isInstance(propertySource)) {
         Collection<PropertySource<?>> sources = ((CompositePropertySource) propertySource)
               .getPropertySources();
         List<PropertySource<?>> filteredSources = new ArrayList<>();
         for (PropertySource<?> p : sources) {
            if (p != null) {
               filteredSources.add(p);
            }
         }
         return filteredSources;
      }
      else {
         return Arrays.asList(propertySource);
      }
   }
}
复制代码

Bootstrap自动配置只是注入了上述三个Bean,并没有实际接入配置中心。实际接入Nacos配置发生在Application容器中。

Application容器

NacosConfigAutoConfiguration向Application容器中注入了4个Bean。其中NacosConfigProperties直接取了父容器中的实例;NacosConfigManager实例与父容器实例不同,但是内部使用的static ConfigService与父容器中的实例是同一个实例。

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigAutoConfiguration {
   @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 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) {
      return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
   }

}
复制代码

1、NacosRefreshHistory

NacosRefreshHistory负责记录近20次配置刷新记录。

public class NacosRefreshHistory {
   private static final int MAX_SIZE = 20;
   private final LinkedList<Record> records = new LinkedList<>();
   private final ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal
         .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

   private MessageDigest md;

   public NacosRefreshHistory() {
      try {
         md = MessageDigest.getInstance("MD5");
      }
      catch (NoSuchAlgorithmException e) {
         log.error("failed to initialize MessageDigest : ", e);
      }
   }

   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();
      }
   }

   public LinkedList<Record> getRecords() {
      return records;
   }
}
复制代码

2、NacosContextRefresher

NacosContextRefresher负责向Nacos注册配置监听和处理配置变更,利用SpringBoot的ApplicationReadyEvent事件钩子,触发注册配置监听逻辑。

public class NacosContextRefresher
      implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
  @Override
	public void onApplicationEvent(ApplicationReadyEvent event) {
		if (this.ready.compareAndSet(false, true)) {
			this.registerNacosListenersForApplications();
		}
	}
}
复制代码

二、配置注入

如何加载配置分为两个方面:何时注入、如何注入。

何时注入

为了实例化单例Bean时能读到Nacos配置,需要在Bean实例化之前就将配置注入Spring的Environment。

Nacos利用PropertySourceBootstrapConfiguration这个ApplicationContextInitializer,在Application容器刷新前(prepareContext阶段)使用NacosPropertySourceLocator将nacos配置转换为PropertySource注入了Environment。

看看Spring提供的PropertySourceBootstrapConfiguration如何调用自定义PropertySourceLocator,如果以后有类似需求可以参考Nacos的实现方式。

配置注入.png

首先spring-cloud-context中的spring.factories将PropertySourceBootstrapConfiguration作为BootstrapConfiguration自动配置,注入了Bootstrap容器。

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
复制代码

接着,PropertySourceBootstrapConfiguration注入了所有PropertySourceLocator。注意,这里在Bootstrap容器中,父容器对子容器是无感知的,这也是为什么NacosPropertySourceLocator需要在Bootstrap容器中注入

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
      ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

   // 注入容器中的PropertySourceLocator
   @Autowired(required = false)
   private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
}
复制代码

接下来看看PropertySourceBootstrapConfiguration实现ApplicationContextInitializer的逻辑。

// PropertySourceBootstrapConfiguration
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
   // PropertySourceLocator提供的配置项结果集
   List<PropertySource<?>> composite = new ArrayList<>();
   AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
   boolean empty = true;
   ConfigurableEnvironment environment = applicationContext.getEnvironment();
   // 循环所有PropertySourceLocator执行它的locateCollection方法
   // 返回的PropertySource放入composite
   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));
         }
      }
      composite.addAll(sourceList);
      empty = false;
   }
   if (!empty) {
      MutablePropertySources propertySources = environment.getPropertySources();
      // ...
      // 将收集到的composite,插入到environment中的propertySources中
      insertPropertySources(propertySources, composite);
      // ...
   }
}
复制代码

默认情况下,这些加载的PropertySource优先级会高于其他配置。代码层面的体现就是,所有外部配置会通过environment.propertySources.addFirst加入environment。

private void insertPropertySources(MutablePropertySources propertySources,
      List<PropertySource<?>> composite) {
   MutablePropertySources incoming = new MutablePropertySources();
   List<PropertySource<?>> reversedComposite = new ArrayList<>(composite);
   Collections.reverse(reversedComposite);
   for (PropertySource<?> p : reversedComposite) {
      incoming.addFirst(p);
   }
   PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
   Binder.get(environment(incoming)).bind("spring.cloud.config",
         Bindable.ofInstance(remoteProperties));
   // #1 会走这environment.propertySources.addFirst
   if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
         && remoteProperties.isOverrideSystemProperties())) {
      for (PropertySource<?> p : reversedComposite) {
         propertySources.addFirst(p);
      }
      return;
   }
   // ...
}
复制代码

Environment会利用PropertySourcesPropertyResolver读取配置,org.springframework.core.env.PropertySourcesPropertyResolver#getProperty是Spring读取配置的入口方法,会循环所有propertySource,排在越前面的优先级越高。

// PropertySourcesPropertyResolver
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
   if (this.propertySources != null) {
      for (PropertySource<?> propertySource : this.propertySources) {
         Object value = propertySource.getProperty(key);
         if (value != null) {
            if (resolveNestedPlaceholders && value instanceof String) {
               value = resolveNestedPlaceholders((String) value);
            }
            logKeyFound(key, propertySource, value);
            return convertValueIfNecessary(value, targetValueType);
         }
      }
   }
   return null;
}
复制代码

如何注入

NacosPropertySourceLocator的locate方法返回Nacos配置,在PropertySourceBootstrapConfiguration中负责将这些配置注入Environment。

// NacosPropertySourceLocator
@Override
public PropertySource<?> locate(Environment env) {
   nacosConfigProperties.setEnvironment(env);
   // 1. 获取Nacos核心API ConfigService
   ConfigService configService = nacosConfigManager.getConfigService();
   long timeout = nacosConfigProperties.getTimeout();
   // 2. 构造Nacos配置Builder
   nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
   
   // 3. dataId前缀按照优先级获取
   // name > prefix > spring.application.name
   String name = nacosConfigProperties.getName();
   String dataIdPrefix = nacosConfigProperties.getPrefix();
   if (StringUtils.isEmpty(dataIdPrefix)) {
      dataIdPrefix = name;
   }
   if (StringUtils.isEmpty(dataIdPrefix)) {
      dataIdPrefix = env.getProperty("spring.application.name");
   }

   // 4. 返回配置按照优先级处理 share < extension < application
   CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
   // share
   loadSharedConfiguration(composite);
   // extention
   loadExtConfiguration(composite);
   // application
   loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
   return composite;
}
复制代码

注意这里的两个优先级:

  • 应用配置的dataId前缀优先级:spring.cloud.nacos.config.name > spring.cloud.nacos.config.prefix > spring.application.name
  • 应用配置、共享配置、扩展配置的优先级:share < extension < application,原因是composite.addFirst,越靠前优先级越高。

读取nacos配置,无非是根据namespace&group&dataId获取配置。这里就关注application配置,share和extention忽略。

// NacosPropertySourceLocator
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
   for (String profile : environment.getActiveProfiles()) {
      String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
      loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
            fileExtension, true);
   }
}
复制代码

应用配置实际上页有三种,优先级从低到高依次是:

  • dataIdPrefix
  • dataIdPrefix.properties
  • dataIdPrefix-profile.properties

NacosPropertySourceLocator只是处理了优先级关系,最终是调用NacosPropertySourceBuilder的build方法返回PropertySource。

// NacosPropertySourceLocator
private void loadNacosDataIfPresent(final CompositePropertySource composite,
      final String dataId, final String group, String fileExtension,
      boolean isRefreshable) {
   // ...
   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) {
   // ...
   return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
         isRefreshable);
}
复制代码

NacosPropertySourceBuilder真正调用了ConfigService获取远程配置,并将配置缓存到NacosPropertySourceRepository。

public class NacosPropertySourceBuilder {
   private ConfigService configService;
   private long timeout;
   NacosPropertySource build(String dataId, String group, String fileExtension,
         boolean isRefreshable) {
      // 1. 调用ConfigService读取远程配置
      List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
            fileExtension);
      // 2. 封装为NacosPropertySource
      NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
            group, dataId, new Date(), isRefreshable);
      // 3. 将配置缓存到NacosPropertySourceRepository
      NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
      // 4. 返回给外部,加入Environment
      return nacosPropertySource;
   }

   private List<PropertySource<?>> loadNacosData(String dataId, String group,
         String fileExtension) {
      String data = configService.getConfig(dataId, group, timeout);
      return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
               fileExtension);
   }
}
复制代码

这里稍微关注一下NacosDataParserHandler负责解析ConfigService返回的配置。

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();
	}
  // 返回单例
  public static NacosDataParserHandler getInstance() {
		return ParserHandler.HANDLER;
	}

	private static class ParserHandler {

		private static final NacosDataParserHandler HANDLER = new NacosDataParserHandler();

	}
}
复制代码

这里关注一下NacosDataParserHandler,是因为这是个spring集成的挺好的案例。往往三方框架会有很多提供给用户的扩展点,比如各种SPI机制。

spring-cloud-starter-alibaba-nacos-config可以让用户实现PropertySourceLoader用于处理不同扩展名的配置文件,只需要用户在自己的spring.factories中添加如下配置:

org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
复制代码

SpringFactoriesLoader.loadFactories方法,可以读到所有PropertySourceLoader的实现类,用于后续处理配置文件解析。

三、动态更新

NacosContextRefresher负责监听nacos配置变更。

Spring容器完全启动以后,NacosContextRefresher会收到ApplicationReadyEvent,此时开启监听

动态更新.png

// NacosContextRefresher
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
   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);
      }
   }
}
复制代码

监听逻辑如下,也是调用ConfigService的addListener方法向nacos服务端发起监听请求。这里注意到Listener的实现,是通过RefreshEvent实现的。

// NacosContextRefresher
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);
               applicationContext.publishEvent(
                     new RefreshEvent(this, null, "Refresh Nacos config"));
            }
         });
   try {
      configService.addListener(dataKey, groupKey, listener);
   }
   catch (NacosException e) {
      log.warn(String.format(
            "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
            groupKey), e);
   }
}
复制代码

也就是说,spring-cloud-starter-alibaba-nacos-config必须配合RefreshScope一同使用才能实现配置动态更新,并且一个配置文件的更新会导致所有RefreshScope里的Bean重新实例化,暂时还不支持单个配置文件的刷新。

// org.springframework.cloud.endpoint.event
public class RefreshEventListener implements SmartApplicationListener {
   private ContextRefresher refresh;
   @Override
   public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
      return ApplicationReadyEvent.class.isAssignableFrom(eventType)
            || RefreshEvent.class.isAssignableFrom(eventType);
   }

   @Override
   public void onApplicationEvent(ApplicationEvent event) {
      if (event instanceof ApplicationReadyEvent) {
         handle((ApplicationReadyEvent) event);
      }
      else if (event instanceof RefreshEvent) {
         handle((RefreshEvent) event);
      }
   }
   public void handle(RefreshEvent event) {
		if (this.ready.get()) { // don't handle events before app is ready
			Set<String> keys = this.refresh.refresh();
		}
	}
}
复制代码

总结

截至目前官方spring-cloud-starter-alibaba-nacos-config仅支持nacos-client版本到1.4.x。

配置注入

ApplicationContext.prepareContext阶段,NacosPropertySourceLocator将Nacos配置注入了Environment。

配置注入.png

NacosPropertySourceLocator读取Nacos配置,底层也是调用了nacos-client的ConfigService里的方法。

配置优先级

利用Spring的CompositePropertySource内部链表结构,越靠前的配置项,优先级越高。

在SpringCloud中Nacos配置分为三类:

  • 应用配置:对应Nacos的一个命名空间下一个分组下的dataId。dataId = {prefix}-{spring.profiles.active}.{file-extension}。对于prefix前缀,优先级spring.cloud.nacos.config.name > spring.cloud.nacos.config.prefix > spring.application.name。应用配置内部也有优先级,从低到高:
    • {prefix}
    • {prefix}-{spring.profiles.active}
    • {prefix}-{spring.profiles.active}.{file-extension}
  • 扩展配置extension-configs:默认不能刷新。
  • 共享配置shared-configs:默认不能刷新。

动态更新

Spring容器完全启动以后,NacosContextRefresher会收到ApplicationReadyEvent,此时开启监听

动态更新.png

NacosContextRefresher循环所有NacosPropertySource,调用nacos-client的ConfigService.addListener注册监听。

// NacosContextRefresher
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
   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);
      }
   }
}
复制代码

当监听器被触发回调时,会发布RefreshEvent事件,只要单个dataId发生变更,将导致所有RefreshScope里的Bean被销毁并重新创建,暂时不支持单个dataId对应的配置重新注入

// NacosContextRefresher
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);
               applicationContext.publishEvent(
                     new RefreshEvent(this, null, "Refresh Nacos config"));
            }
         });
   configService.addListener(dataKey, groupKey, listener);
}                                          
复制代码