找回密码
 立即注册
首页 业界区 业界 Nacos源码—5.Nacos配置中心实现分析

Nacos源码—5.Nacos配置中心实现分析

睁扼妤 2025-6-2 23:12:38
大纲
1.关于Nacos配置中心的几个问题
2.Nacos如何整合SpringBoot读取远程配置
3.Nacos加载读取远程配置数据的源码分析
4.客户端如何感知远程配置数据的变更
5.集群架构下节点间如何同步配置数据
 
1.关于Nacos配置中心的几个问题
问题一:SpringBoot项目启动时如何加载Nacos服务端存储的配置数据?
 
问题二:Nacos配置中心有很多类型的配置数据,它们之间的优先级是怎样的?
 
问题三:在Nacos后台修改配置数据后,客户端是如何实现感知的?
 
问题四:Nacos服务端的配置数据如何存储,集群间会如何同步数据?
 
2.Nacos如何整合SpringBoot读取远程配置
(1)通过PropertySourceLocator将Nacos配置中心整合到SpringBoot
(2)SpringBoot启动时如何执行到PropertySourceLocator扩展接口
(3)SpringBoot如何自动装配NacosPropertySourceLocator实现类
(4)NacosPropertySourceLocator如何加载Nacos服务端的配置数据
 
(1)通过PropertySourceLocator将Nacos配置中心整合到SpringBoot
在SpringBoot的启动过程中,会有一个准备上下文的动作,这个准备上下文动作会加载配置数据。
 
SpringBoot有一个用来收集配置数据的扩展接口PropertySourceLocator,nacos-config正是利用该接口将Nacos配置中心整合到SpringBoot中。
 
(2)SpringBoot启动时如何执行到PropertySourceLocator扩展接口
SpringBoot项目启动时都会使用main()方法。在执行SpringApplication的run()方法的过程中,会调用SpringApplication的prepareContext()方法来准备上下文,然后调用SpringApplication的applyInitializers()方法来初始化应用。
 
由于SpringBoot会有很多个初始化器,所以在SpringApplication的applyInitializers()方法中,会先通过SpringApplication的getInitializers()方法获取初始化器列表,然后循环遍历调用初始化器ApplicationContextInitializer的initialize()方法。
 
在这些初始化器列表initializers中,会有一个名为PropertySourceBootstrapConfiguration的初始化器,所以会调用到PropertySourceBootstrapConfiguration的initialize()方法。
 
在PropertySourceBootstrapConfiguration的initialize()方法中,SpringBoot会获取PropertySourceLocator扩展接口的所有实现类,然后遍历调用PropertySourceLocator实现类的locateCollection()方法。
 
在调用PropertySourceLocator实现类的locateCollection()方法时,会先调用PropertySourceLocator扩展接口的locateCollection()方法,从而才会触发调用PropertySourceLocator实现类实现的locate()方法,比如调用NacosPropertySourceLocator的locate()方法。
  1. @SpringBootApplication
  2. public class StockServiceApplication {
  3.     public static void main(String[] args) {
  4.         SpringApplication.run(StockServiceApplication.class, args);
  5.     }
  6. }
  7. public class SpringApplication {
  8.     private List> initializers;
  9.     ...
  10.    
  11.     public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
  12.         return run(new Class<?>[] { primarySource }, args);
  13.     }
  14.    
  15.     public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
  16.         return new SpringApplication(primarySources).run(args);
  17.     }
  18.    
  19.     //Run the Spring application, creating and refreshing a new
  20.     public ConfigurableApplicationContext run(String... args) {
  21.         StopWatch stopWatch = new StopWatch();
  22.         stopWatch.start();
  23.         ConfigurableApplicationContext context = null;
  24.         Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
  25.         configureHeadlessProperty();
  26.         SpringApplicationRunListeners listeners = getRunListeners(args);
  27.         listeners.starting();
  28.         
  29.         try {
  30.             ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  31.             ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
  32.             configureIgnoreBeanInfo(environment);
  33.             Banner printedBanner = printBanner(environment);
  34.             context = createApplicationContext();
  35.             exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
  36.             //准备上下文
  37.             prepareContext(context, environment, listeners, applicationArguments, printedBanner);
  38.             refreshContext(context);
  39.             afterRefresh(context, applicationArguments);
  40.             stopWatch.stop();
  41.             if (this.logStartupInfo) {
  42.                 new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
  43.             }
  44.             listeners.started(context);
  45.             callRunners(context, applicationArguments);
  46.         } catch (Throwable ex) {
  47.             handleRunFailure(context, ex, exceptionReporters, listeners);
  48.             throw new IllegalStateException(ex);
  49.         }
  50.         try {
  51.             listeners.running(context);
  52.         } catch (Throwable ex) {
  53.             handleRunFailure(context, ex, exceptionReporters, null);
  54.             throw new IllegalStateException(ex);
  55.         }
  56.         return context;
  57.     }
  58.    
  59.     private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
  60.         context.setEnvironment(environment);
  61.         postProcessApplicationContext(context);
  62.         //初始化应用
  63.         applyInitializers(context);
  64.         listeners.contextPrepared(context);
  65.         if (this.logStartupInfo) {
  66.             logStartupInfo(context.getParent() == null);
  67.             logStartupProfileInfo(context);
  68.         }
  69.         
  70.         //Add boot specific singleton beans
  71.         ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
  72.         beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
  73.         if (printedBanner != null) {
  74.             beanFactory.registerSingleton("springBootBanner", printedBanner);
  75.         }
  76.         if (beanFactory instanceof DefaultListableBeanFactory) {
  77.             ((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
  78.         }
  79.         if (this.lazyInitialization) {
  80.             context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
  81.         }
  82.         
  83.         //Load the sources
  84.         Set<Object> sources = getAllSources();
  85.         Assert.notEmpty(sources, "Sources must not be empty");
  86.         load(context, sources.toArray(new Object[0]));
  87.         listeners.contextLoaded(context);
  88.     }
  89.    
  90.     //Apply any {@link ApplicationContextInitializer}s to the context before it is refreshed.
  91.     @SuppressWarnings({ "rawtypes", "unchecked" })
  92.     protected void applyInitializers(ConfigurableApplicationContext context) {
  93.         //getInitializers()方法会获取初始化器列表,然后循环调用初始化器的initialize()方法
  94.         for (ApplicationContextInitializer initializer : getInitializers()) {
  95.             Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
  96.             Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
  97.             initializer.initialize(context);
  98.         }
  99.     }
  100.    
  101.     public Set> getInitializers() {
  102.         return asUnmodifiableOrderedSet(this.initializers);
  103.     }
  104.     ...
  105. }
  106. @Configuration(proxyBeanMethods = false)
  107. @EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
  108. public class PropertySourceBootstrapConfiguration implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
  109.     @Autowired(required = false)
  110.     private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
  111.     ...
  112.    
  113.     @Override
  114.     public void initialize(ConfigurableApplicationContext applicationContext) {
  115.         List<PropertySource<?>> composite = new ArrayList<>();
  116.         AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
  117.         boolean empty = true;
  118.         ConfigurableEnvironment environment = applicationContext.getEnvironment();
  119.         
  120.         //遍历PropertySourceLocator扩展接口的所有实现类this.propertySourceLocators
  121.         for (PropertySourceLocator locator : this.propertySourceLocators) {
  122.             Collection<PropertySource<?>> source = locator.locateCollection(environment);
  123.             if (source == null || source.size() == 0) {
  124.                 continue;
  125.             }
  126.             List<PropertySource<?>> sourceList = new ArrayList<>();
  127.             for (PropertySource<?> p : source) {
  128.                 if (p instanceof EnumerablePropertySource) {
  129.                     EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
  130.                     sourceList.add(new BootstrapPropertySource<>(enumerable));
  131.                 } else {
  132.                     sourceList.add(new SimpleBootstrapPropertySource(p));
  133.                 }
  134.             }
  135.             logger.info("Located property source: " + sourceList);
  136.             composite.addAll(sourceList);
  137.             empty = false;
  138.         }
  139.         
  140.         if (!empty) {
  141.             MutablePropertySources propertySources = environment.getPropertySources();
  142.             String logConfig = environment.resolvePlaceholders("${logging.config:}");
  143.             LogFile logFile = LogFile.get(environment);
  144.             for (PropertySource<?> p : environment.getPropertySources()) {
  145.                 if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
  146.                     propertySources.remove(p.getName());
  147.                 }
  148.             }
  149.             insertPropertySources(propertySources, composite);
  150.             reinitializeLoggingSystem(environment, logConfig, logFile);
  151.             setLogLevels(applicationContext, environment);
  152.             handleIncludedProfiles(environment);
  153.         }
  154.     }
  155.     ...
  156. }
  157. public interface PropertySourceLocator {
  158.     PropertySource<?> locate(Environment environment);
  159.    
  160.     default Collection<PropertySource<?>> locateCollection(Environment environment) {
  161.         return locateCollection(this, environment);
  162.     }
  163.     static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator, Environment environment) {
  164.         //比如调用NacosPropertySourceLocator.locate()方法
  165.         PropertySource<?> propertySource = locator.locate(environment);
  166.         if (propertySource == null) {
  167.             return Collections.emptyList();
  168.         }
  169.         if (CompositePropertySource.class.isInstance(propertySource)) {
  170.             Collection<PropertySource<?>> sources = ((CompositePropertySource) propertySource).getPropertySources();
  171.             List<PropertySource<?>> filteredSources = new ArrayList<>();
  172.             for (PropertySource<?> p : sources) {
  173.                 if (p != null) {
  174.                     filteredSources.add(p);
  175.                 }
  176.             }
  177.             return filteredSources;
  178.         } else {
  179.             return Arrays.asList(propertySource);
  180.         }
  181.     }
  182. }
