千家信息网

怎么进行SpringCloud配置刷新机制的简单分析

发表于:2024-11-14 作者:千家信息网编辑
千家信息网最后更新 2024年11月14日,今天就跟大家聊聊有关怎么进行SpringCloud配置刷新机制的简单分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。SpringCloud
千家信息网最后更新 2024年11月14日怎么进行SpringCloud配置刷新机制的简单分析

今天就跟大家聊聊有关怎么进行SpringCloud配置刷新机制的简单分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

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 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 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 refresh() {
Set keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}

//
public synchronized Set refreshEnvironment() {
Map before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set 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方法中执行过重的逻辑,前者会影响服务的可用性,在高并发下会阻塞太多数的请求。后者会影响配置刷新的时延性

看完上述内容,你们对怎么进行SpringCloud配置刷新机制的简单分析有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注行业资讯频道,感谢大家的支持。

0