Nacos配置中心Client源码解析

源码解析之配置加载流程


文章有点长,还需慢慢看。
源码阅读前的建议:
1.了解过Spring&SpringBoot源码(必须)。
2.了解过SpringBoot配置文件的加载流程(必须)

开始撸起!

1.前序

在我们项目中使用Nacos做配置中心时,我们首先会引入Nacos-Config的依赖

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

了解过SpringBoot源码的小伙伴肯定知道其他框架与SpringBoot进行整合的时候肯定会有个XXXAutoConfiguration的类,这是一个自动配置装配的类,关于自动装配类的加载原理本文暂时不做介绍。
那么Nacos是否存在这样的类呢?答案是肯定有的
在这里插入图片描述这个地方需要我们需要重点关注的2个类:

  • NacosConfigBootstrapConfiguration
    启动时拉取配置的原理。
  • NacosConfigAutoConfiguration
    更改配置时实现动态刷新的原理。

2.NacosConfigBootstrapConfiguration

public class NacosConfigBootstrapConfiguration {
	
	@Bean
	@ConditionalOnMissingBean
	public NacosConfigManager nacosConfigManager(
			NacosConfigProperties nacosConfigProperties) {
		// 构造器里面创建了NacosConfigService(下面会说到)
		return new NacosConfigManager(nacosConfigProperties);
	}
	...
	@Bean
	public NacosPropertySourceLocator nacosPropertySourceLocator(
			NacosConfigManager nacosConfigManager) {
		return new NacosPropertySourceLocator(nacosConfigManager);
	}
}

自动配置了一个 NacosPropertySourceLocator,PropertySourceLocator接口是实现配置文件加载的接口,
在这里插入图片描述
spring boot 在启动时读取配置文件时,会获取 PropertySourceLocator 的实现类进行循环调用,本文暂时不对这块进行解释。
最终会调用 locate()方法:PropertySource<?> locate(Environment environment);

	public PropertySource<?> locate(Environment env) {
		nacosConfigProperties.setEnvironment(env);
		// 连接nacos server的客户端
		ConfigService configService = nacosConfigManager.getConfigService();
		String name = nacosConfigProperties.getName();
		String dataIdPrefix = nacosConfigProperties.getPrefix();
		// 名字为 nacos 的 PropertySource 
		CompositePropertySource composite = new CompositePropertySource(
				NACOS_PROPERTY_SOURCE_NAME);
		// 1.先加载共享配置
		loadSharedConfiguration(composite);
		// 2.在加载扩展配置
		loadExtConfiguration(composite);
		// 3.加载指定|没指定profile的配置
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		return composite;
	}

上面就能很清楚的看到nacos的配置加载顺序了,根据配置覆盖原则,先加载的会被后加载的覆盖,外部配置的加载,不优先本地配置的加载,后加载的优先级会更高。
直接分析 loadApplicationConfiguration 的加载,因为其他的都是一样的方式。

	private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) {
		String fileExtension = properties.getFileExtension();
		String nacosGroup = properties.getGroup();
		// load directly once by default
		// 直接就是 服务名的:web-provider
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
				fileExtension, true);
		// load with suffix, which have a higher priority than the default
		// 带上后缀的 web-provider.yaml
		loadNacosDataIfPresent(compositePropertySource,
				dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		// Loaded with profile, which have a higher priority than the suffix
		// 带上profile+后缀的 web-provider-dev.yaml
		for (String profile : environment.getActiveProfiles()) {
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
					fileExtension, true);
		}
	}

loadNacosDataIfPresent():

	private void loadNacosDataIfPresent(final CompositePropertySource composite,
			final String dataId, final String group, String fileExtension,
			boolean isRefreshable) {
		NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
				fileExtension, isRefreshable);
		// 从server端拉取成功后将配置信息放到最前面
		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);
	}
	
	NacosPropertySource build(String dataId, String group, String fileExtension,
		boolean isRefreshable) {
		// 这个地方开始向远端拉取配置
		List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
				fileExtension);
		NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
				group, dataId, new Date(), isRefreshable);
		NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
		return nacosPropertySource;
   }

loadNacosData(): 真正拉取配置的开始

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