复制代码
(3)SpringBoot如何自动装配NacosPropertySourceLocator实现类
在nacos-config的spring.factories文件中,可以看到一个自动装配的配置类NacosConfigBootstrapConfiguration。
1.png
NacosConfigBootstrapConfiguration类会创建三个Bean对象。
 
第一个是NacosPropertySourceLocator。这样SpringBoot就能扫描到NacosPropertySourceLocator这个Bean,然后将NacosPropertySourceLocator整合到SpringBoot的启动流程中。在SpringBoot启动时,就会调用NacosPropertySourceLocator的locate()方法。
 
第二个是NacosConfigManager。由于NacosConfigManager的构造方法会创建ConfigService对象,所以在NacosPropertySourceLocator的locate()方法中,可以通过NacosConfigManager的getConfigService()方法获取ConfigService对象。
 
ConfigService是一个接口,定义了获取配置、发布配置、移除配置等方法。ConfigService只有一个实现类NacosConfigService,Nacos配置中心源码的核心其实就是这个NacosConfigService对象。
  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
  3. public class NacosConfigBootstrapConfiguration {
  4.     @Bean
  5.     public NacosPropertySourceLocator nacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
  6.         return new NacosPropertySourceLocator(nacosConfigManager);
  7.     }
  8.    
  9.     @Bean
  10.     @ConditionalOnMissingBean
  11.     public NacosConfigManager nacosConfigManager(NacosConfigProperties nacosConfigProperties) {
  12.         return new NacosConfigManager(nacosConfigProperties);
  13.     }
  14.    
  15.     @Bean
  16.     @ConditionalOnMissingBean
  17.     public NacosConfigProperties nacosConfigProperties() {
  18.         return new NacosConfigProperties();
  19.     }
  20. }
  21. @Order(0)
  22. public class NacosPropertySourceLocator implements PropertySourceLocator {
  23.     private NacosPropertySourceBuilder nacosPropertySourceBuilder;
  24.     private NacosConfigProperties nacosConfigProperties;
  25.     private NacosConfigManager nacosConfigManager;
  26.    
  27.     public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
  28.         this.nacosConfigManager = nacosConfigManager;
  29.         this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
  30.     }
  31.     ...
  32.    
  33.     @Override
  34.     public PropertySource<?> locate(Environment env) {
  35.         nacosConfigProperties.setEnvironment(env);
  36.         ConfigService configService = nacosConfigManager.getConfigService();
  37.         if (null == configService) {
  38.             log.warn("no instance of config service found, can't load config from nacos");
  39.             return null;
  40.         }
  41.         long timeout = nacosConfigProperties.getTimeout();
  42.         nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
  43.         String name = nacosConfigProperties.getName();
  44.         String dataIdPrefix = nacosConfigProperties.getPrefix();
  45.         if (StringUtils.isEmpty(dataIdPrefix)) {
  46.             dataIdPrefix = name;
  47.         }
  48.         if (StringUtils.isEmpty(dataIdPrefix)) {
  49.             dataIdPrefix = env.getProperty("spring.application.name");
  50.         }
  51.         CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
  52.         loadSharedConfiguration(composite);
  53.         loadExtConfiguration(composite);
  54.         loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
  55.         return composite;
  56.     }
  57.     ...
  58. }
  59. public class NacosConfigManager {
  60.     private static ConfigService service = null;
  61.     private NacosConfigProperties nacosConfigProperties;
  62.     public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
  63.         this.nacosConfigProperties = nacosConfigProperties;
  64.         //创建ConfigService对象,其实就是创建NacosConfigService对象
  65.         createConfigService(nacosConfigProperties);
  66.     }
  67.     //使用双重检查创建单例
  68.     static ConfigService createConfigService(NacosConfigProperties nacosConfigProperties) {
  69.         if (Objects.isNull(service)) {
  70.             synchronized (NacosConfigManager.class) {
  71.                 try {
  72.                     if (Objects.isNull(service)) {
  73.                         //通过反射创建ConfigService对象,即NacosConfigService对象,然后给service属性赋值
  74.                         service = NacosFactory.createConfigService(nacosConfigProperties.assembleConfigServiceProperties());
  75.                     }
  76.                 } catch (NacosException e) {
  77.                     log.error(e.getMessage());
  78.                     throw new NacosConnectionFailureException(nacosConfigProperties.getServerAddr(), e.getMessage(), e);
  79.                 }
  80.             }
  81.         }
  82.         return service;
  83.     }
  84.    
  85.     public ConfigService getConfigService() {
  86.         if (Objects.isNull(service)) {
  87.             createConfigService(this.nacosConfigProperties);
  88.         }
  89.         return service;
  90.     }
  91.    
  92.     public NacosConfigProperties getNacosConfigProperties() {
  93.         return nacosConfigProperties;
  94.     }
  95. }
  96. public interface ConfigService {
  97.     ...
  98.     String getConfig(String dataId, String group, long timeoutMs) throws NacosException;
  99.     boolean publishConfig(String dataId, String group, String content) throws NacosException;
  100.     boolean removeConfig(String dataId, String group) throws NacosException;
  101.     ...
  102. }
  103. public class NacosConfigService implements ConfigService {
  104.     ...
  105.     ...
  106. }
复制代码
(4)NacosPropertySourceLocator如何加载Nacos服务端的配置数据
在NacosPropertySourceLocator的locate()方法中,一共会加载三个不同类型的配置数据:共享的、额外的、自身应用的,加载这些配置数据时最终都会调用loadNacosDataIfPresent()方法。
 
执行NacosPropertySourceLocator的loadNacosDataIfPresent()方法时,会通过NacosPropertySourceBuilder创建NacosPropertySource对象。
 
