3

SpringCloud配置刷新机制的简单分析[nacos为例子]

 3 years ago
source link: http://www.cnblogs.com/MyNameIsSam/p/14348506.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SpringCloud Nacos

  1. 本文主要分为SpringCloud Nacos的设计思路
  2. 简单分析一下触发刷新事件后发生的过程以及一些踩坑经验

org.springframework.cloud.bootstrap.config.PropertySourceLocator

  1. 这是一个SpringCloud提供的启动器加载配置类,实现locate,注入到上下文中即可发现配置
/**
 * @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);
  1. com.alibaba.cloud.nacos.client.NacosPropertySourceLocator
  • 该类为nacos实现的配置发现类
  1. org.springframework.core.env.PropertySource
  • 改类为springcloud抽象出来表达属性源的类
  • com.alibaba.cloud.nacos.client.NacosPropertySource / nacos实现了这个类,并赋予了其他属性
/**
 * Nacos Group.
 */
private final String group;

/**
 * Nacos dataID.
 */
private final String dataId;

/**
 * timestamp the property get.
 */
private final Date timestamp;

/**
 * Whether to support dynamic refresh for this Property Source.
 */
private final boolean isRefreshable;

大概讲解com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#locate

  1. 源码解析
@Override
public PropertySource<?> locate(Environment env) {
	nacosConfigProperties.setEnvironment(env);
	// 获取nacos配置的服务类,http协议,访问nacos的api接口获得配置
	ConfigService configService = nacosConfigManager.getConfigService();

	if (null == configService) {
		log.warn("no instance of config service found, can't load config from nacos");
		return null;
	}
	long timeout = nacosConfigProperties.getTimeout();
	// 构建一个builder
	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);
	// 加载应用配置,应用配置的优先级是最高,所以这里放在最后面来做,是因为添加配置的地方都是addFirst,所以最先的反而优先级最后
	loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

	return composite;
}
  1. 每次nacos检查到配置更新的时候就会触发上下文配置刷新,就会调取locate这个方法

org.springframework.cloud.endpoint.event.RefreshEvent

  1. 该事件为spring cloud内置的事件,用于刷新配置

com.alibaba.cloud.nacos.refresh.NacosRefreshHistory

  1. 该类用于nacos刷新历史的存放,用来保存每次拉取的配置的md5值,用于比较配置是否需要刷新

com.alibaba.cloud.nacos.refresh.NacosContextRefresher

  1. 该类是Nacos用来管理一些内部监听器的,主要是配置刷新的时候可以出发回调,并且发出spring cloud上下文的配置刷新事件

com.alibaba.cloud.nacos.NacosPropertySourceRepository

  1. 该类是nacos用来保存拉取到的数据的
  2. 流程:
  • 刷新器检查到配置更新,保存到NacosPropertySourceRepository
  • 发起刷新事件
  • locate执行,直接读取NacosPropertySourceRepository

com.alibaba.nacos.client.config.NacosConfigService

  1. 该类是nacos的主要刷新配置服务类
  2. com.alibaba.nacos.client.config.impl.ClientWorker
  • 该类是服务类里主要的客户端,协议是HTTP
  • clientWorker启动的时候会初始化2个线程池,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;
            }
        });

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);
        }
    }
}, 1L, 10L, TimeUnit.MILLISECONDS);
  1. com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable
  • 该类用于长轮询任务
  • com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5比对MD5之后开始刷新配置

com.alibaba.cloud.nacos.parser

  1. 该包提供了很多文件类型的转换器
  2. 加载数据的时候会根据文件扩展名去查找一个转换器实例
// com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#loadNacosData
private Map<String, Object> loadNacosData(String dataId, String group,
			String fileExtension) {
	String data = null;
	try {
		data = configService.getConfig(dataId, group, timeout);
		if (StringUtils.isEmpty(data)) {
			log.warn(
					"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
					dataId, group);
			return EMPTY_MAP;
		}
		if (log.isDebugEnabled()) {
			log.debug(String.format(
					"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
					group, data));
		}
		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;
}
  1. 数据会变成key value的形式,然后转换成PropertySource

如何配置一个启动配置类

  1. 由于配置上下文是属于SpringCloud管理的,所以本次的注入跟以往SpringBoot不一样
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
  1. 如何在SpringCloud和SpringBoot共享一个bean呢(举个例子)
@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);
	}
	return new NacosConfigProperties();
}

关于刷新机制的流程

org.springframework.cloud.endpoint.event.RefreshEventListener

// 外层方法
public synchronized Set<String> refresh() {
	Set<String> keys = refreshEnvironment();
	this.scope.refreshAll();
	return keys;
}

