获取nacos配置中心文件值_nacos统一配置中心源码解析

配置文件想必大家都很熟悉,无论什么架构 都离不开配置,虽然spring boot已经大大简化了配置,但如果服务很多 环境也好几个,管理配置起来还是很麻烦,并且每次改完配置都需要重启服务,nacos config出现就解决了这些问题,它把配置统一放到服务进行管理,客户端这边进行有需要的获取,可以实时对配置进行修改和发布

如何使用nacos config

首先需要引入nacos config jar包

com.alibaba.cloud

spring-cloud-starter-alibaba-nacos-config

2.2.1.RELEASE

在nacos控制台提前配置需要的配置文件

配置文件格式支持text、json、xml、yaml、html、properties,注意spring boot启动支持的配置文件格式只能为yaml或properties格式,其它格式的配置文件需要后续我们自己写代码去获取

我们来看db.properties也是就数据库配置

data id就是对应配置文件id,group为分组,配置内容就是properties格式的

再来看bootstrap.properties如何引用这个配置文件

spring.application.name=nacos-config

server.port=20200#命名空间

spring.cloud.nacos.config.namespace=${nacos_register_namingspace:0ca74337-8f42-49c3-aec9-32f268a937c4}

#组名

spring.cloud.nacos.config.group=${spring.application.name}

#文件格式

spring.cloud.nacos.config.file-extension=properties

#nacos server地址

spring.cloud.nacos.config.server-addr=localhost:8848#加载配置文件

spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties

spring.cloud.nacos.config.ext-config[1].data-id=db.properties

spring.cloud.nacos.config.ext-config[2].data-id=mybatis-plus.properties

注意 加载配置文件的分组名默认为DEFAULT_GROUP,如需指定分组 需要再指定

spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties

spring.cloud.nacos.config.ext-config[0].group=${spring.cloud.nacos.config.group}

#或者

spring.cloud.nacos.config.ext-config[1].data-id=undertow.properties

spring.cloud.nacos.config.ext-config[1].group=MY_DEFAULT

在这里解释下namespace和group的概念,namespace可以用来解决不同环境的问题,group是来管理配置分组的,它们的关系如下图

spring boot启动容器如何加载nacos config配置文件

这个配置作用是spring在启动之间准备上下文时会启用这个配置 来导入nacos相关配置文件,为后续容器启动做准备

来看NacosConfigBootstrapConfiguration这个配置类

NacosConfigProperties:对应我们上面在bootstrap.properties中对应的配置信息

NacosConfigManager: 持有NacosConfigProperties和ConfigService,ConfigService用来查询 发布配置的相关接口

NacosPropertySourceLocator:它实现了PropertySourceLocator ,spring boot启动时调用PropertySourceLocator.locate(env)用来加载配置信息,下面来看相关源码

/******************************************NacosPropertySourceLocator******************************************/

public PropertySource>locate(Environment env) {

ConfigService configService= this.nacosConfigProperties.configServiceInstance();if (null ==configService) {

log.warn("no instance of config service found, can't load config from nacos");return null;

}else{long timeout = (long)this.nacosConfigProperties.getTimeout();this.nacosPropertySourceBuilder = newNacosPropertySourceBuilder(configService, timeout);

String name= this.nacosConfigProperties.getName();

String dataIdPrefix= this.nacosConfigProperties.getPrefix();if(StringUtils.isEmpty(dataIdPrefix)) {

dataIdPrefix=name;

}if(StringUtils.isEmpty(dataIdPrefix)) {

dataIdPrefix= env.getProperty("spring.application.name");

}

CompositePropertySource composite= new CompositePropertySource("NACOS");//加载共享的配置文件 不同指定分组 默认DEFAULT_GROUP,对应配置spring.cloud.nacos.config.sharedDataids=shared_1.properties,shared_2.properties

this.loadSharedConfiguration(composite);//对应spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties的配置

this.loadExtConfiguration(composite);//加载当前应用配置

this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);returncomposite;

}

}//看一个加载实现即可 流程都差不多 具体实现在NacosPropertySourceBuilder.loadNacosData()方法完成/******************************************具体实现在NacosPropertySourceBuilder******************************************/

privateProperties loadNacosData(String dataId, String group, String fileExtension) {

String data= null;try{//向nacos server拉取配置文件

data = this.configService.getConfig(dataId, group, this.timeout);if (!StringUtils.isEmpty(data)) {

log.info(String.format("Loading nacos data, dataId: '%s', group: '%s'", dataId, group));//spring boot配置当然只支持properties和yaml文件格式

if (fileExtension.equalsIgnoreCase("properties")) {

Properties properties= newProperties();

properties.load(newStringReader(data));returnproperties;

}if (fileExtension.equalsIgnoreCase("yaml") || fileExtension.equalsIgnoreCase("yml")) {

YamlPropertiesFactoryBean yamlFactory= newYamlPropertiesFactoryBean();

yamlFactory.setResources(new Resource[]{newByteArrayResource(data.getBytes())});returnyamlFactory.getObject();

}

}

}catch(NacosException var6) {

log.error("get data from Nacos error,dataId:{}, ", dataId, var6);

}catch(Exception var7) {

log.error("parse data from Nacos error,dataId:{},data:{},", newObject[]{dataId, data, var7});

}returnEMPTY_PROPERTIES;

}

至此我们在nacos上配置的properties和yaml文件都载入到spring配置文件中来了,后面可通过context.Environment.getProperty(propertyName)来获取相关配置信息

配置如何随spring boot加载进来我们说完了,接来下来看修改完配置后如何实时刷新

nacos config动态刷新

当nacos config更新后,根据配置中的refresh属性来判断是否刷新配置,配置如下

spring.cloud.nacos.config.ext-config[0].refresh=true

首先sprin.factories 配置了EnableAutoConfiguration=NacosConfigAutoConfiguration,NacosConfigAutoConfiguration配置类会注入一个NacosContextRefresher,它首先监听了ApplicationReadyEvent,然后注册一个nacos listener用来监听nacos config配置修改后发布一个spring refreshEvent用来刷新配置和应用

public class NacosContextRefresher implements ApplicationListener, ApplicationContextAwarepublic voidonApplicationEvent(ApplicationReadyEvent event) {//只注册一次

if (this.ready.compareAndSet(false, true)) {this.registerNacosListenersForApplications();

}

}private voidregisterNacosListenersForApplications() {if (this.refreshProperties.isEnabled()) {

Iterator var1=NacosPropertySourceRepository.getAll().iterator();while(var1.hasNext()) {

NacosPropertySource nacosPropertySource=(NacosPropertySource)var1.next();//对应刚才所说的配置 需要配置文件是否需要刷新

if(nacosPropertySource.isRefreshable()) {

String dataId=nacosPropertySource.getDataId();//注册nacos监听器

this.registerNacosListener(nacosPropertySource.getGroup(), dataId);

}

}

}

}private void registerNacosListener(final String group, finalString dataId) {

Listener listener= (Listener)this.listenerMap.computeIfAbsent(dataId, (i) ->{return newListener() {public voidreceiveConfigInfo(String configInfo) {

NacosContextRefresher.refreshCountIncrement();

String md5= "";if (!StringUtils.isEmpty(configInfo)) {try{

MessageDigest md= MessageDigest.getInstance("MD5");

md5= (new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))).toString(16);

}catch (UnsupportedEncodingException |NoSuchAlgorithmException var4) {

NacosContextRefresher.log.warn("[Nacos] unable to get md5 for dataId: " +dataId, var4);

}

}//添加刷新记录

NacosContextRefresher.this.refreshHistory.add(dataId, md5);//发布一个spring refreshEvent事件 对应监听器为RefreshEventListener 该监听器会完成配置的更新应用

NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));if(NacosContextRefresher.log.isDebugEnabled()) {

NacosContextRefresher.log.debug("Refresh Nacos config group " + group + ",dataId" +dataId);

}

}publicExecutor getExecutor() {return null;

}

};

});try{this.configService.addListener(dataId, group, listener);

}catch(NacosException var5) {

var5.printStackTrace();

}

}

我们说完了nacos config动态刷新,那么肯定有对应的动态监听,nacos config会监听nacos server上配置的更新状态

nacos config动态监听

一般来说客户端和服务端数据交互无非就两种方式

pull:客户端主动从服务器拉取数据

push: 由服务端主动向客户端推送数据

