Spring Cloud +Nacos 配置中心原理分析

前言

        Nacos因其出色的的读写性能以及简单灵活的配置方式,被很多公司应用于配置管理,将Nacos当做配置服务器或者配置中心。Nacos还可以很方便的和Spring集成,这也提高了其使用的频率

        本文将通过Nacos的简单示例,探索Nacos实现原理,如果没有搭建过Nacos服务器的同学可以参考:链接:Nacos服务器搭建

简单示例

使用Nacos +SpringCloud 可以很快的完成配置中心搭建并应用,先看一个简单示例(Nacos +SpringCloud),在SpringCloud中主要是使用@RefreshScope + @Value 来实现配置更新。

@Value的作用是通过注解将常量、配置文件中的值、其他bean的属性值注入到变量中,作为变量的初始值。

@RefreshScope 是SpringCloud中的注解,需要热加载的bean就需要加上这个注解 , 表示是需要RefreshScope 代理的bean, 这个bean强制为懒加载,只有第一次使用的时候生成实例,配置变更的时候直接调用destroy()方法销毁当前的bean, 再根据配置信息生成新的bean, 完成热加载。  RefreshScope注解,可以看看另一篇总结 【SpringCloud】RefreshScope 自动刷新配置

新增配置

  • 命名空间-新命名空间 创建一个叫 Nacos测试空间,空间名称ID=nacos-test
  • 配置列表-Nacos测试空间:右侧加号增加配置nacos-demo.yaml

项目搭建

1. 增加依赖spring-cloud-alibaba-nacos-config

plugins {
   id 'io.spring.dependency-management' version '1.0.11.RELEASE'
   id 'java'
}

group = 'com.springnacos'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
   mavenCentral()
}

dependencies {
   compile'org.springframework.boot:spring-boot-starter-web:2.0.9.RELEASE'
   compile 'org.springframework.boot:spring-boot-configuration-processor:2.0.9.RELEASE'
   compile 'org.springframework.cloud:spring-cloud-alibaba-nacos-config:0.2.2.RELEASE'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
   useJUnitPlatform()
}

2. 定义bootstrat.yml文件

Nacos当配置中心的时候,一般将nacos配置信息写入bootstrap.yml , 其中 bootstrap.yml和 application.yml区别如下

bootstrap.yml用来在程序引导时执行,应用于更加早期配置信息读取,bootstrap.yml 先于 application.yml 加载

application.yml 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。

spring:
  application:
    name: service-system
  cloud:
    nacos:
      config:
        #nacos配置中心服务器地址
        server-addr: localhost:8848
        #配置文件后缀,用于拼接配置配置文件名称
        file-extension: yaml
        #配置命名空间(填入前面新建的命名空间ID)
        namespace: nacos-test
        #配置分组
        group: TEST_GROUP
        #配置自动刷新
        refresh-enabled: true
        #配置文件的前缀 :prefix−{spring.profile.active}.${file-extension}
        #prefix表示配置文件前缀,默认是spring.application.name的值,如果配置了spring.cloud.nacos.config.prefix就取prefix的值
        #spring.profile.active 表示项目使用的profile.active配置,没有则配置文件中没有此段名称
        #file-extension 表示配置文件的后缀,目前只支持yml和properties
        prefix: nacos-demo

3. 定义配置,配置使用@Value注解,同时增加springcloud的热刷新注解@RefreshScope

package com.test.springnacos.conf;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;


package com.test.springnacos.example1;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

@RefreshScope
@Component
public class ValueConf {

    //@Value获取最新值一定要加@RefreshScope注解,配置文件中配置refresh: true
    @Value("${datasource.username}")
    private String username;

    @Value("${datasource.password}")
    private String password;

    //略getter && setter
}

4. 定义测试类

package com.test.springnacos.example1;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NacosRefreshScopeTest {

    @Autowired
    private ValueConf conf;

    //访问:http://localhost:8080/getMessage
    @RequestMapping("/getMessage")
    public String getMessage() {
        return "</br>username:" + conf.getUsername() + "</br>password:" + conf.getPassword();
    }

}

5. 测试结果

访问:http://localhost:8080/getMessage
url:nacos-service:3306
username:nacos-n
password:nacos-p
version:1.0.0

当修改配置后username:nacos-name刷新
url:nacos-service:3306
username:nacos-name
password:nacos-p
version:1.0.0

问题列表

1. 启动报错

   检查springcloud的版本是否兼容

2. 没自动更新配置

    @Value获取最新值一定要加SpringCloud @RefreshScope注解,配置文件中配置自动刷新 refresh-enabled: true

实现原理

        看看上面的示例效果,我们大概可以了解到Nacos简单的实现原理Nacos 服务其通过Namespace-Group-DataId的方式保存了配置信息,Nacos 和Springcloud集成后,Nacos 服务器信息发生变化的时候,客户端能在很短时间内发现变更,并通过Springcloud@RefreshScope 注解实现更新配置,仔细看看实例,是不是大家也有以下几个疑问呢?下面来深入探索Nacos的实现原理

  • Nacos 是如何与Springcloud无缝集成?
  • Nacos 客户端是如何知道Nacos中的配置发生了变化?
  • 客户端发现配置变更又是如何实现变更配置?

工程结构

首先在SpringCloud中应用Nacos  需要引用spring-cloud-alibaba-nacos-config 这个包,查看包中的pom文件会发现加载后还引入了 nacos-client包,

spring-cloud-alibaba-nacos-config包结构大概如下, 

  1. client包中的类:主要用于配置信息的创建与加载
  2. endpoint包中的类:主要是配置了,主要用于启动加载对象, 也就是和SpringBoot集成
  3. refresh包中的类:主要用户实现配置更新, 其中配置管理的核心类NacosContextRefresher

对象模型

        我们先看下spring.factories文件,SpringFactories是spring提供的自定义的SPI机制,SPI 全称为(Service Provider Interface)是一种动态发现服务的机制,SpringBoot启动后,它读取META-INF/spring.factories文件中配置的接口实现类名称,然后在程序中读取这些配置文件并实例化。这里提到了2个重要的配置类 NacosConfigBootstrapConfiguration和NacosConfigAutoConfiguration , 这2个Bean都有注解@Configuration ,从Spring3.0以后,@Configuration用于定义配置类,配置类用于替换原来的xml, 在配置类中定义Bean, 当Spring启动启动的时候会加载这些Bean

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.alibaba.nacos.NacosConfigAutoConfiguration,\
org.springframework.cloud.alibaba.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.cloud.alibaba.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer

1.NacosConfigBootstrapConfiguration  

NacosConfigBootstrapConfiguration  中创建了2个Bean : NacosConfigProperties 和 NacosPropertySourceLocator  对象

  • NacosConfigProperties 对象:主要用于记录Nacos的服务器相关信息,比如Nacos服务地址(serverAddr),Nacos服务分组等, 值得注意的是这个对象里有集合List<Config> ,对象里记录的的是配置中某个空间的配置信息,例如group dataId等
  • NacosPropertySourceLocator   对象:依赖NacosConfigProperties,通过NacosPropertySoucreBuilder创建并加载配置,并将配置保存到NacosPropertySourceRepository中, NacosPropertySourceRepository可以看成一个缓存对象, 这里需要注意的是NacosPropertySourceLocator 对象实现了接口PropertySourceLocator,在springcloud提供了PropertySourceLocator接口来支持扩展自定义配置加载到spring Environment中,这样就实现了启动时将Nacos配置信息加载到spring Environment

2.NacosConfigAutoConfiguration  

NacosConfigAutoConfiguration  创建了4个Bean ,

  • NacosContextRefresher 对象: 是配置刷新的核心对象,刷新配置都是通过该对象实现, 
  • NacosRefreshProperties对象:对象就一个属性,是否可以刷新,对应bootstrap.yaml 文件中配置的   spring.cloud.nacos.config.refresh.enabled
  • NacosRefreshHistory 对象,主要是记录配置变更历史,对象持有LinkedList<Record>  每一个Record 记录 timestamp,dataId ,md5 ,其中md5是配置内容加密后的字符串
  • NacosConfiProperties对象:对象主要记录了nacos服务器配置并提供了获取ConfigService服务实例的行为方法
@Configuration
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {

	@Bean
	@ConditionalOnMissingBean  //这个注解表示存在了就不再创建相同的Bean
	public NacosConfigProperties nacosConfigProperties() {
		return new NacosConfigProperties();
	}

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

}


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

	@Bean
	public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
		if (context.getParent() != null
				&& BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
						context.getParent(), NacosConfigProperties.class).length > 0) {
			return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
					NacosConfigProperties.class);
		}
		NacosConfigProperties nacosConfigProperties = new NacosConfigProperties();
		return nacosConfigProperties;
	}

	@Bean
	public NacosRefreshProperties nacosRefreshProperties() {
		return new NacosRefreshProperties();
	}

	@Bean
	public NacosRefreshHistory nacosRefreshHistory() {
		return new NacosRefreshHistory();
	}

	@Bean
	public NacosContextRefresher nacosContextRefresher(
			NacosConfigProperties nacosConfigProperties,
			NacosRefreshProperties nacosRefreshProperties,
			NacosRefreshHistory refreshHistory) {
		return new NacosContextRefresher(nacosRefreshProperties, refreshHistory,
				nacosConfigProperties.configServiceInstance());
	}
}

3.NacosConfigProperties

NacosConfigProperties 是Nacos配置的核心对象,记录了serverAddr,namespace,goup等配置信息以及 List<Config> 和  ConfigService

  • 成员变量记录了nacos服务的基本配置信息, List<Config> 用于保存 dataId 和 group 等信息
  • 提供获取ConfigService 是一个接口实例的方法,用于实现服务器端连接通信,NacosConfigProperties  通过NacosFactory获取接口
	private String serverAddr;
	private String encode;
	private String group = "DEFAULT_GROUP";
	private String prefix;
	private String fileExtension = "properties";
	private int timeout = 3000;
	private String endpoint;
	private String namespace;
	private String accessKey;
	private String secretKey;
	private String contextPath;
	private String clusterName;
	private String name;
	private String sharedDataids;
	private String refreshableDataids;
	private List<Config> extConfig;
	private ConfigService configService;

    //..
    public ConfigService configServiceInstance() {
        
        //...
		try {
			configService = NacosFactory.createConfigService(properties);
			return configService;
		}
		catch (Exception e) {
			log.error("create config service error!properties={},e=,", this, e);
			return null;
		}
    }

4.NacosContextRefresher

NacosContextRefresher 是实现刷新的核心对象,对象本身继承了ApplicationListener ,表明自己本身是Spring的监听器,当Spring发布事件的时候就会获取事件,当捕获事件后会向configserver注册一个监听器

	
    //用于记录配置是否支持刷新
    private final NacosRefreshProperties refreshProperties;
    //用于和服务器链接
    private final ConfigService configService;
    
	public NacosContextRefresher(NacosRefreshProperties refreshProperties,
			NacosRefreshHistory refreshHistory, ConfigService configService) {
		this.refreshProperties = refreshProperties;
		this.refreshHistory = refreshHistory;
		this.configService = configService;
	}
    
	@Override
	public void onApplicationEvent(ApplicationReadyEvent event) {
		// 用于捕获Spring的事件
		if (this.ready.compareAndSet(false, true)) {
            //通过此方法将自定义的监听器注册Nacos服务器
            //以便服务器配置发生变更后通知客户端
			this.registerNacosListenersForApplications();
		}
	}

	private void registerNacosListenersForApplications() {
        //如果支持刷新
		if (refreshProperties.isEnabled()) {
            //所有Repository缓存的配置信息都刷新
			for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository
					.getAll()) {

				if (!nacosPropertySource.isRefreshable()) {
					continue;
				}

				String dataId = nacosPropertySource.getDataId();
                //将配置注册到服务器端
				registerNacosListener(nacosPropertySource.getGroup(), dataId);
			}
		}
	}
    
    private void registerNacosListener(final String group, final String dataId) {
        //1。 创建监听器
		Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
			@Override
			public void receiveConfigInfo(String configInfo) {
                //监听器的回调方法: 当服务器端配置发生变化的时候会调用这个方法来实现变更客户端信息    
                //变更客户端配置
                //1.变更刷新次数
				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 (NoSuchAlgorithmException | UnsupportedEncodingException e) {
						log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
					}
				}
                //2.记录变更历史
				refreshHistory.add(dataId, md5);
                //3.发布RefreshEvent事件,更新客户端的配置
				applicationContext.publishEvent(
						new RefreshEvent(this, null, "Refresh Nacos config"));
				if (log.isDebugEnabled()) {
					log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
				}
			}

			@Override
			public Executor getExecutor() {
				return null;
			}
		});
        
        //2. 将监听器注册到服务器端,
		try {
			configService.addListener(dataId, group, listener);
		}
		catch (NacosException e) {
			e.printStackTrace();
		}

    }

5.ConfigService

刚才看到 NacosConfigProperties中通过 configService = NacosFactory.createConfigService(properties); 那现在来看看ConfigService, ConfigService 是一个接口,属于nacos-api工程中。当引用spring-cloud-alibaba-nacos-config的时候会自动引入这个工程,