顺着调用一直往下:
com.alibaba.nacos.client.config.NacosConfigService#getConfig

private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        ConfigResponse cr = new ConfigResponse();
        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);
        // 1.优先使用本地配置
        // 配置文件存在路径:System.getProperty("user.home") + File.separator + "nacos"+ File.separator + "config";
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        if (content != null) {
            return content;
        }
        // 2.远程拉取
        String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
        cr.setContent(ct[0]);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }
    
	public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout){
				HttpRestResult<String> result = null;
				// 发起http请求 GET /v1/cs/configs
	            result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
	}

在这里插入图片描述
从原理分析上应证了官网的描述。
从上面分析可以看出应用在启动时会进行远程配置的读取,Nacos在启动时读取配置的原则是:
1.先使用本地路径的
2.在远端拉取
然后把加载到的配置放到spring环境里面,上面就是启动时配置加载的大致过程。

下面来重点分析几个类:
NacosConfigService的实例化:

    public NacosConfigService(Properties properties) throws NacosException {
        initNamespace(properties);
        this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        this.agent.start();
        // 这个地方创建了一个ClientWorker类 
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }

ClientWorker的实例化:

 init(properties);
 // 创建了一个线程池
 this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
     @Override
     public Thread newThread(Runnable r) {
         Thread t = new Thread(r);
         t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
         t.setDaemon(true);
         return t;
     }
 });
 // 执行线程任务
 this.executor.scheduleWithFixedDelay(new Runnable() {
     @Override
     public void run() {
         try {
            // 调用了ClientWorker中的这个方法
             checkConfigInfo();
         } catch (Throwable e) {
             LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
         }
     }
 }, 1L, 10L, TimeUnit.MILLISECONDS);

checkConfigInfo():执行长轮训任务,检查远端配置和本地配置的一致性,不一致就重新拉取远端配置到本地,并且回调监听方法。

public void checkConfigInfo() {
        // Dispatch taskes.
        int listenerSize = cacheMap.size();
        // Round up the longingTaskCount.
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                // The task list is no order.So it maybe has issues when changing.
                // 线程池执行 LongPollingRunnable 线程任务类
                // LongPollingRunnable就是长轮训,运用了servlet3的异步servlet技术(这个需要结合服务端源码一起分析才能看出来)
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

LongPollingRunnable长轮训线程任务类:
直接分析run方法:

public void run() {
    // CacheData 里面有个 Listener 属性就是需要回调的监听方法
    List<CacheData> cacheDatas = new ArrayList<CacheData>();
    List<String> inInitializingCacheList = new ArrayList<String>();
    try {
        // check failover config
        for (CacheData cacheData : cacheMap.values()) {
            if (cacheData.getTaskId() == taskId) {
                cacheDatas.add(cacheData);
                ...
            }
        }
        // 本地配置文件加密的md5等信息,发送给server端进行比较
        // 如果md5不一样,这个地方只会返回需要拉取的配置信息
        List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
        // 这个循环里面会进行配置重新拉取
        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];
            }
            // 重新从server端拉取最新的配置
            String[] ct = getServerConfig(dataId, group, tenant, 3000L);
            CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
            cache.setContent(ct[0]);
        }
        for (CacheData cacheData : cacheDatas) {
            if (!cacheData.isInitializing() || inInitializingCacheList
                    .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                // 这个地方回调Listener方法 
                cacheData.checkListenerMd5();
                cacheData.setInitializing(false);
            }
        }
        executorService.execute(this);
        
    } catch (Throwable e) {
        executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
    }
}

先看checkUpdateDataIds(),这是个比较重要的方法:

List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
    Map<String, String> params = new HashMap<String, String>(2);
    params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
    Map<String, String> headers = new HashMap<String, String>(2);
    headers.put("Long-Pulling-Timeout", "" + timeout);
    try {
        long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
        // POST /v1/cs/configs/listener 这个请求是用来注册配置监听的,会返回有变化的配置文件的基本信息
        // 在后面进行server端源码分析的时候会进行分析的
        HttpRestResult<String> result = agent
                .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),readTimeoutMs);
        if (result.ok()) {
            setHealthServer(true);
            // 解析结果
            return parseUpdateDataIdResponse(result.getData());
         }
    return Collections.emptyList();
}