这两种模式优缺点各不一样,pull模式需要考虑的是什么时候向服务端拉取数据 可能会存在数据延迟问题,而push模式需要客户端和服务端维护一个长连接 如果客户端较多会给服务端造成压力 但它的实时性会更好

nacos采用的是pull模式,但它作了优化 可以看做是pull+push,客户端会轮询向服务端发出一个长连接请求,这个长连接最多30s就会超时,服务端收到客户端的请求会先判断当前是否有配置更新,有则立即返回

如果没有服务端会将这个请求拿住“hold”29.5s加入队列,最后0.5s再检测配置文件无论有没有更新都进行正常返回,但等待的29.5s期间有配置更新可以提前结束并返回,下面会在源码中讲解具体怎么处理的

nacos client处理

动态监听的发起是在ConfigService的实现类NacosConfigService的构造方法中,它是对外nacos config api接口,在之前加载配置文件和NacosContextRefresher构造方法中都会获取或创建

这里都会先判断是否已经创建了ConfigServer,没有则实例化一个NacosConfigService,来看它的构造函数

/***************************************** NacosConfigService *****************************************/

public NacosConfigService(Properties properties) throwsNacosException {

String encodeTmp=properties.getProperty(PropertyKeyConst.ENCODE);if(StringUtils.isBlank(encodeTmp)) {

encode=Constants.ENCODE;

}else{

encode=encodeTmp.trim();

}

initNamespace(properties);//用来向nacos server发起请求的代理,这里用到了装饰模式

agent = new MetricsHttpAgent(newServerHttpAgent(properties));

agent.start();//客户端的一个工作类,agent作为它的构造传参 可猜想到里面肯定会做一些远程调用

worker = newClientWorker(agent, configFilterChainManager, properties);

}/***************************************** ClientWorker *****************************************/

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, finalProperties properties) {this.agent =agent;this.configFilterChainManager =configFilterChainManager;//Initialize the timeout parameter

init(properties);//这个线程池只有一个核心线程 用来执行checkConfigInfo()方法

executor = Executors.newScheduledThreadPool(1, newThreadFactory() {

@OverridepublicThread newThread(Runnable r) {

Thread t= newThread(r);

t.setName("com.alibaba.nacos.client.Worker." +agent.getName());

t.setDaemon(true);returnt;

}

});//其它需要执行线程的地方都交给这个线程池来处理

executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), newThreadFactory() {

@OverridepublicThread newThread(Runnable r) {

Thread t= newThread(r);

t.setName("com.alibaba.nacos.client.Worker.longPolling." +agent.getName());

t.setDaemon(true);returnt;

}

});//执行一个调用checkConfigInfo()方法的周期性任务,每10ms执行一次,首次执行延迟1ms后执行

executor.scheduleWithFixedDelay(newRunnable() {

@Overridepublic voidrun() {try{

checkConfigInfo();

}catch(Throwable e) {

LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);

}

}

},1L, 10L, TimeUnit.MILLISECONDS);

}

NacosConfigService构造方法主要创建一个agent 它是用来向nacos server发出请求的,然后又创建了一个clientwoker,它的构造方法创建了两个线程池,第一个线程池只有一个核心线程,它会执行一个周期性任务只用来调用checkconfiginfo()方法,第二个线程是后续由需要执行线程的地方都交给它来执行,下面重点来看checkconfiginfo()方法

public voidcheckConfigInfo() {//分任务

int listenerSize =cacheMap.get().size();//向上取整为批数

int longingTaskCount = (int) Math.ceil(listenerSize /ParamUtil.getPerTaskConfigSize());if (longingTaskCount >currentLongingTaskCount) {for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {

executorService.execute(newLongPollingRunnable(i));

}

currentLongingTaskCount=longingTaskCount;

}

}

AtomicReference> cacheMap = new AtomicReference>(

new HashMap());

cacheMap:缓存着需要刷新的配置,它是在调用ConfigService 添加监听器方式时会放入,可以自定义监听配置刷新

//添加一个config监听器,用来监听dataId为ErrorCode,group为DEFAULT_GROUP的config