// 
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;
}
  1. 该类是对RefreshEvent监听的处理
  2. 直接定位到org.springframework.cloud.context.refresh.ContextRefresher#refreshEnvironment,这个方法是主要的刷新配置的方法,具体做的事:
  • 归并得到刷新之前的配置key value
  • org.springframework.cloud.context.refresh.ContextRefresher#addConfigFilesToEnvironment 模拟一个新的SpringApplication,触发大部分的SpringBoot启动流程,因此也会触发读取配置,于是就会触发上文所讲的Locator,然后得到一个新的Spring应用,从中获取新的聚合配置源,与旧的Spring应用配置源进行比较,并且把本次变更的配置放置到旧的去,然后把新的Spring应用关闭
  • 比较新旧配置,把配置拿出来,触发一个事件org.springframework.cloud.context.environment.EnvironmentChangeEvent
  • 跳出该方法栈后,执行org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll

简单分析 EnvironmentChangeEvent

  1. org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind()
  • 代码如下:
@ManagedOperation
public boolean rebind(String name) {
	if (!this.beans.getBeanNames().contains(name)) {
		return false;
	}
	if (this.applicationContext != null) {
		try {
			Object bean = this.applicationContext.getBean(name);
			// 获取source对象
			if (AopUtils.isAopProxy(bean)) {
				bean = ProxyUtils.getTargetObject(bean);
			}
			if (bean != null) {
				// 重新触发销毁和初始化的周期方法
				this.applicationContext.getAutowireCapableBeanFactory()
						.destroyBean(bean);
			    // 因为触发初始化生命周期,就可以触发
			    // org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization
				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呢?
  • org.springframework.cloud.context.properties.ConfigurationPropertiesBeans#postProcessBeforeInitialization 该方法会在Spring refresh上下文时候执行的bean生命后期里的其中一个后置处理器,它会检查注解 @ConfigurationProperties,这些bean就是上面第一步讲的重绑定的bean
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
		throws BeansException {
	if (isRefreshScoped(beanName)) {
		return bean;
	}
	ConfigurationProperties annotation = AnnotationUtils
			.findAnnotation(bean.getClass(), ConfigurationProperties.class);
	if (annotation != null) {
		this.beans.put(beanName, bean);
	}
	else if (this.metaData != null) {
		annotation = this.metaData.findFactoryAnnotation(beanName,
				ConfigurationProperties.class);
		if (annotation != null) {
			this.beans.put(beanName, bean);
		}
	}
	return bean;
}

简单分析org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll

@ManagedOperation(description = "Dispose of the current instance of all beans "
			+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
	super.destroy();
	this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
  1. org.springframework.cloud.context.scope.GenericScope#destroy()
  • 对BeanLifecycleWrapper实例集合进行销毁
  • BeanLifecycleWrapper是什么?
private static class BeanLifecycleWrapper {
    // bean的名字
	private final String name;
    // 获取bean
	private final ObjectFactory<?> objectFactory;
    // 真正的实例
	private Object bean;
    // 销毁函数
	private Runnable callback;
}
  • BeanLifecycleWrapper是怎么构造的?
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
	BeanLifecycleWrapper value = this.cache.put(name,
			new BeanLifecycleWrapper(name, objectFactory));
	this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
	try {
		return value.getBean();
	}
	catch (RuntimeException e) {
		this.errors.put(name, e);
		throw e;
	}
}
  • 以上代码可以追溯到Spring在创建bean的某一个分支代码,org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 347行代码
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
	throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
	Object scopedInstance = scope.get(beanName, () -> {
		beforePrototypeCreation(beanName);
		try {
			return createBean(beanName, mbd, args);
		}
		finally {
			afterPrototypeCreation(beanName);
		}
	});
	bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
  • 销毁完之后呢?其实就是把BeanLifecycleWrapper绑定的bean变成了null,那配置怎么刷新呢?@RefreshScope标记的对象一开始就是被初始化为代理对象,然后在执行它的@Value的属性的get操作的时候,会进入代理方法,代理方法里会去获取Target,这里就会触发 org.springframework.cloud.context.scope.GenericScope#get
public Object getBean() {
	if (this.bean == null) {
		synchronized (this.name) {
			if (this.bean == null) {
			    // 因为bean为空,所以会触发一次bean的重新初始化,走了一遍生命周期流程所以配置又回来了
				this.bean = this.objectFactory.getObject();
			}
		}
	}
	return this.bean;
}

踩坑

  1. 上面的分析简单分析到那里,那么在使用这种配置自动刷新机制有什么坑呢?
  • 使用@RefreshScople的对象,如果把配置中心的某一行属性删掉,那么对应的bean对应的属性会变为null,但是使用@ConfigaruationProperties的对象则不会,为什么呢?因为前者是整个bean重新走了一遍生命流程,但是后者只会执行init方法
  • 不管使用@RefreshScople和@ConfigaruationProperties都不应该在destory和init方法中执行过重的逻辑,前者会影响服务的可用性,在高并发下会阻塞太多数的请求。后者会影响配置刷新的时延性

最后

  1. 感谢阅读完这篇文章的大佬们,如果发现文章中有什么错误的话,请留言,不甚感激!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK