Spring-聊聊Spring Cloud配置动态更新Nacos篇

有用过Nacos小伙伴应该清楚,在Spring Cloud项目中,可以动态更新配置类的值,例如下面场景。
我们在使用 Spring Cloud Gateway时,向做一个开关,如果新项目上线凌晨上线,测试还在验收,需要做一套ip白名单,这样场景下,就可以利用配置中心,当验收通过后,再更新对应变量值,实现动态配置。

本文将从配置加载、配置动态刷新两个角度进行分析。

配置加载

上一篇文章,研究过Spring 启动时 prepareEnvironment 方法过程,其中最后留下的疑惑为,加载完bootstrap后,还会去加载application配置吗?答案却是会的。因为二者不冲突。bootstrap配置可以立刻被夹在,但是application配置则可能由于spring.config.name 名称被更改而不被加载。

从Nacos中,加载配置类的关键在 prepareContext
SpringApplicationprepareContext 方法:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
			SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
		applyInitializers(context);
		listeners.contextPrepared(context);
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}
		...
PropertySourceBootstrapConfiguration 作用

PropertySourceBootstrapConfiguration 是一个 ApplicationContextInitializer 类型,他在 prepareContext 时,会进行初始化,并执行 initialize 方法。
PropertySourceBootstrapConfigurationinitialize 方法。

	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		CompositePropertySource composite = new CompositePropertySource(
				BOOTSTRAP_PROPERTY_SOURCE_NAME);
		AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
		boolean empty = true;
		ConfigurableEnvironment environment = applicationContext.getEnvironment();
		for (PropertySourceLocator locator : this.propertySourceLocators) {
			PropertySource<?> source = null;
			source = locator.locate(environment);  // 从 PropertySourceLocator 中,加载配置
			if (source == null) {
				continue;
			}
			logger.info("Located property source: " + source);
			composite.addPropertySource(source);     // 加入配置
			empty = false;
		}
		if (!empty) { // 配置不为空,则加入到application中。
			MutablePropertySources propertySources = environment.getPropertySources();
			String logConfig = environment.resolvePlaceholders("${logging.config:}");
			LogFile logFile = LogFile.get(environment);
			if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
				propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
			}
			insertPropertySources(propertySources, composite);  // 将composite配置加入到environment中
			reinitializeLoggingSystem(environment, logConfig, logFile);
			setLogLevels(applicationContext, environment); // 设置数据绑定日志
			handleIncludedProfiles(environment);  // 设置active profiles
		}
	}

在 上面代码中,有一段 从 propertySourceLocators 中获取PropertySource,而后将拼凑完的propertySource信息,放到environment中。
并且他并不会管是否有相同actives.profile被加载,同样会加载一份配置。通过 spring.cloud.config.allowOverride 判断是否需要覆盖对应的本地配置,默认是能覆盖。

PropertySourceLocator

PropertySourceLocator 主要用于为environment加载配置,里面只有一个方法:

public interface PropertySourceLocator {

	/**
	 * @param environment The current Environment.
	 * @return A PropertySource, or null if there is none.
	 * @throws IllegalStateException if there is a fail-fast condition.
	 */
	PropertySource<?> locate(Environment environment);

}

本文以Nacos配置为例,NacosPropertySourceLocator :

	@Override
	public PropertySource<?> locate(Environment env) {
		ConfigService configService = nacosConfigProperties.configServiceInstance();
		if (null == configService) {
			log.warn("no instance of config service found, can't load config from nacos");
			return null;
		}
		long timeout = nacosConfigProperties.getTimeout();
		nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
				timeout);
		String name = nacosConfigProperties.getName();
		String dataIdPrefix = nacosConfigProperties.getPrefix();
		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = name;
		}
		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = env.getProperty("spring.application.name");
		}
		CompositePropertySource composite = new CompositePropertySource(
				NACOS_PROPERTY_SOURCE_NAME);
		loadSharedConfiguration(composite);
		loadExtConfiguration(composite);
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		return composite;
	}

在执行 NacosPropertySourceLocatorlocate方法,总体思路比较容易理解:
Nacos通过applicationName,profile名字,加上nameSpace去调用nacos服务端接口获取配置信息,再将获取的配置解析并构造一个 CompositePropertySource 并返回。

配置更新

用过Nacos的小伙伴应该清楚,当使用Nacos作为配置中心时,在线修改Nacos配置,当使用@Refresh 或者 @ConfigurationProperties ,往往可以动态刷新配置的值。
使用Nacos作为配置中心时,本地会启动一个client端,为 com.alibaba.nacos.client.config.impl.ClientWorker。在其构造方法中,会启动多个定时任务线程池 ScheduledThreadPoolExecutor。用于定时拉取nacos变更配置信息。
对于Nacos实时更新,Nacos目前版本(1.1.4) 是通过一个timeout为30秒的http请求,即30s请求一次,一次超时30秒,服务端不立刻返回,如果这段时间有配置变更则立刻返回http请求。
具体代码可由 ClientWorker 构造方法深入理解。
当有拉取完信息后,nacos会将本次和上次拉取的信息进行对比,如果有变更,则会将变更的信息发送。

CacheDatasafeNotifyListener 方法:

    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() {
                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();
                    listener.receiveConfigInfo(contentTmp);
                    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);
    }

准备完成之后,通过 NacosContextRefresherregisterNacosListener发送Spring 事件出去。
NacosContextRefresherregisterNacosListener 方法:

			@Override
			public void receiveConfigInfo(String configInfo) {
				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);
					}
				}
				refreshHistory.add(dataId, md5);
				applicationContext.publishEvent(
						new RefreshEvent(this, null, "Refresh Nacos config"));
				if (log.isDebugEnabled()) {
					log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
				}
			}

RefreshEvent 是Spring cloud提供的刷新事件,会引发 RefreshEndpoint.refresh() 方法调用。

最终到 ContextRefresher 时,会将所有变更的keys都从environment中获取,比较完后将keys再发送 EnvironmentChangeEvent 出去有Binder进行配置刷新。

	public synchronized Set<String> refreshEnvironment() {
		Map<String, Object> before = extract(
				this.context.getEnvironment().getPropertySources());
		addConfigFilesToEnvironment();
		Set<String> keys = changes(before,
				extract(this.context.getEnvironment().getPropertySources())).keySet();
		this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
		return keys;
	}

EnvironmentChangeEvent 则是由 ConfigurationPropertiesRebinder 进行处理

	@Override
	public void onApplicationEvent(EnvironmentChangeEvent event) {
		if (this.applicationContext.equals(event.getSource())
				// Backwards compatible
				|| event.getKeys().equals(event.getSource())) {
			rebind();
		}
	}
	public void rebind() {
		this.errors.clear();
		for (String name : this.beans.getBeanNames()) {
			rebind(name);
		}
	}

具体的ConfigurationPropertiesRebinder.rebind操作:

	public boolean rebind(String name) {
		if (!this.beans.getBeanNames().contains(name)) {
			return false;
		}
		if (this.applicationContext != null) {
			try {
				Object bean = this.applicationContext.getBean(name);
				if (AopUtils.isAopProxy(bean)) {
					bean = ProxyUtils.getTargetObject(bean);
				}
				if (bean != null) {
					this.applicationContext.getAutowireCapableBeanFactory()
							.destroyBean(bean);   // 销毁
					this.applicationContext.getAutowireCapableBeanFactory()
							.initializeBean(bean, name);   // 初始化
					return true;
				}
			}
			catch (RuntimeException e) {
				this.errors.put(name, e);
				throw e;
			}
			catch (Exception e) {
				this.errors.put(name, e);
				throw new IllegalStateException("Cannot rebind to " + name, e);
			}
		}
		return false;
	}

具体处理逻辑,则是将所有配置bean中,符合要求的bean进行重新绑定一次,即destroyBeaninitializeBean 一次,而对应配置则同初始化一样,从environment中获取,这样就完成了刷新。

以上则为Spring Cloud中配置动态刷新原理。

关注博主公众号: 六点A君。
哈哈哈,一起研究Spring:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值