在构建NacosPropertySource对象的过程中,会调用NacosPropertySourceBuilder的loadNacosData()方法加载配置。
 
而执行NacosPropertySourceBuilder的loadNacosData()方法时,最终会调用NacosConfigService的getConfig()方法来加载Nacos配置,即调用NacosConfigService的getConfigInner()方法来加载Nacos配置。
 
在执行NacosConfigService的getConfigInner()方法时,首先会先获取一下本地是否有对应的配置数据,如果有则优先使用本地的。本地数据是在从Nacos配置中心获取到数据后,持久化到本地的数据快照。如果本地没有,才会去发起HTTP请求获取远程Nacos服务端的配置数据。也就是调用ClientWorker的getServerConfig()方法来获取远程配置数据。获取到Nacos配置中心的数据后,会马上将数据持久化到本地。
  1. @Order(0)
  2. public class NacosPropertySourceLocator implements PropertySourceLocator {
  3.     private NacosPropertySourceBuilder nacosPropertySourceBuilder;
  4.     private NacosConfigProperties nacosConfigProperties;
  5.     private NacosConfigManager nacosConfigManager;
  6.    
  7.     public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
  8.         this.nacosConfigManager = nacosConfigManager;
  9.         this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
  10.     }
  11.     ...
  12.    
  13.     @Override
  14.     public PropertySource<?> locate(Environment env) {
  15.         nacosConfigProperties.setEnvironment(env);
  16.         //获取NacosConfigService对象
  17.         ConfigService configService = nacosConfigManager.getConfigService();
  18.         if (null == configService) {
  19.             log.warn("no instance of config service found, can't load config from nacos");
  20.             return null;
  21.         }
  22.         //获取yml配置信息
  23.         long timeout = nacosConfigProperties.getTimeout();
  24.         //传入NacosConfigService对象创建NacosPropertySourceBuilder构造器
  25.         nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
  26.         String name = nacosConfigProperties.getName();
  27.         String dataIdPrefix = nacosConfigProperties.getPrefix();
  28.         if (StringUtils.isEmpty(dataIdPrefix)) {
  29.             dataIdPrefix = name;
  30.         }
  31.         if (StringUtils.isEmpty(dataIdPrefix)) {
  32.             dataIdPrefix = env.getProperty("spring.application.name");
  33.         }
  34.         CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
  35.         //加载共享的配置数据,对应的配置是:spring.cloud.nacos.shared-configs
  36.         loadSharedConfiguration(composite);
  37.         //加载额外的配置数据,对应的配置是:spring.cloud.nacos.extension-configs
  38.         loadExtConfiguration(composite);
  39.         //加载自身应用的配置数据
  40.         loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
  41.         return composite;
  42.     }
  43.    
  44.     private void loadSharedConfiguration(CompositePropertySource compositePropertySource) {
  45.         List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties.getSharedConfigs();
  46.         if (!CollectionUtils.isEmpty(sharedConfigs)) {
  47.             checkConfiguration(sharedConfigs, "shared-configs");
  48.             loadNacosConfiguration(compositePropertySource, sharedConfigs);
  49.         }
  50.     }
  51.    
  52.     private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
  53.         List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties.getExtensionConfigs();
  54.         if (!CollectionUtils.isEmpty(extConfigs)) {
  55.             checkConfiguration(extConfigs, "extension-configs");
  56.             loadNacosConfiguration(compositePropertySource, extConfigs);
  57.         }
  58.     }
  59.    
  60.     private void loadNacosConfiguration(final CompositePropertySource composite, List<NacosConfigProperties.Config> configs) {
  61.         for (NacosConfigProperties.Config config : configs) {
  62.             loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(), NacosDataParserHandler.getInstance().getFileExtension(config.getDataId()), config.isRefresh());
  63.         }
  64.     }
  65.    
  66.     private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix, NacosConfigProperties properties, Environment environment) {
  67.         String fileExtension = properties.getFileExtension();
  68.         String nacosGroup = properties.getGroup();
  69.         //load directly once by default
  70.         loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup, fileExtension, true);
  71.         //load with suffix, which have a higher priority than the default
  72.         loadNacosDataIfPresent(compositePropertySource, dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
  73.         //Loaded with profile, which have a higher priority than the suffix
  74.         for (String profile : environment.getActiveProfiles()) {
  75.             String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
  76.             loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup, fileExtension, true);
  77.         }
  78.     }
  79.    
  80.     //加载三个不同类型的配置数据最终都会调用到loadNacosDataIfPresent()方法
  81.     private void loadNacosDataIfPresent(final CompositePropertySource composite, final String dataId, final String group, String fileExtension, boolean isRefreshable) {
  82.         ...
  83.         //加载Nacos中的配置数据
  84.         NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group, fileExtension, isRefreshable);
  85.         //把从Nacos中读取到的配置添加到Spring容器中
  86.         this.addFirstPropertySource(composite, propertySource, false);
  87.     }
  88.    
  89.     private NacosPropertySource loadNacosPropertySource(final String dataId, final String group, String fileExtension, boolean isRefreshable) {
  90.         ...
  91.         //创建NacosPropertySourceBuilder构造器时已传入NacosConfigService对象
  92.         return nacosPropertySourceBuilder.build(dataId, group, fileExtension, isRefreshable);
  93.     }
  94.     ...
  95. }
  96. public class NacosPropertySourceBuilder {
  97.     private ConfigService configService;
  98.     private long timeout;
  99.    
  100.     public NacosPropertySourceBuilder(ConfigService configService, long timeout) {
  101.         //创建NacosPropertySourceBuilder构造器时已传入NacosConfigService对象
  102.         this.configService = configService;
  103.         this.timeout = timeout;
  104.     }
  105.    
  106.     NacosPropertySource build(String dataId, String group, String fileExtension, boolean isRefreshable) {
  107.         //加载Nacos中的配置数据
  108.         List<PropertySource<?>> propertySources = loadNacosData(dataId, group, fileExtension);
  109.         NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources, group, dataId, new Date(), isRefreshable);
  110.         NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
  111.         return nacosPropertySource;
  112.     }
  113.    
  114.     private List<PropertySource<?>> loadNacosData(String dataId, String group, String fileExtension) {
  115.         String data = null;
  116.         try {
  117.             //调用NacosConfigService.getConfig()方法
  118.             data = configService.getConfig(dataId, group, timeout);
  119.             ...
  120.             return NacosDataParserHandler.getInstance().parseNacosData(dataId, data, fileExtension);
  121.         } catch (NacosException e) {
  122.             ...
  123.         }
  124.         return Collections.emptyList();
  125.     }
  126. }
  127. public class NacosConfigService implements ConfigService {
  128.     private final ClientWorker worker;
  129.     ...
  130.    
  131.     @Override
  132.     public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
  133.         return getConfigInner(namespace, dataId, group, timeoutMs);
  134.     }
  135.    
  136.     private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
  137.         group = null2defaultGroup(group);
  138.         ParamUtils.checkKeyParam(dataId, group);
  139.         ConfigResponse cr = new ConfigResponse();
  140.    
  141.         cr.setDataId(dataId);
  142.         cr.setTenant(tenant);
  143.         cr.setGroup(group);
  144.    
  145.         //优先使用本地配置
  146.         String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
  147.         if (content != null) {
  148.             LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content));
  149.             cr.setContent(content);
  150.             configFilterChainManager.doFilter(null, cr);
  151.             content = cr.getContent();
  152.             return content;
  153.         }
  154.       
  155.         //如果本地配置没有,才会调用远程Nacos服务端的配置
  156.         try {
  157.             //通过ClientWorker.getServerConfig()方法来读取远程配置数据
  158.             String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
  159.             cr.setContent(ct[0]);
  160.             configFilterChainManager.doFilter(null, cr);
  161.             content = cr.getContent();
  162.             return content;
  163.         } catch (NacosException ioe) {
  164.             if (NacosException.NO_RIGHT == ioe.getErrCode()) {
  165.                 throw ioe;
  166.             }
  167.             LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}", agent.getName(), dataId, group, tenant, ioe.toString());
  168.         }
  169.         LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content));
  170.         content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
  171.         cr.setContent(content);
  172.         configFilterChainManager.doFilter(null, cr);
  173.         content = cr.getContent();
  174.         return content;
  175.     }
  176.     ...
  177. }
  178. public class ClientWorker implements Closeable {
  179.     ...
  180.     public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
  181.         String[] ct = new String[2];
  182.         if (StringUtils.isBlank(group)) {
  183.             group = Constants.DEFAULT_GROUP;
  184.         }
  185.    
  186.         HttpRestResult<String> result = null;
  187.         try {
  188.             //组装参数
  189.             Map<String, String> params = new HashMap<String, String>(3);
  190.             if (StringUtils.isBlank(tenant)) {
  191.                 params.put("dataId", dataId);
  192.                 params.put("group", group);
  193.             } else {
  194.                 params.put("dataId", dataId);
  195.                 params.put("group", group);
  196.                 params.put("tenant", tenant);
  197.             }
  198.             //发起服务调用HTTP请求,请求地址是:/v1/cs/configs
  199.             result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
  200.         } catch (Exception ex) {
  201.             String message = String.format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", agent.getName(), dataId, group, tenant);
  202.             LOGGER.error(message, ex);
  203.             throw new NacosException(NacosException.SERVER_ERROR, ex);
  204.         }
  205.    
  206.         switch (result.getCode()) {
  207.             //如果请求成功
  208.             case HttpURLConnection.HTTP_OK:
  209.                 //将数据持久化到本地
  210.                 LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
  211.                 ct[0] = result.getData();
  212.                 if (result.getHeader().getValue(CONFIG_TYPE) != null) {
  213.                     ct[1] = result.getHeader().getValue(CONFIG_TYPE);
  214.                 } else {
  215.                     ct[1] = ConfigType.TEXT.getType();
  216.                 }
  217.                 return ct;
  218.             ...
  219.         }
  220.     }
  221.     ...
  222. }