public class NacosFactory {

    public static ConfigService createConfigService(Properties properties) throws NacosException {
        //1.通过工厂获取
        return ConfigFactory.createConfigService(properties);
    }
    // .....
}

public class ConfigFactory {

    
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            //2.通过反射得到NacosConfigService实例
            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 e) {
            throw new NacosException(-400, e.getMessage());
        }
    //...
}

从程序上看,通过反射得到了ConfigService接口的一个实例  NacosConfigService (位于nacos-client工程中),查看NacosConfigService 对象信息对象,他持有2个重要对象,代理对象HttpAgent和长轮询对象ClientWorker,

    /**
     * http agent  //1. HTTP代理
     */
    private HttpAgent agent;
    /**
     * longpolling //2. 长轮询对象
     */
    private ClientWorker worker;
 
   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);

        //3.agent  的实际工作者是ServerHttpAgent
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        worker = new ClientWorker(agent, configFilterChainManager);
    }

6.HttpAgent

HttpAgent 是个代理接口,实现对象是 ServiceHttpAgent,通过properties对象解析出serverUrls 完成Http调用

     @Override
    public HttpResult httpGet(String path, List<String> headers, List<String> paramValues, String encoding,
                              long readTimeoutMs) throws IOException {
        final long endTime = System.currentTimeMillis() + readTimeoutMs;

        boolean isSSL = false;

        do {
            try {
                List<String> newHeaders = getSpasHeaders(paramValues);
                if (headers != null) {
                    newHeaders.addAll(headers);
                }
                HttpResult result = HttpSimpleClient.httpGet(
                    getUrl(serverListMgr.getCurrentServerAddr(), path, isSSL), newHeaders, paramValues, encoding,
                    readTimeoutMs, isSSL);
                if (result.code == HttpURLConnection.HTTP_INTERNAL_ERROR
                    || result.code == HttpURLConnection.HTTP_BAD_GATEWAY
                    || result.code == HttpURLConnection.HTTP_UNAVAILABLE) {
                    LOGGER.error("[NACOS ConnectException] currentServerAddr: {}, httpCode: {}",
                        serverListMgr.getCurrentServerAddr(), result.code);
                } else {
                    return result;
                }
            } catch (ConnectException ce) {
                LOGGER.error("[NACOS ConnectException] currentServerAddr:{}", serverListMgr.getCurrentServerAddr());
                serverListMgr.refreshCurrentServerAddr();
            } catch (SocketTimeoutException stoe) {
                LOGGER.error("[NACOS SocketTimeoutException] currentServerAddr:{}", serverListMgr.getCurrentServerAddr());
                serverListMgr.refreshCurrentServerAddr();
            } catch (IOException ioe) {
                LOGGER.error("[NACOS IOException] currentServerAddr: " + serverListMgr.getCurrentServerAddr(), ioe);
                throw ioe;
            }
        } while (System.currentTimeMillis() <= endTime);

        LOGGER.error("no available server");
        throw new ConnectException("no available server");
    }

7.ClientWorker

ClientWork维护了2个线程池

  • 第一个线程池只拥有一个线程的线程池用来执行定时任务的 executor,每隔 10毫秒就会执行一次,从方法名上看checkConfigInfo() 方法是一个检查配置信息的方法
  • 第二个线程池是一个普通的线程池,从名称上看线程池是做长轮询的,而且线程是守护线程
   @SuppressWarnings("PMD.ThreadPoolCreationRule")
    public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;
        //1. 只有1个线程的线程池
        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;
            }
        });
        
        //2.创建一个长链接线程,且是守护线程
        executorService = Executors.newCachedThreadPool(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;
            }
        });
        
        //3.线程池调度每隔10毫秒执行checkConfigInfo方法
        executor.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                try {
                    //4.从名称上看是一个检查配置的方法
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

    //5.检查的时候使用长链接的线程池线程处理长链接任务LongPollingRunnable
    public void checkConfigInfo() {
        // 分任务
        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(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

 8.LongPollingRunnable

 class LongPollingRunnable implements Runnable {
        private int taskId;

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

        public void run() {
            try {
                List<CacheData> cacheDatas = new ArrayList<CacheData>();
                //1.循环cacheMap中的每一个CacheData 检查本地配置checkLocalConfig
                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);
                        }
                    }
                }

                List<String> inInitializingCacheList = new ArrayList<String>();
                // check server config  检测服务器上变更的的配置
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                
                //2.每一个groupKey 循环处理
                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.获取服务器端的配置信息
                        String content = getServerConfig(dataId, group, tenant, 3000L);        
                        //4.将配置信息存入 CacheData 对象中,content记录配置值md5记录将content字符串做md5加密后的值
                        CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(content);
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}",
                            agent.getName(), dataId, group, tenant, cache.getMd5(),
                            ContentUtils.truncateContent(content));
                    } 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))) {
                        //5.通过checkListenerMd5检测cacheData之前的MD5数据和从服务器获取的MD5配置是否相同,如果不同质性safeNotifyListener
                        cacheData.checkListenerMd5();
                        cacheData.setInitializing(false);
                    }
                }
                inInitializingCacheList.clear();
            } catch (Throwable e) {
                LOGGER.error("longPolling error", e);
            } finally {
                executorService.execute(this);
            }
        }
    }


