SpringCloud Nacos配置中心--客户端源码解析

SpringCloud Nacos配置中心--客户端源码解析

1、 SpringCloud实现配置加载

SpringCLoud在启动时,会在PropertySourceBootstrapConfiguration.initialize()方法中调用PropertySourceLocator.locate()方法来获取远程配置信息。

PropertySourceLocator接口的主要作用是实现应用外部化配置可动态加载

SpringBoot启动时,回在SpringApplication.run方法中调用prepareEnvironment方法进行环境准备工作

public ConfigurableApplicationContext run(String... args) {
  ...
              ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
 ...
}


  private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
       ...
        // 这里发布了一个ApplicationEnvironmentPreparedEvent事件
        listeners.environmentPrepared((ConfigurableEnvironment)environment);
        ...
        return (ConfigurableEnvironment)environment;
    }

BootstrapApplicationListener监听器会去订阅这个事件。

public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
...
   if (context == null) {
      context = bootstrapServiceContext(environment, event.getSpringApplication(),
            configName);
      event.getSpringApplication()
            .addListeners(new CloseContextOnFailureApplicationListener(context));
   }

...
}

private ConfigurableApplicationContext bootstrapServiceContext(
  ...
   // 通过SpringBoot的自动装配机制,导入了BootstrapImportSelector.class
  builder.sources(BootstrapImportSelectorConfiguration.class)
;
 ...
}
  
  
@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration 
{

}

  
  public class BootstrapImportSelector implements EnvironmentAwareDeferredImportSelector {
  public String[] selectImports(AnnotationMetadata annotationMetadata) {
...
  // Use names and ensure unique to protect against duplicates
  List<String> names = new ArrayList<>(SpringFactoriesLoader
    .loadFactoryNames(BootstrapConfiguration.classclassLoader));
...
  }
 }

BootstrapApplicationListener订阅到事件后,会通过SpringBoot的自动装配机制,引入BootstrapImportSelector,从加载spring.facotories文件上的BootstrapConfiguration配置信息

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration

通过SpringBoot的SPI机制,装载了NacosConfigBootstrapConfiguration 到Ioc容器中,之后便是熟悉的SpringBoot注入Bean的方式了,在这里会将NacosPropertySourceLocator注入到Spring中。

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

   @Bean
   @ConditionalOnMissingBean
   public NacosConfigProperties nacosConfigProperties() {
      return new NacosConfigProperties();
   }

   @Bean
   @ConditionalOnMissingBean
   public NacosConfigManager nacosConfigManager(
         NacosConfigProperties nacosConfigProperties)
 
{
      return new NacosConfigManager(nacosConfigProperties);
   }

   @Bean
   public NacosPropertySourceLocator nacosPropertySourceLocator(
         NacosConfigManager nacosConfigManager)
 
{
      return new NacosPropertySourceLocator(nacosConfigManager);
   }

}

1)、NacosPropertySourceLocator

public PropertySource<?> locate(Environment env) {
   nacosConfigProperties.setEnvironment(env);
   // 创建Nacos的Java SDK,用于从Nacos服务器拉取配置
   ConfigService configService = nacosConfigManager.getConfigService();
   /*
   ...
  */

   CompositePropertySource composite = new CompositePropertySource(
         NACOS_PROPERTY_SOURCE_NAME);

   loadSharedConfiguration(composite);
   loadExtConfiguration(composite);
  // 加载应用程序的配置
   loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

   return composite;
}

loadApplicationConfiguration方法最终会调用loadNacosData方法从Nacos配置中心加载配置

private Map<String, Object> loadNacosData(String dataId, String group,
   String fileExtension)
 
{
  String data = null;
  try {
      // 从Nacos配置中心加载配置
   data = configService.getConfig(dataId, group, timeout);
   /*
    ...
   */

     // 通过责任链模式,使用不同的解析器解析从Nacos加载的配置(properties、yaml、json、xml等)
   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);
  }
  return EMPTY_MAP;
 }

2、 Nacos配置热刷新

Nacos会通过SpringBoot的SPI机制加载NacosConfigAutoConfiguration,在该Bean中会注入NacosContextRefresher监听器

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\

NacosContextRefresher实现了ApplicationListener,监听了ApplicationReadyEvent事件,在SpringBoot环境加载完成后会触发此监听器,具体代码如下:

public class NacosContextRefresher
  implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware 
{