在这里插入图片描述
server端的 监听配置接口 运用了servlet3的异步servlet技术来达到长轮训。

下面只要拉取变化了的配置即可:
getServerConfig(xxx):

public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
            throws NacosException {
        HttpRestResult<String> result = null;
        try {
            Map<String, String> params = new HashMap<String, String>(3);
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("tenant", tenant);
            // 拉取配置 GET /v1/cs/configs,和上面的获取配置一样
            result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
        } catch (Exception ex) {
            throw new NacosException(NacosException.SERVER_ERROR, ex);
        }
        switch (result.getCode()) {
            case HttpURLConnection.HTTP_OK:
            	// 保存快照到本地
                LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
                ct[0] = result.getData();
                if (result.getHeader().getValue(CONFIG_TYPE) != null) {
                    ct[1] = result.getHeader().getValue(CONFIG_TYPE);
                } else {
                    ct[1] = ConfigType.TEXT.getType();
                }
                return ct;
             .....
        }
    }

cacheData.checkListenerMd5();

 void checkListenerMd5() {
     for (ManagerListenerWrap wrap : listeners) {
         if (!md5.equals(wrap.lastCallMd5)) {
            // md5 不一样直接发布 配置更新事件
             safeNotifyListener(dataId, group, content, type, md5, wrap);
         }
     }
 }
 private void safeNotifyListener(final String dataId, final String group, final String content, 
         final String md5, final ManagerListenerWrap listenerWrap) {
     final Listener listener = listenerWrap.listener;
     Runnable job = new Runnable() {
         @Override
         public void run() {
             try {  
                 ConfigResponse cr = new ConfigResponse();
                 cr.setDataId(dataId);
                 cr.setGroup(group);
                 cr.setContent(content);
                 configFilterChainManager.doFilter(null, cr);
                 String contentTmp = cr.getContent();
                 // 回调 Listener的 receiveConfigInfo
                 listener.receiveConfigInfo(contentTmp);
                 // compare lastContent and content
                 if (listener instanceof AbstractConfigChangeListener) {
                     Map data = ConfigChangeHandler.getInstance()
                             .parseChangeData(listenerWrap.lastContent, content, type);
                     ConfigChangeEvent event = new ConfigChangeEvent(data);
                     // AbstractConfigChangeListener.receiveConfigChange(event);
                     ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                     listenerWrap.lastContent = content;
                 }
          }
      }
  };
    if (null != listener.getExecutor()) {
         listener.getExecutor().execute(job);
     } else {
         job.run();
     }
}

这个地方会回调listener的事件,bean的更新也是在这个地方触发的,下面的这个方法发布了RefreshEvent事件,
RefreshEventListener是监听类,它会刷新与配置有关的bean,达到动态配置更新。
NacosConfigAutoConfiguration中注册的bean:

@Bean
public NacosContextRefresher nacosContextRefresher(
		NacosConfigManager nacosConfigManager,
		NacosRefreshHistory nacosRefreshHistory) {
	return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
}

NacosContextRefresher:bean刷新的自动配置类。
com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener

private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
	lst -> new AbstractSharedListener() {
		@Override
		public void innerReceive(String dataId, String group,
				String configInfo) {
			refreshCountIncrement();
			nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
			// RefreshEvent
			applicationContext.publishEvent(
					new RefreshEvent(this, null, "Refresh Nacos config"));
		}
	});
 // 注册指定配置文件的监听
 configService.addListener(dataKey, groupKey, listener);
}

从这里我们也可以看出,我们也可以自己实现listener接口,达到自定义监听指定配置文件的目的。

OK,客户端大致部分已经分析完毕。
Nacos 客户端配置更新感知完全都是通过定时任务来完成的,核心在于 ClientWork类, ClientWork内部使用线程池启动LongPollingRunnable线程任务。
LongPollingRunnable线程任务类:

  • 1.向server端注册配置监听,server会返回变动了的配置信息。
  • 2.拉取最新变动了的配置信息。
  • 3.通过返回的md5和本地的md5进行比较,不一样发布配置更新事件,刷新本地容器中的bean。

以上就是client端的源码解析,涉及到server端请求的接口,下一篇来进行分析。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值