private void safeNotifyListener(final String dataId, final String group, final String content,
                                    final String md5, final ManagerListenerWrap listenerWrap) {
        //6.获取监听,还记得之前NacosContextRefresher对象接收spring事件后会调用ConfigService注册一个监听器吗,这里就开始用到了
        final Listener listener = listenerWrap.listener;
        //7.独立线程处理
        Runnable job = new Runnable() {
            public void run() {
                ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
                ClassLoader appClassLoader = listener.getClass().getClassLoader();
                try {
                    if (listener instanceof AbstractSharedListener) {
                        AbstractSharedListener adapter = (AbstractSharedListener)listener;
                        adapter.fillContext(dataId, group);
                        LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                    }
                    // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                    Thread.currentThread().setContextClassLoader(appClassLoader);

                    ConfigResponse cr = new ConfigResponse();
                    cr.setDataId(dataId);
                    cr.setGroup(group);
                    cr.setContent(content);
                    configFilterChainManager.doFilter(null, cr);
                    String contentTmp = cr.getContent();
                    //8.调用监听并接收最新配置
                    listener.receiveConfigInfo(contentTmp);
                    //9.更新最新的MD5值
                    listenerWrap.lastCallMd5 = md5;
                    LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                        listener);
                } catch (NacosException de) {
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,
                        dataId, group, md5, listener, de.getErrCode(), de.getErrMsg());
                } catch (Throwable t) {
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId, group,
                        md5, listener, t.getCause());
                } finally {
                    Thread.currentThread().setContextClassLoader(myClassLoader);
                }
            }
        };

        final long startNotify = System.currentTimeMillis();
        try {
            if (null != listener.getExecutor()) {
                listener.getExecutor().execute(job);
            } else {
                job.run();
            }
        } catch (Throwable t) {
            LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId, group,
                md5, listener, t.getCause());
        }
        final long finishNotify = System.currentTimeMillis();
        LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
            name, (finishNotify - startNotify), dataId, group, md5, listener);
    }

看到这里我们大概可以总结整体实现过程:

  •  SpringBoot启动后加载了配置类,配置类中定义了Nacos的对象Bean,
  •  NacosContextRefresher创建了1个接收configInfo配置信息的监听器Listener注册到NacosConfigService服务的 ClientWorker 的数据对象CacheData中 
  •  NacosConfigService 对象在初始化的时候启动了ClientWorker对象的配置检测任务, 
  •  ClientWorker每10毫秒就会通过ServerHttpAgent 连接Nacos服务器并拉取最新的配置信息,并与客户端的信息比较 
  •  ClientWorker比较后发现有不同的配置中 则会 从 CacheData的监听类表中找到group和dataId对应的Listener
  • 最后执行 Listener的receiveConfigInfo方法,receiveConfigInfo方法中发布RefreshEvent ,通过SpringCould的@RefreshScope 可以实现动态刷新

程序流程