configService.addListener("ErrorCode","DEFAULT_GROUP",newListener() {

@OverridepublicExecutor getExecutor() {return null;

}

@Overridepublic void receiveConfigInfo(String s) { //当配置更新时会调用监听器该方法

Map> map = JSON.parseObject(s, Map.class);//根据自己的业务需要来处理

}

});

这里采用了一个策略:将cacheMap中的数量以3000分一个组,分别创建一个LongPollingRunnable用来监听配置更新,这个LongPollingRunnable就是我们之前所说的长连接任务,来看这个长连接任务

class LongPollingRunnable implementsRunnable {private inttaskId;public LongPollingRunnable(inttaskId) {this.taskId =taskId;

}

@Overridepublic voidrun() {

List cacheDatas = new ArrayList();

List inInitializingCacheList = new ArrayList();try{//check failover config

for(CacheData cacheData : cacheMap.get().values()) {if (cacheData.getTaskId() ==taskId) {

cacheDatas.add(cacheData);try{//1、检查本地配置

checkLocalConfig(cacheData);if(cacheData.isUseLocalConfigInfo()) {

cacheData.checkListenerMd5();

}

}catch(Exception e) {

LOGGER.error("get local config info error", e);

}

}

}// 2、向nacos server发出一个长连接 30s超时,返回nacos server有更新过的dataIds

List 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{//3、向nacos server请求获取config最新内容

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

}

}

}//4、对有变化的config调用对应监听器去处理

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) {//发生异常延迟执行

executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);

}

}

}

这个长轮询主要做了4个步骤

检查本地配置,如果存在本地配置,并且与缓存中的本地配置版本不一样,把本地配置内容更新到缓存,并触发事件,这块源码比较简单,读者跟到源码一读编制

向nacos server发出一个长连接,30s超时,nacos server会返回有变化的dataIds

根据变化的dataId,从服务端拉取最新的配置内容然后更新到缓存中

对有变化的配置 触发事件监听器来处理

讲完了nacos client处理流程,再来看服务端这边怎么处理这个长连接的

nacos server处理

服务端长连接接口是/config/listener,对应源码包为config

/****************************************** ConfigController ******************************************/@PostMapping("/listener")

@Secured(action= ActionTypes.READ, parser = ConfigResourceParser.class)public voidlistener(HttpServletRequest request, HttpServletResponse response)throwsServletException, IOException {

request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);

String probeModify= request.getParameter("Listening-Configs");if(StringUtils.isBlank(probeModify)) {throw new IllegalArgumentException("invalid probeModify");

}

probeModify=URLDecoder.decode(probeModify, Constants.ENCODE);//需要检查更新的config信息

MapclientMd5Map;try{

clientMd5Map=MD5Util.getClientMd5Map(probeModify);

}catch(Throwable e) {throw new IllegalArgumentException("invalid probeModify");

}//长连接处理

inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());

}/****************************************** ConfigServletInner ******************************************/

publicString doPollingConfig(HttpServletRequest request, HttpServletResponse response,

Map clientMd5Map, int probeRequestSize) throwsIOException {//判断是否支持长轮询

if(LongPollingService.isSupportLongPolling(request)) {//长轮询处理

longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);return HttpServletResponse.SC_OK + "";

}//不支持长轮询,直接与当前配置作比较,返回有变更的配置

List changedGroups =MD5Util.compareMd5(request, response, clientMd5Map);//Compatible with short polling result.

String oldResult =MD5Util.compareMd5OldResult(changedGroups);

String newResult=MD5Util.compareMd5ResultString(changedGroups);/** 省略

* 会响应变更的配置信息*/

return HttpServletResponse.SC_OK + "";

}/****************************************** LongPollingService ******************************************/

public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, MapclientMd5Map,intprobeRequestSize) {

String str=req.getHeader(LongPollingService.LONG_POLLING_HEADER);

String noHangUpFlag=req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);

String appName=req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);

String tag= req.getHeader("Vipserver-Tag");//服务端这边最多处理时长29.5s,需要留0.5s来返回,以免客户端那边超时

int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);//Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.