复制代码
(5)总结
Nacos是按如下方式整合SpringBoot去读取远程配置数据的:在SpringBoot项目启动的过程中,有一个步骤是准备上下文,该步骤中就会去加载配置文件,加载配置文件时就会调用Nacos提供的获取配置数据的HTTP接口,最终完成从Nacos服务端拉获取置数据的整个流程,并且在获取到配置数据后会将数据持久化到本地。
2.png
 
3.Nacos加载读取远程配置数据的源码分析
(1)配置文件的类型与使用介绍
(2)远程配置文件的加载顺序源码
(3)远程配置文件的读取源码分析
(4)总结
 
(1)配置文件的类型与使用介绍
SpringBoot在启动过程中,会调用Nacos实现的加载配置文件扩展接口PropertySourceLocator,从而实现加载Nacos配置中心的远程配置文件。
 
在Nacos实现的扩展接口PropertySourceLocator中,便会加载好几个不同类型的配置文件,这些配置文件会存在优先级关系:自身应用的配置文件 > 额外的配置文件 > 共享的配置文件。
 
一.读取自身应用的配置文件
第一种情况:如下的项目yaml配置是最简单的配置,只需指定Nacos配置中心的地址。在读取Nacos配置中心文件时,是通过微服务名称去加载的,所以只需要在Nacos后台创建一个stock-service配置文件就可以读取到。
  1. spring:
  2.     application:
  3.         name: stock-service
  4.     cloud:
  5.         nacos:
  6.             # 配置中心
  7.             config:
  8.                 server-addr: http://124.223.102.236:8848
复制代码
3.png
第二种情况:但项目中一般会指定配置文件的类型,所以可以在如下项目yaml配置中把配置文件类型加上。在项目yaml配置中加上配置文件类型后,会使用使用带后缀的配置文件。并且会覆盖之前的配置,说明带文件后缀的配置文件的优先级更高。
  1. spring:
  2.     application:
  3.         name: stock-service
  4.     cloud:
  5.         nacos:
  6.             # 配置中心
  7.             config:
  8.                 server-addr: http://124.223.102.236:8848            # 配置文件类型            file-extension: yaml
复制代码
4.png
第三种情况:当然公司配置文件一般也会区分环境的。测试环境有测试环境的配置文件,生产环境有生产环境的配置文件。在如下的项目yaml配置中指定使用区分了环境的配置文件,这时带有环境变量的配置文件,比前面两个配置文件优先级更高。
  1. spring:
  2.     application:
  3.         name: stock-service
  4.     profiles:
  5.         # 测试环境
  6.         active: test
  7.     cloud:
  8.         nacos:
  9.             # 配置中心
  10.             config:
  11.                 server-addr: http://124.223.102.236:8848
  12.             # 配置文件类型
  13.             file-extension: yaml
复制代码
5.png
总结:读取自身应用的配置文件,如上三种情况,会存在优先级关系。通过微服务名称简单去获取stock-service配置文件的优先级最低,指定配置文件类型去获取stock-service配置文件的优先级比前者高,指定项目环境去获取stock-service配置文件的优先级是最高。
 
二.读取共享的配置文件
实际中会存在多个业务系统都共用同一数据库、Redis等中间件,这时不宜把每个中间件信息都配置到每个业务系统中,而是应该统一集中管理。比如在一个共享配置文件中配置,各业务系统使用共享配置文件即可。
 
项目yaml配置指定读取Nacos的共享配置文件如下:在spring.cloud.nacos.config配置下可以指定shared-configs配置。shared-configs配置是一个数组类型,表示可以配置多个共享配置文件,所以可以通过shared-configs配置将一些中间件配置管理起来。但要注意共享配置文件里的配置不要和自身应用配置文件里的配置重复,因为自身应用配置文件比共享配置文件的优先级高。
 
当然除了自身应用配置文件、共享配置文件外,还有一种额外的配置文件。如果一些配置不适合放在前两种配置文件,可以放到额外的配置文件中。
  1. spring:
  2.     application:
  3.         name: stock-service
  4.     profiles:
  5.         # 测试环境
  6.         active: test
  7.     cloud:
  8.         nacos:
  9.             # 配置中心
  10.             config:
  11.                 server-addr: http://124.223.102.236:8848
  12.                 # 配置文件类型
  13.                 file-extension: yaml
  14.                 # 共享配置文件
  15.                 shared-configs:
  16.                     dataId: common-mysql.yaml
  17.                     group: DEFAULT_GROUP
  18.                     # 中间件配置一般不需要刷新
  19.                     refresh: false
复制代码
6.png
(2)远程配置文件的加载顺序源码
在NacosPropertySourceLocator的locate()方法中,最先加载的配置文件,相同配置项会被后面加载的配置文件给覆盖掉。因为这些配置文件本身就是kv形式存储,所以共享配置文件优先级最低。自身应用配置文件 > 额外配置文件 > 共享配置文件。
 
在NacosPropertySourceLocator的loadApplicationConfiguration()方法中,加载自身应用的配置文件的优先级为:"微服务名"的配置文件 < "微服务名.后缀名"的配置文件 < "微服务-环境变量名.后缀名"的配置文件。同样对于相同配置项,先加载的会被后加载的替换掉。
 