启动时加载配置对象

  1. SpringBoot启动后创建对象SpringApplication并启动运行,启动后properContext阶段处理中会循环所有的ApplicationContextInitializer对象并初始化,PropertySourceBootstrapConfiguration对象实现了ApplicationContextInitializer,用于加载配置
  2. PropertySourceBootstrapConfiguration初始化的时候会将所实现PropertySourceLocator接口的Bean做locate操作,刚才说了NacosPropertySourceLocator 对象实现了接口PropertySourceLocator,所以执行NacosPropertySourceLocator #locate初始化Naocos相关的信息
  3. NacosPropertySourceLocator加载时候先通过NacosConfigProperties对象获取ConfigService接口, NacosConfigProperties记录了Nacos配置信息通过NacosFactory用反射的方式返回ConfigService接口。
  4. 得到ConfigService接口后,创建NacosPropertySourceBuilder对象
  5. 然后加载数据,loadNacosDataIfPresent()加载处理的时候先判断是否刷新过,如果刷新过,直接NacosPropertySourceRepository从中获取配置,如果没有刷新过调用nacosPropertySourceBuilder#build方法获取配置信息NacosPropertySource
  6. NacosPropertySourceBuilder处理时先通过configService#getConfig 从Nacos服务器查询到配置信息对象,这是个String对象, 然后转换成Properties配置对象
  7. NacosPropertySourceBuilder得到配置对象Properties后,将其封装成NacosPropertySource信息,并将信息保存到Nacos缓存对象NacosPropertySourceRepository对象中
  8. 这些都处理后,NacosPropertySourceLocator同时将NacosPropertySource信息加载到CompositePropertySource中

启动时注册客户端监听

Nacos启动的时候是如何更新配置的呢?这需要看下核心对象NacosContextRefresher的源码就一目了然了, NacosContextRefresher对象实现了接口ApplicationListener,ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。当ApplicationContext发布ApplicationEvent时,ApplicationListener的所有实现Bean都将自动被触发,那么更新配置肯定是通过事件来实现的

SpringBoot启动后会对事件监听器做很多操作,先starting, 然后start等, 我们查下源码会发现refreshContext阶段处理后会调用#listeners.running(context)

public ConfigurableApplicationContext run(String... args) {
        ....
		SpringApplicationRunListeners listeners = getRunListeners(args);//得到所有监听器
		listeners.starting();
		try {
			.....
			context = createApplicationContext();
			.....
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			refreshContext(context);// 这里开始加载配置
			.....
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);//运行监听器
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}

  1.  NacosContextRefresher对象实现了接口ApplicationListener 并且是一个Bean, 那么SpringApplicaiton#getRunListeners的时候得到了NacosContextRefresher对象
  2.  listeners.running(context) 运行鉴定的时候会发布ApplicationReadyEvent 事件
  3.  NacosContextRefresher 捕获ApplicationReadyEvent事件后,调用本地方法registerNacosListenersForApplications实现向服务器注册NacosListener,configService#addListener(dataId, group, listener),当服务器修改了配置,客户端将使用此监听器实现回调
  4.  针对NacosPropertySourceRepository缓存中的所有配置,创建监听器,监听器中发布了刷新事件RefreshEvent ,SpirngCoud通过RefreshEvent 来实现更新spring Environment, 具体更新的方式就和之前介绍的@RefreshScope处理流程一样
public interface ConfigService {
  
  /**
     * Add a listener to the configuration, after the server modified the configuration, the client will use the
     * incoming listener callback. Recommended asynchronous processing, the application can implement the getExecutor
     * method in the ManagerListener, provide a thread pool of execution. If not provided, use the main thread callback, May
     * block other configurations or be blocked by other configurations.
     *
     * @param dataId   dataId
     * @param group    group
     * @param listener listener
     * @throws NacosException NacosException
     */
    void addListener(String dataId, String group, Listener listener) throws NacosException;

    //...
}

变更配置并更新

  • ClientWorker 每10毫秒检测一次配置信息,启动一个长链接线程
  • 线程先从Nacos服务器获取变更的配置ID
  • 发现有变更配置后,再从服务器获取变更的配置信息并MD5加密
  • 通过MD5的内容比较新旧配置是否一样, 不一致的时候cacheData创建异步线程, 将变更内容发送给注册的监听器 
  • 注册的监听器先保存刷新历史信息, 然后发布RefreshEvent
  • SpringCloud的RefreshEventListener捕获变更事件 后, 更新环境配置

全文总结

  • SpringCloud+Nacos 使用@RefreshScope + @Value 方式获取并动态更新配置 ,使用时候注意注解@RefreshScope,并且naocos配置refresh-enabled:=true
  • NacosContextRefresher 将监听注册到ClientWorker中, ClientWorker每10毫秒从服务上获取变更配置并检查,不一致的时候通知注册的监听器更新Spring配置
  • ClientWorker链接服务器使用的方式是长轮询,属于"拉取方式",优点服务器不需要维护有哪些客户端,服务器占用资源少 

下一篇:SpringSession原理以及源码分析

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

=PNZ=BeijingL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值