long timeout = Math.max(10000, Long.parseLong(str) -delayTime);if(isFixedPolling()) {

timeout= Math.max(10000, getFixedPollingInterval());//Do nothing but set fix polling timeout.

} else{//不支持长轮询 本地对比返回

long start =System.currentTimeMillis();

List changedGroups =MD5Util.compareMd5(req, rsp, clientMd5Map);if (changedGroups.size() > 0) {

generateResponse(req, rsp, changedGroups);//log....

return;

}else if (noHangUpFlag != null &&noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {//log....

return;

}

}

String ip=RequestUtil.getRemoteIp(req);//将http响应交给异步线程,返回一个异步响应上下文, 当配置更新后可以主动调用及时返回,不用非等待29.5s

final AsyncContext asyncContext =req.startAsync();//AsyncContext.setTimeout() is incorrect, Control by oneself

asyncContext.setTimeout(0L);//执行客户端长连接任务,

ConfigExecutor.executeLongPolling(newClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

}/****************************************** ClientLongPolling ******************************************/

class ClientLongPolling implementsRunnable {

@Overridepublic voidrun() {//提交一个任务,延迟29.5s执行

asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(newRunnable() {

@Overridepublic voidrun() {try{

getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());//Delete subsciber's relations.

allSubs.remove(ClientLongPolling.this);if(isFixedPolling()) {//检查变更配置 并相应

List changedGroups =MD5Util

.compareMd5((HttpServletRequest) asyncContext.getRequest(),

(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);if (changedGroups.size() > 0) {

sendResponse(changedGroups);

}else{

sendResponse(null);

}

}else{

sendResponse(null);

}

}catch(Throwable t) {

LogUtil.DEFAULT_LOG.error("long polling error:" +t.getMessage(), t.getCause());

}

}

}, timeoutTime, TimeUnit.MILLISECONDS);

allSubs.add(this);

}

}

final Queue allSubs

上面大部分地方都比较好懂,主要解释下ClientLongPolling作用,它首先会提交一个任务,无论配置有没有更新 最终都会进行响应,延迟29.5s执行,然后会把自己添加到一个队列中,之前说过,服务端这边配置有更新后 会找出正在等待配置更新的长连接任务,提前结束这个任务并返回,

来看这一步是怎么处理的

publicLongPollingService() {

allSubs= new ConcurrentLinkedQueue();

ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);//Register LocalDataChangeEvent to NotifyCenter.

NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);//Register A Subscriber to subscribe LocalDataChangeEvent.

NotifyCenter.registerSubscriber(newSubscriber() {

@Overridepublic voidonEvent(Event event) {if(isFixedPolling()) {//Ignore.

} else{if (event instanceofLocalDataChangeEvent) {

LocalDataChangeEvent evt=(LocalDataChangeEvent) event;

ConfigExecutor.executeLongPolling(newDataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));

}

}

}

@Overridepublic Class extends Event>subscribeType() {return LocalDataChangeEvent.class;

}

});

}class DataChangeTask implementsRunnable {

@Overridepublic voidrun() {try{

ConfigCacheService.getContentBetaMd5(groupKey);//找出等在该配置的长连接,然后进行提前返回

for (Iterator iter =allSubs.iterator(); iter.hasNext(); ) {

ClientLongPolling clientSub=iter.next();if(clientSub.clientMd5Map.containsKey(groupKey)) {//If published tag is not in the beta list, then it skipped.

if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {continue;

}//If published tag is not in the tag list, then it skipped.

if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {continue;

}

getRetainIps().put(clientSub.ip, System.currentTimeMillis());

iter.remove();//Delete subscribers' relationships.

clientSub.sendResponse(Arrays.asList(groupKey));

}

}

}catch(Throwable t) {

LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));

}

}

}

LongPollingService构造函数中,会注册一个订阅,用来监听LocalDataChangeEvent,当发生该事件时,会执行一个数据变更任务,这个任务就是找出等在配置的长连接,提前返回

我们在nacos控制台修改一个配置文件进行发布,会调用ConfigController.publishConfig接口,但这个接口发布的是ConfigDataChangeEvent事件,大意了。。。LocalDataChangeEvent事件发布在ConfigCacheService,这里怎么调用的我就不深追,留给有兴趣的读者

至此nacos config动态监听、刷新就串联起来了,nacos的相关源码都比较好理解,跟着源码追进去就一目了然了

#感谢您访问本站#

#本文转载自互联网,若侵权,请联系删除,谢谢!657271#qq.com#

你可能感兴趣的:(获取nacos配置中心文件值)