但不管获取的是哪一种类型的配置文件,最终都调用NacosPropertySourceLocator的loadNacosDataIfPresent()方法。在这个方法里最终会通过HTTP方式去获取Nacos服务端的配置文件数据,请求的HTTP地址是"/v1/cs/configs",获得数据后会马上持久化到本地。
  1. @Order(0)
  2. public class NacosPropertySourceLocator implements PropertySourceLocator {
  3.     ...
  4.     @Override
  5.     public PropertySource<?> locate(Environment env) {
  6.         nacosConfigProperties.setEnvironment(env);
  7.         //获取NacosConfigService对象
  8.         ConfigService configService = nacosConfigManager.getConfigService();
  9.         if (null == configService) {
  10.             log.warn("no instance of config service found, can't load config from nacos");
  11.             return null;
  12.         }
  13.         //获取yml配置信息
  14.         long timeout = nacosConfigProperties.getTimeout();
  15.         //传入NacosConfigService对象创建NacosPropertySourceBuilder构造器
  16.         nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
  17.         String name = nacosConfigProperties.getName();
  18.         String dataIdPrefix = nacosConfigProperties.getPrefix();
  19.         if (StringUtils.isEmpty(dataIdPrefix)) {
  20.             dataIdPrefix = name;
  21.         }
  22.         if (StringUtils.isEmpty(dataIdPrefix)) {
  23.             dataIdPrefix = env.getProperty("spring.application.name");
  24.         }
  25.         CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
  26.         //1.加载共享的配置数据,对应的配置是:spring.cloud.nacos.shared-configs
  27.         loadSharedConfiguration(composite);
  28.         //2.加载额外的配置数据,对应的配置是:spring.cloud.nacos.extension-configs
  29.         loadExtConfiguration(composite);
  30.         //3.加载自身应用的配置数据
  31.         loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
  32.         return composite;
  33.     }
  34.    
  35.     private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix, NacosConfigProperties properties, Environment environment) {
  36.         String fileExtension = properties.getFileExtension();
  37.         String nacosGroup = properties.getGroup();
  38.         //1.加载"微服务名"的配置文件
  39.         loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup, fileExtension, true);
  40.         //2.加载"微服务名.后缀名"的配置文件
  41.         loadNacosDataIfPresent(compositePropertySource, dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
  42.         //3.加载"微服务-环境变量名.后缀名"的配置文件,因为环境变量可以配置多个,所以这里是循环
  43.         for (String profile : environment.getActiveProfiles()) {
  44.             String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
  45.             loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup, fileExtension, true);
  46.         }
  47.     }
  48.    
  49.     private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
  50.         List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties.getExtensionConfigs();
  51.         if (!CollectionUtils.isEmpty(extConfigs)) {
  52.             checkConfiguration(extConfigs, "extension-configs");
  53.             loadNacosConfiguration(compositePropertySource, extConfigs);
  54.         }
  55.     }
  56.    
  57.     private void loadNacosConfiguration(final CompositePropertySource composite, List<NacosConfigProperties.Config> configs) {
  58.         for (NacosConfigProperties.Config config : configs) {
  59.             loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(), NacosDataParserHandler.getInstance().getFileExtension(config.getDataId()), config.isRefresh());
  60.         }
  61.     }
  62.     ...
  63. }
复制代码
(3)远程配置文件的读取源码
Nacos服务端处理HTTP请求"/v1/cs/configs"的入口是:ConfigController的getConfig()方法。
 
执行ConfigController的getConfig()方法时,会调用ConfigServletInner的doGetConfig()方法,而该方法的核心代码就是通过DiskUtil的targetBetaFile()方法获取磁盘上的文件数据。
 
所以Nacos客户端发送HTTP请求来获取配置文件数据时,Nacos服务端并不是去数据库中获取对应的配置文件数据,而是直接读取本地磁盘文件的配置文件数据然后返回给客户端。那么Nacos服务端是什么时候将配置文件数据持久化到本地磁盘文件的?
 
其实在执行ExternalDumpService的init()方法进行初始化Bean实例时,会调用DumpService的dumpOperate()方法,然后会调用DumpService的dumpConfigInfo()方法,接着会调用DumpAllProcessor的process()方法查询数据库。
 
