今天就跟大家聊聊有关怎么进行SpringCloud配置刷新机制的简单分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
主要分为SpringCloud Nacos的设计思路
简单分析一下触发刷新事件后发生的过程以及一些踩坑经验
这是一个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);
com.alibaba.cloud.nacos.client.NacosPropertySourceLocator
该类为nacos实现的配置发现类
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;
源码解析
@Overridepublic 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;}
每次nacos检查到配置更新的时候就会触发上下文配置刷新,就会调取locate这个方法
该事件为spring cloud内置的事件,用于刷新配置
该类用于nacos刷新历史的存放,用来保存每次拉取的配置的md5值,用于比较配置是否需要刷新
该类是Nacos用来管理一些内部监听器的,主要是配置刷新的时候可以出发回调,并且发出spring cloud上下文的配置刷新事件
该类是nacos用来保存拉取到的数据的
流程:
刷新器检查到配置更新,保存到NacosPropertySourceRepository
发起刷新事件
locate执行,直接读取NacosPropertySourceRepository
该类是nacos的主要刷新配置服务类
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);
com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable
该类用于长轮询任务
com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5比对MD5之后开始刷新配置
该包提供了很多文件类型的转换器
加载数据的时候会根据文件扩展名去查找一个转换器实例
// com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#loadNacosDataprivate 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;}
数据会变成key value的形式,然后转换成PropertySource
由于配置上下文是属于SpringCloud管理的,所以本次的注入跟以往SpringBoot不一样
org.springframework.cloud.bootstrap.BootstrapConfiguration=\com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
如何在SpringCloud和SpringBoot共享一个bean呢(举个例子)
@Beanpublic 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();}
// 外层方法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;}
该类是对RefreshEvent监听的处理
直接定位到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
org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind()
代码如下:
@ManagedOperationpublic 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
@Overridepublic 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;}
@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());}
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是怎么构造的?
@Overridepublic 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;}
上面的分析简单分析到那里,那么在使用这种配置自动刷新机制有什么坑呢?
使用@RefreshScople的对象,如果把配置中心的某一行属性删掉,那么对应的bean对应的属性会变为null,但是使用@ConfigaruationProperties的对象则不会,为什么呢?因为前者是整个bean重新走了一遍生命流程,但是后者只会执行init方法
不管使用@RefreshScople和@ConfigaruationProperties都不应该在destory和init方法中执行过重的逻辑,前者会影响服务的可用性,在高并发下会阻塞太多数的请求。后者会影响配置刷新的时延性
看完上述内容,你们对怎么进行SpringCloud配置刷新机制的简单分析有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注亿速云行业资讯频道,感谢大家的支持。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:https://my.oschina.net/u/4504531/blog/4944919