 @Override
 public void onApplicationEvent(ApplicationReadyEvent event) {
  // many Spring context
  if (this.ready.compareAndSet(falsetrue)) {
      // 注册nacos配置监听器
   this.registerNacosListenersForApplications();
  }
 }
  
  
  private void registerNacosListenersForApplications() {
  ...
    registerNacosListener(propertySource.getGroup(), dataId);
  ...
 }
  
  
  private void registerNacosListener(final String groupKey, final String dataKey) {
  String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    // 生成一个Nacos的监听器并记录到listenerMap中,
  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(thisnull"Refresh Nacos config"));
      if (log.isDebugEnabled()) {
       log.debug(String.format(
         "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
         group, dataId, configInfo));
      }
     }
    });
  try {
      // 将监听器添加到Nacos的客户端中,如果NacosServer的相关配置发生了变化,便会触发对应Id的监听器,发布一个Spring的配置刷新事件,从而刷新Spring的本地配置
   configService.addListener(dataKey, groupKey, listener);
  }
  catch (NacosException e) {
   log.warn(String.format(
     "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
     groupKey), e);
  }
 }
  
}

Nacos在感知到SpringBoot的环境配置加载完成后,会生成一个Nacos客户端的监听器,监听NacosServer的配置信息,如果有服务端出现了配置变化,则会触发Nacos的监听器发布一个Spring的RefreshEvent事件来刷新SpringBoot的本地配置。

public class RefreshEventListener implements SmartApplicationListener {

  public void handle(RefreshEvent event) {
      if (this.ready.get()) { // don't handle events before app is ready
        log.debug("Event received " + event.getEventDesc());
        Set<String> keys = this.refresh.refresh();
        log.info("Refresh keys changed: " + keys);
      }
  }
}

SpringBoot的RefreshEventListener监听器监听到配置刷新事件后,会调用handle方法,刷新本地的Bean信息,将加了@RefreshScope注解的Bean给销毁掉。这样,在重新使用到对应Bean的时候,会使用新的配置信息来创建Bean。

3、Nacos获取配置客户端

上面提到Nacos Client端会在NacosPropertySourceLocator中通过ConfigService来获取Nacos Server端的配置,创建ConfigService的路线如下:

nacosConfigManager.getConfigService(); --> NacosFactory.createConfigService

public class ConfigFactory {
    public ConfigFactory() {
    }

    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable var4) {
            throw new NacosException(-400, var4);
        }
    }
}

可以看到在NacosFactory工厂类中,最终是通过反射机制创建了com.alibaba.nacos.client.config.NacosConfigService

public class NacosConfigService implements ConfigService {
  public NacosConfigService(Properties properties) throws NacosException {
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            encode = Constants.ENCODE;
        } else {
            encode = encodeTmp.trim();
        }
        initNamespace(properties);
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }
}

NacosConfigService的构造方法中,会初始化一个HttpAgent,这里用了装饰器模式,实际使用的是ServerHttpAgent;还初始化了一个ClientWork,这是客户端的一个工作类;

ClientWork的构造方法如下

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

    // Initialize the timeout parameter

    init(properties);

    executor = Executors.newScheduledThreadPool(1new 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;
        }
    });

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

    executor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            try {
                checkConfigInfo();
            } catch (Throwable e) {
                LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
            }
        }
    }, 1L10L, TimeUnit.MILLISECONDS);
}

ClientWork的构造方法中,创建了两个线程池,每10毫秒会执行checkConfigInfo方法,检查Nacos配置

public void checkConfigInfo() {
    // 分任务
    int listenerSize = cacheMap.get().size();
    // 向上取整为批数,计算需要启动的线程数,配置数量/3000
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}

在该方法中,会启动一个长轮询线程LongPollingRunnable来监听服务端的配置,每个任务会处理3000个监听配置集,多于3000个配置,则会启动新的LongPollingRunnable线程来执行监听。

class LongPollingRunnable implements Runnable {
    private int taskId;

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

    @Override
    public void run() {

        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            // check failover config
            for (CacheData cacheData : cacheMap.get().values()) {
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    try {
                        checkLocalConfig(cacheData);
                        if (cacheData.isUseLocalConfigInfo()) {
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // check server config
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            LOGGER.info("get changedGroupKeys:" + changedGroupKeys);

            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 {
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    CacheData cache = cacheMap.get().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);
                }
            }
            for (CacheData cacheData : cacheDatas) {
                if (!cacheData.isInitializing() || inInitializingCacheList
                    .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();

            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);
            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
        }
    }
}

LongPollingRunnable实现了Runnable接口,所以启动线程时会执行run方法,在run方法中,会先遍历cacheMap,筛选出当前长轮询线程负责监听的配置数据,并检查是否本地配置文件的数据是否有更新,如果有变更则直接触发事件通知。

接着会调用checkUpdateDataIds方法,基于长连接的方式来监听服务端配置的变化,最后根据数据的key去服务端获取最新的数据,最后再重新调度一下this,继续启动长轮询线程监听配置。

checkUpdateDataIds方法中,最终调用了checkUpdateConfigStr方法。

List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {


    List<String> params = new ArrayList<String>(2);
    params.add(Constants.PROBE_MODIFY_REQUEST);
    params.add(probeUpdateString);

    List<String> headers = new ArrayList<String>(2);
    headers.add("Long-Pulling-Timeout");
    headers.add("" + timeout);

    // told server do not hang me up if new initializing cacheData added in
    if (isInitializingCacheList) {
        headers.add("Long-Pulling-Timeout-No-Hangup");
        headers.add("true");
    }

    if (StringUtils.isBlank(probeUpdateString)) {
        return Collections.emptyList();
    }

    try {
        // In order to prevent the server from handling the delay of the client's long task,
        // increase the client's read timeout to avoid this problem.
    // readTimeOutMs = 45000
        long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
        HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
            agent.getEncode(), readTimeoutMs);

        if (HttpURLConnection.HTTP_OK == result.code) {
            setHealthServer(true);
            return parseUpdateDataIdResponse(result.content);
        } else {
            setHealthServer(false);
            LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
        }
    } catch (IOException e) {
        setHealthServer(false);
        LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
        throw e;
    }
    return Collections.emptyList();
}