DumpAllProcessor的process()方法会做两件事:一是通过分页查询数据库中的config_info表数据,二是将查询到的数据持久化到本地磁盘文件中。
  1. @RestController
  2. @RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
  3. public class ConfigController {
  4.     private final ConfigServletInner inner;
  5.     ...
  6.     //Get configure board infomation fail.
  7.     @GetMapping
  8.     @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
  9.     public void getConfig(HttpServletRequest request, HttpServletResponse response,
  10.             @RequestParam("dataId") String dataId, @RequestParam("group") String group,
  11.             @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
  12.             @RequestParam(value = "tag", required = false) String tag)
  13.             throws IOException, ServletException, NacosException {
  14.         //check tenant
  15.         ParamUtils.checkTenant(tenant);
  16.         tenant = NamespaceUtil.processNamespaceParameter(tenant);
  17.         //check params
  18.         ParamUtils.checkParam(dataId, group, "datumId", "content");
  19.         ParamUtils.checkParam(tag);
  20.    
  21.         final String clientIp = RequestUtil.getRemoteIp(request);
  22.         inner.doGetConfig(request, response, dataId, group, tenant, tag, clientIp);
  23.     }
  24.     ...
  25. }
  26. @Service
  27. public class ConfigServletInner {
  28.     ...
  29.     public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group,
  30.             String tenant, String tag, String clientIp)  throws IOException, ServletException {
  31.         ...
  32.         File file = null;
  33.         //核心代码:获取磁盘上的文件数据
  34.         file = DiskUtil.targetBetaFile(dataId, group, tenant);
  35.         ...
  36.     }
  37.     ...
  38. }
  39. @Conditional(ConditionOnExternalStorage.class)
  40. @Component
  41. public class ExternalDumpService extends DumpService {
  42.     ...
  43.     @PostConstruct
  44.     @Override
  45.     protected void init() throws Throwable {
  46.         dumpOperate(processor, dumpAllProcessor, dumpAllBetaProcessor, dumpAllTagProcessor);
  47.     }
  48.     ...
  49. }
  50. //Dump data service.
  51. public abstract class DumpService {
  52.     protected DumpProcessor processor;
  53.     protected DumpAllProcessor dumpAllProcessor;
  54.     protected DumpAllBetaProcessor dumpAllBetaProcessor;
  55.     protected DumpAllTagProcessor dumpAllTagProcessor;
  56.     protected final PersistService persistService;
  57.     protected final ServerMemberManager memberManager;
  58.     ...
  59.     protected void dumpOperate(DumpProcessor processor, DumpAllProcessor dumpAllProcessor, DumpAllBetaProcessor dumpAllBetaProcessor, DumpAllTagProcessor dumpAllTagProcessor) throws NacosException {
  60.         ...
  61.         //持久化配置文件到磁盘
  62.         dumpConfigInfo(dumpAllProcessor);
  63.         ...
  64.     }
  65.    
  66.     private void dumpConfigInfo(DumpAllProcessor dumpAllProcessor) throws IOException {
  67.         ...
  68.         //查询数据库配置
  69.         dumpAllProcessor.process(new DumpAllTask());
  70.         ...
  71.     }
  72.     ...
  73. }
  74. public class DumpAllProcessor implements NacosTaskProcessor {
  75.     static final int PAGE_SIZE = 1000;
  76.     final DumpService dumpService;
  77.     final PersistService persistService;
  78.    
  79.     public DumpAllProcessor(DumpService dumpService) {
  80.         this.dumpService = dumpService;
  81.         this.persistService = dumpService.getPersistService();
  82.     }
  83.    
  84.     @Override
  85.     public boolean process(NacosTask task) {
  86.         //查询最大ID
  87.         long currentMaxId = persistService.findConfigMaxId();
  88.         long lastMaxId = 0;
  89.         while (lastMaxId < currentMaxId) {
  90.             //分页查询配置信息
  91.             Page<ConfigInfoWrapper> page = persistService.findAllConfigInfoFragment(lastMaxId, PAGE_SIZE);
  92.             if (page != null && page.getPageItems() != null && !page.getPageItems().isEmpty()) {
  93.                 for (ConfigInfoWrapper cf : page.getPageItems()) {
  94.                     long id = cf.getId();
  95.                     lastMaxId = id > lastMaxId ? id : lastMaxId;
  96.                     if (cf.getDataId().equals(AggrWhitelist.AGGRIDS_METADATA)) {
  97.                         AggrWhitelist.load(cf.getContent());
  98.                     }
  99.                     if (cf.getDataId().equals(ClientIpWhiteList.CLIENT_IP_WHITELIST_METADATA)) {
  100.                         ClientIpWhiteList.load(cf.getContent());
  101.                     }
  102.                     if (cf.getDataId().equals(SwitchService.SWITCH_META_DATAID)) {
  103.                         SwitchService.load(cf.getContent());
  104.                     }
  105.                     //把查询到的配置信息写入到磁盘
  106.                     boolean result = ConfigCacheService.dump(cf.getDataId(), cf.getGroup(), cf.getTenant(), cf.getContent(), cf.getLastModified(), cf.getType());               
  107.                     final String content = cf.getContent();
  108.                     final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
  109.                     LogUtil.DUMP_LOG.info("[dump-all-ok] {}, {}, length={}, md5={}", GroupKey2.getKey(cf.getDataId(), cf.getGroup()), cf.getLastModified(), content.length(), md5);
  110.                 }
  111.                 DEFAULT_LOG.info("[all-dump] {} / {}", lastMaxId, currentMaxId);
  112.             } else {
  113.                 lastMaxId += PAGE_SIZE;
  114.             }
  115.         }
  116.         return true;
  117.     }
  118. }
  119. public class ConfigCacheService {
  120.     ...
  121.     //Save config file and update md5 value in cache.
  122.     public static boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs, String type) {
  123.         String groupKey = GroupKey2.getKey(dataId, group, tenant);
  124.         CacheItem ci = makeSure(groupKey);
  125.         ci.setType(type);
  126.         final int lockResult = tryWriteLock(groupKey);
  127.         assert (lockResult != 0);
  128.    
  129.         if (lockResult < 0) {
  130.             DUMP_LOG.warn("[dump-error] write lock failed. {}", groupKey);
  131.             return false;
  132.         }
  133.    
  134.         try {
  135.             final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
  136.             if (md5.equals(ConfigCacheService.getContentMd5(groupKey))) {
  137.                 DUMP_LOG.warn("[dump-ignore] ignore to save cache file. groupKey={}, md5={}, lastModifiedOld={}, " + "lastModifiedNew={}", groupKey, md5, ConfigCacheService.getLastModifiedTs(groupKey), lastModifiedTs);
  138.             } else if (!PropertyUtil.isDirectRead()) {
  139.                 //调用持久化到本地磁盘的方法
  140.                 DiskUtil.saveToDisk(dataId, group, tenant, content);
  141.             }
  142.             updateMd5(groupKey, md5, lastModifiedTs);
  143.             return true;
  144.         } catch (IOException ioe) {
  145.             DUMP_LOG.error("[dump-exception] save disk error. " + groupKey + ", " + ioe.toString(), ioe);
  146.             if (ioe.getMessage() != null) {
  147.                 String errMsg = ioe.getMessage();
  148.                 if (NO_SPACE_CN.equals(errMsg) || NO_SPACE_EN.equals(errMsg) || errMsg.contains(DISK_QUATA_CN) || errMsg.contains(DISK_QUATA_EN)) {
  149.                     //Protect from disk full.
  150.                     FATAL_LOG.error("磁盘满自杀退出", ioe);
  151.                     System.exit(0);
  152.                 }
  153.             }
  154.             return false;
  155.         } finally {
  156.             releaseWriteLock(groupKey);
  157.         }
  158.     }
  159.     ...
  160. }
复制代码
(4)总结
一.不同类型配置文件的优先级:自身应用配置文件 > 额外配置文件 > 共享配置文件。
 
二.自身应用配置文件的优先级:"微服务名"的配置文件 < "微服务名.后缀名"的配置文件 < "微服务-环境变量名.后缀名"的配置文件。
 
三.Nacos客户端向服务端获取配置数据的流程
客户端向服务端查询配置数据时,服务端会直接获取其本地磁盘文件中的配置进行返回。
 
服务端本地磁盘文件上的配置数据,是在服务端启动时查询数据库数据,然后持久化到本地磁盘上的。
 
所以如果直接手动修改数据库中的配置信息,客户端是不生效的,因为客户端向服务端获取配置信息时并不是读取数据库的。
7.png
 
4.客户端如何感知远程配置数据的变更
(1)ConfigService对象使用介绍
(2)客户端注册监听器的源码
(3)回调监听器的方法的源码
 
(1)ConfigService对象使用介绍
ConfigService是一个接口,定义了获取配置、发布配置、移除配置等方法。ConfigService只有一个实现类NacosConfigService,Nacos配置中心源码的核心其实就是这个NacosConfigService对象。
 
步骤一:手动创建ConfigService对象
首先定义好基本的Nacos信息,然后利用NacosFactory工厂类来创建ConfigService对象。
  1. public class Demo {
  2.     public static void main(String[] args) throws Exception {
  3.         //步骤一:配置信息
  4.         String serverAddr = "124.223.102.236:8848";
  5.         String dataId = "stock-service-test.yaml";
  6.         String group = "DEFAULT_GROUP";
  7.         Properties properties = new Properties();
  8.         properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
  9.         //步骤一:获取配置中心服务
  10.         ConfigService configService = NacosFactory.createConfigService(properties);
  11.     }
  12. }
复制代码
步骤二:获取配置、发布配置
创建好ConfigService对象后,就可以使用ConfigService对象的getConfig()方法来获取配置信息,还可以使用ConfigService对象的publishConfig()方法来发布配置信息。
 
如下Demo先获取一次配置数据,然后发布新配置,紧接着重新获取数据。发现第二次获取的配置数据已发生变化,从而也说明发布配置成功了。
  1. public class Demo {
  2.     public static void main(String[] args) throws Exception {
  3.         //步骤一:配置信息
  4.         String serverAddr = "124.223.102.236:8848";
  5.         String dataId = "stock-service-test.yaml";
  6.         String group = "DEFAULT_GROUP";
  7.         Properties properties = new Properties();
  8.         properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
  9.         //步骤一:获取配置中心服务
  10.         ConfigService configService = NacosFactory.createConfigService(properties);
  11.      
  12.         //步骤二:从配置中心获取配置
  13.         String content = configService.getConfig(dataId, group, 5000);
  14.         System.out.println("发布配置前" + content);
  15.       
  16.         //步骤二:发布配置
  17.         configService.publishConfig(dataId, group, "userName: userName被修改了", ConfigType.PROPERTIES.getType());
  18.         Thread.sleep(300L);
  19.         //步骤二:从配置中心获取配置
  20.         content = configService.getConfig(dataId, group, 5000);
  21.         System.out.println("发布配置后" + content);
  22.     }
  23. }
复制代码
步骤三:添加监听器
可以使用ConfigService对象的addListener()方法来添加监听器。通过dataId + group这两个参数,就可以注册一个监听器。当dataId + group对应的配置在服务端发生改变时,客户端的监听器就可以马上感知并对配置数据进行刷新。
  1. public class Demo {
  2.     public static void main(String[] args) throws Exception {
  3.         //步骤一:配置信息
  4.         String serverAddr = "124.223.102.236:8848";
  5.         String dataId = "stock-service-test.yaml";
  6.         String group = "DEFAULT_GROUP";
  7.         Properties properties = new Properties();
  8.         properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
  9.         //步骤一:获取配置中心服务
  10.         ConfigService configService = NacosFactory.createConfigService(properties);
  11.      
  12.         //步骤二:从配置中心获取配置
  13.         String content = configService.getConfig(dataId, group, 5000);
  14.         System.out.println("发布配置前" + content);
  15.       
  16.         //步骤二:发布配置
  17.         configService.publishConfig(dataId, group, "userName: userName被修改了", ConfigType.PROPERTIES.getType());
  18.         Thread.sleep(300L);
  19.         //步骤二:从配置中心获取配置
  20.         content = configService.getConfig(dataId, group, 5000);
  21.         System.out.println("发布配置后" + content);
  22.      
  23.         //步骤三:注册监听器
  24.         configService.addListener(dataId, group, new Listener() {
  25.             @Override
  26.             public void receiveConfigInfo(String configInfo) {
  27.                 System.out.println("感知配置变化:" + configInfo);
  28.             }
  29.             @Override
  30.             public Executor getExecutor() {
  31.                 return null;
  32.             }
  33.         });
  34.         //阻断进程关闭
  35.         Thread.sleep(Integer.MAX_VALUE);
  36.     }
  37. }
复制代码
(2)客户端注册监听器的源码
Nacos客户端是什么时候为dataId + group注册监听器的?
 
在nacos-config下的spring.factories文件中,有一个自动装配的配置类NacosConfigAutoConfiguration,在该配置类中定义了一个NacosContextRefresher对象,而NacosContextRefresher对象会监听ApplicationReadyEvent事件。
 
在NacosContextRefresher的onApplicationEvent()方法中,会执行registerNacosListenersForApplications()方法,这个方法中会遍历每一个dataId + group注册Nacos监听器。
 
对于每一个dataId + group,则通过调用registerNacosListener()方法来进行Nacos监听器的注册,也就是最终调用ConfigService对象的addListener()方法来注册监听器。
  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
  3. public class NacosConfigAutoConfiguration {
  4.     ...
  5.     @Bean
  6.     public NacosContextRefresher nacosContextRefresher(NacosConfigManager nacosConfigManager, NacosRefreshHistory nacosRefreshHistory) {
  7.         return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
  8.     }
  9.     ...
  10. }
  11. public class NacosContextRefresher implements ApplicationListener, ApplicationContextAware {
  12.     private final ConfigService configService;
  13.     ...
  14.    
  15.     @Override
  16.     public void onApplicationEvent(ApplicationReadyEvent event) {
  17.         //many Spring context
  18.         if (this.ready.compareAndSet(false, true)) {
  19.             this.registerNacosListenersForApplications();
  20.         }
  21.     }
  22.    
  23.     //register Nacos Listeners.
  24.     private void registerNacosListenersForApplications() {
  25.         if (isRefreshEnabled()) {
  26.             //获取全部的配置
  27.             for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {
  28.                 //判断当前配置是否需要刷新
  29.                 if (!propertySource.isRefreshable()) {
  30.                     continue;
  31.                 }
  32.                 String dataId = propertySource.getDataId();
  33.                 //注册监听器
  34.                 registerNacosListener(propertySource.getGroup(), dataId);
  35.             }
  36.         }
  37.     }
  38.    
  39.     private void registerNacosListener(final String groupKey, final String dataKey) {
  40.         String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
  41.         Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() {
  42.             @Override
  43.             public void innerReceive(String dataId, String group, String configInfo) {
  44.                 //监听器的回调方法处理逻辑
  45.                 refreshCountIncrement();
  46.                 //记录刷新历史
  47.                 nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
  48.                 //发布RefreshEvent刷新事件
  49.                 applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
  50.                 if (log.isDebugEnabled()) {
  51.                     log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
  52.                 }
  53.             }
  54.         });
  55.         try {
  56.             //注册监听器
  57.             configService.addListener(dataKey, groupKey, listener);
  58.         } catch (NacosException e) {
  59.             log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), e);
  60.         }
  61.     }
  62.     ...
  63. }
复制代码
(3)回调监听器的方法的源码
给每一个dataId + group注册Nacos监听器后,当Nacos服务端的配置文件发生变更时,就会回调监听器的方法,也就是会触发调用AbstractSharedListener的innerReceive()方法。然后调用applicationContext.publishEvent()发布RefreshEvent刷新事件,而发布的RefreshEvent刷新事件会被RefreshEventListener类来处理。
 
RefreshEventListener类不是Nacos中的类了,而是SpringCloud的类。它在处理刷新事件时,会销毁被@RefreshScope注解修饰的类的Bean,也就是会调用添加了@RefreshScope注解的类的destroy()方法。把Bean实例销毁后,后面需要用到这个Bean时才重新进行创建。重新进行创建的时候,就会获取最新的配置文件,从而完成刷新效果。
 
(4)总结
客户端注册Nacos监听器,服务端修改配置后,客户端刷新配置的流程:
8.webp
 
5.集群架构下节点间如何同步配置数据
(1)Nacos控制台的配置管理模块
(2)变更配置数据时的源码
(3)集群节点间的配置数据变更同步
(4)服务端通知客户端配置数据已变更
(5)总结
 
(1)Nacos控制台的配置管理模块
在这个模块中,可以通过配置列表维护我们的配置文件,可以通过历史版本找到配置的发布记录,并且支持回滚操作。当编辑配置文件时,客户端可以及时感知变化并刷新其配置文件。当服务端通知客户端配置变更时,也会通知集群节点进行数据同步。
9.webp
当用户在Nacos控制台点击确认发布按钮时,Nacos会大概进行如下处理:
一.修改配置文件数据
二.保存配置发布历史
三.通知并触发客户端监听事件进行配置文件变更
四.通知集群对配置文件进行变更
 
点击确认发布按钮时,会发起HTTP请求,地址为"/nacos/v1/cs/configs"。通过请求地址可知处理入口是ConfigController的publishConfig()方法。
 
(2)变更配置数据时的源码
ConfigController的publishConfig()方法中的两行核心代码是:一.新增或修改配置数据的PersistService的insertOrUpdate()方法,二.发布配置变更事件的ConfigChangePublisher的notifyConfigChange()方法。
 
一.新增或者修改配置数据
其中PersistService有两个实现类:一是EmbeddedStoragePersistServiceImpl,它是Nacos内置的Derby数据库。二是ExternalStoragePersistServiceImpl,它是Nacos外置数据库如MySQL。
 
在ExternalStoragePersistServiceImpl的insertOrUpdate()方法中,如果执行ExternalStoragePersistServiceImpl的updateConfigInfo()方法,那么会先查询对应的配置,然后更新配置,最后保存配置历史。
  1. @RestController
  2. @RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
  3. public class ConfigController {
  4.     private final PersistService persistService;
  5.     ...
  6.    
  7.     @PostMapping
  8.     @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
  9.     public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response,
  10.             @RequestParam(value = "dataId") String dataId, @RequestParam(value = "group") String group,
  11.             @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
  12.             @RequestParam(value = "content") String content, @RequestParam(value = "tag", required = false) String tag,
  13.             @RequestParam(value = "appName", required = false) String appName,
  14.             @RequestParam(value = "src_user", required = false) String srcUser,
  15.             @RequestParam(value = "config_tags", required = false) String configTags,
  16.             @RequestParam(value = "desc", required = false) String desc,
  17.             @RequestParam(value = "use", required = false) String use,
  18.             @RequestParam(value = "effect", required = false) String effect,
  19.             @RequestParam(value = "type", required = false) String type,
  20.             @RequestParam(value = "schema", required = false) String schema) throws NacosException {
  21.    
  22.         final String srcIp = RequestUtil.getRemoteIp(request);
  23.         final String requestIpApp = RequestUtil.getAppName(request);
  24.         srcUser = RequestUtil.getSrcUserName(request);
  25.         //check type
  26.         if (!ConfigType.isValidType(type)) {
  27.             type = ConfigType.getDefaultType().getType();
  28.         }
  29.         //check tenant
  30.         ParamUtils.checkTenant(tenant);
  31.         ParamUtils.checkParam(dataId, group, "datumId", content);
  32.         ParamUtils.checkParam(tag);
  33.         Map<String, Object> configAdvanceInfo = new HashMap<String, Object>(10);
  34.         MapUtils.putIfValNoNull(configAdvanceInfo, "config_tags", configTags);
  35.         MapUtils.putIfValNoNull(configAdvanceInfo, "desc", desc);
  36.         MapUtils.putIfValNoNull(configAdvanceInfo, "use", use);
  37.         MapUtils.putIfValNoNull(configAdvanceInfo, "effect", effect);
  38.         MapUtils.putIfValNoNull(configAdvanceInfo, "type", type);
  39.         MapUtils.putIfValNoNull(configAdvanceInfo, "schema", schema);
  40.         ParamUtils.checkParam(configAdvanceInfo);
  41.    
  42.         if (AggrWhitelist.isAggrDataId(dataId)) {
  43.             LOGGER.warn("[aggr-conflict] {} attemp to publish single data, {}, {}", RequestUtil.getRemoteIp(request), dataId, group);
  44.             throw new NacosException(NacosException.NO_RIGHT, "dataId:" + dataId + " is aggr");
  45.         }
  46.    
  47.         final Timestamp time = TimeUtils.getCurrentTime();
  48.         String betaIps = request.getHeader("betaIps");
  49.         ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
  50.         configInfo.setType(type);
  51.         if (StringUtils.isBlank(betaIps)) {
  52.             if (StringUtils.isBlank(tag)) {
  53.                 //新增配置或者修改配置
  54.                 persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, true);
  55.                 //发布配置改变事件
  56.                 ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
  57.             } else {
  58.                 persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, true);
  59.                 //发布配置改变事件
  60.                 ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));
  61.             }
  62.         } else {
  63.             //beta publish
  64.             persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, true);
  65.             //发布配置改变事件
  66.             ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
  67.         }
  68.         ConfigTraceService.logPersistenceEvent(dataId, group, tenant, requestIpApp, time.getTime(), InetUtils.getSelfIP(), ConfigTraceService.PERSISTENCE_EVENT_PUB, content);
  69.         return true;
  70.     }
  71.     ...
  72. }
  73. //External Storage Persist Service.
  74. @SuppressWarnings(value = {"PMD.MethodReturnWrapperTypeRule", "checkstyle:linelength"})
  75. @Conditional(value = ConditionOnExternalStorage.class)
  76. @Component
  77. public class ExternalStoragePersistServiceImpl implements PersistService {
  78.     private DataSourceService dataSourceService;
  79.     ...
  80.    
  81.     @Override
  82.     public void insertOrUpdate(String srcIp, String srcUser, ConfigInfo configInfo, Timestamp time, Map<String, Object> configAdvanceInfo, boolean notify) {
  83.         try {
  84.             addConfigInfo(srcIp, srcUser, configInfo, time, configAdvanceInfo, notify);
  85.         } catch (DataIntegrityViolationException ive) { // Unique constraint conflict
  86.             updateConfigInfo(configInfo, srcIp, srcUser, time, configAdvanceInfo, notify);
  87.         }
  88.     }
  89.    
  90.     @Override
  91.     public void updateConfigInfo(final ConfigInfo configInfo, final String srcIp, final String srcUser, final Timestamp time, final Map<String, Object> configAdvanceInfo, final boolean notify) {
  92.         boolean result = tjt.execute(status -> {
  93.             try {
  94.                 //查询已存在的配置数据
  95.                 ConfigInfo oldConfigInfo = findConfigInfo(configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant());
  96.                 String appNameTmp = oldConfigInfo.getAppName();
  97.                 if (configInfo.getAppName() == null) {
  98.                     configInfo.setAppName(appNameTmp);
  99.                 }
  100.                 //更新配置数据
  101.                 updateConfigInfoAtomic(configInfo, srcIp, srcUser, time, configAdvanceInfo);
  102.                 String configTags = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("config_tags");
  103.                 if (configTags != null) {
  104.                     // delete all tags and then recreate
  105.                     removeTagByIdAtomic(oldConfigInfo.getId());
  106.                     addConfigTagsRelation(oldConfigInfo.getId(), configTags, configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant());
  107.                 }
  108.                 //保存到发布配置历史表
  109.                 insertConfigHistoryAtomic(oldConfigInfo.getId(), oldConfigInfo, srcIp, srcUser, time, "U");
  110.             } catch (CannotGetJdbcConnectionException e) {
  111.                 LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
  112.                 throw e;
  113.             }
  114.             return Boolean.TRUE;
  115.         });
  116.     }
  117.    
  118.     @Override
  119.     public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {
  120.         final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
  121.         try {
  122.             return this.jt.queryForObject("SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);
  123.         } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null.
  124.             return null;
  125.         } catch (CannotGetJdbcConnectionException e) {
  126.             LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
  127.             throw e;
  128.         }
  129.     }
  130.     ...
  131. }
复制代码
二.发布配置变更事件
执行ConfigChangePublisher的notifyConfigChange()方法发布配置变更事件时,最终会把事件添加到DefaultPublisher.queue阻塞队列中,完成事件发布。
 
NotifyCenter在其静态方法中,会创建DefaultPublisher并进行初始化。在执行DefaultPublisher的init()方法时,就会开启一个异步任务。该异步任务便会不断从阻塞队列DefaultPublisher.queue中获取事件,然后调用DefaultPublisher的receiveEvent()方法处理配置变更事件。
 
在DefaultPublisher的receiveEvent()方法中,会循环遍历事件订阅者。其中就会包括来自客户端,以及来自集群节点的两个订阅者。前者会通知客户端发生了配置变更事件,后者会通知各集群节点发生了配置变更事件。而且进行事件通知时,都会调用DefaultPublisher的notifySubscriber()方法。该方法会异步执行订阅者的监听逻辑,也就是subscriber.onEvent()方法。
 
具体的subscriber订阅者有:用来通知集群节点进行数据同步的订阅者AsyncNotifyService,用来通知客户端处理配置文件变更的订阅者LongPollingService。
 
事件发布机制的实现简单总结:发布者需要一个Set存放注册的订阅者,发布者发布事件时,需要遍历调用订阅者处理事件的方法。
[code]public class ConfigChangePublisher {    //Notify ConfigChange.    public static void notifyConfigChange(ConfigDataChangeEvent event) {        if (PropertyUtil.isEmbeddedStorage() && !EnvUtil.getStandaloneMode()) {            return;        }        NotifyCenter.publishEvent(event);    }}//Unified Event Notify Center.public class NotifyCenter {    static {        ...        try {            // Create and init DefaultSharePublisher instance.            INSTANCE.sharePublisher = new DefaultSharePublisher();            INSTANCE.sharePublisher.init(SlowEvent.class, shareBufferSize);                } catch (Throwable ex) {            LOGGER.error("Service class newInstance has error : {}", ex);        }        ThreadUtils.addShutdownHook(new Runnable() {            @Override            public void run() {                shutdown();            }        });    }        //注册订阅者    public static  void registerSubscriber(final Subscriber consumer) {        ...        addSubscriber(consumer, subscribeType);    }        private static void addSubscriber(final Subscriber consumer, Class

相关推荐

您需要登录后才可以回帖 登录 | 立即注册