可以看到checkUpdateConfigStr方法最终是通过agent.httoPost调用了/v1/cs/configs/listener接口实现长轮询请求。而长轮询请求是在客户端设置了一个比较长的超时时间,默认45秒(老版本30秒)。如果服务端的数据发生了变更,客户端会收到一个HttpResult,服务端返回的是变更配置的DataIdGroupTenant。获得这些信息后,会在LongPollingRunnable线程中调用getServerConfig方法去Nacos服务端读取具体的配置内容。

public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
    throws NacosException {
    String[] ct = new String[2];
    if (StringUtils.isBlank(group)) {
        group = Constants.DEFAULT_GROUP;
    }

    HttpResult result = null;
    try {
        List<String> params = null;
        if (StringUtils.isBlank(tenant)) {
            params = new ArrayList<String>(Arrays.asList("dataId", dataId, "group", group));
        } else {
            params = new ArrayList<String>(Arrays.asList("dataId", dataId, "group", group, "tenant", tenant));
        }
        result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
    } catch (IOException e) {
        String message = String.format(
            "[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", agent.getName(),
            dataId, group, tenant);
        LOGGER.error(message, e);
        throw new NacosException(NacosException.SERVER_ERROR, e);
    }

    switch (result.code) {
        case HttpURLConnection.HTTP_OK:
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.content);
            ct[0] = result.content;
            if (result.headers.containsKey(CONFIG_TYPE)) {
                ct[1] = result.headers.get(CONFIG_TYPE).get(0);
            } else {
                ct[1] = ConfigType.TEXT.getType();
            }
            return ct;
        case HttpURLConnection.HTTP_NOT_FOUND:
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
            return ct;
        case HttpURLConnection.HTTP_CONFLICT: {
            LOGGER.error(
                "[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
                    + "tenant={}", agent.getName(), dataId, group, tenant);
            throw new NacosException(NacosException.CONFLICT,
                "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
        }
        case HttpURLConnection.HTTP_FORBIDDEN: {
            LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(), dataId,
                group, tenant);
            throw new NacosException(result.code, result.content);
        }
        default: {
            LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", agent.getName(), dataId,
                group, tenant, result.code);
            throw new NacosException(result.code,
                "http error, code=" + result.code + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
        }
    }
}

可以看到,getServerConfig最终是通过agent.httpGet调用服务端的/v1/cs/configs接口来获取服务端的配置数据。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud Alibaba Nacos 是一个服务注册中心和配置中心,可以实现服务的注册与发现、配置的动态管理等功能,同时还提供了容灾和高可用的支持。下面简单介绍如何使用 Nacos 实现 Spring Cloud 的配置容灾。 首先,在应用的 `pom.xml` 文件中添加如下依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.3.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2.2.3.RELEASE</version> </dependency> ``` 然后在 `application.properties` 中配置 Nacos 的地址和应用的名称: ```properties spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 spring.cloud.nacos.discovery.namespace=your-namespace spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=your-namespace spring.cloud.nacos.config.file-extension=properties spring.application.name=your-application-name ``` 其中 `server-addr` 是 Nacos 的地址,`namespace` 是命名空间,`file-extension` 是配置文件的扩展名,`application.name` 是应用的名称。 接着在 `bootstrap.properties` 中配置应用的环境和配置来源: ```properties spring.profiles.active=dev spring.cloud.nacos.config.prefix=${spring.application.name}-${spring.profiles.active} spring.cloud.nacos.config.group=DEFAULT_GROUP spring.cloud.nacos.config.shared-dataids=${spring.application.name}-${spring.profiles.active}.properties ``` 其中 `spring.profiles.active` 是应用的环境,`prefix` 是配置文件的前缀,`group` 是配置文件所在的分组,`shared-dataids` 是配置文件的名称。 最后,在代码中使用 `@Value` 注解来获取配置项的值: ```java @RestController public class ConfigController { @Value("${config.key}") private String configValue; @GetMapping("/config") public String getConfig() { return configValue; } } ``` 其中 `config.key` 是配置项的名称。 以上就是使用 Nacos 实现 Spring Cloud 的配置容灾的简单示例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值