SpringCloud Nacos
- 本文主要分爲SpringCloud Nacos的設計思路
- 簡單分析一下觸發刷新事件後發生的過程以及一些踩坑經驗
org.springframework.cloud.bootstrap.config.PropertySourceLocator
- 這是一個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
- 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
- 源碼解析
@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;
}
- 每次nacos檢查到配置更新的時候就會觸發上下文配置刷新,就會調取locate這個方法
org.springframework.cloud.endpoint.event.RefreshEvent
- 該事件爲spring cloud內置的事件,用於刷新配置
com.alibaba.cloud.nacos.refresh.NacosRefreshHistory
- 該類用於nacos刷新歷史的存放,用來保存每次拉取的配置的md5值,用於比較配置是否須要刷新
com.alibaba.cloud.nacos.refresh.NacosContextRefresher
- 該類是Nacos用來管理一些內部監聽器的,主要是配置刷新的時候能夠出發回調,而且發出spring cloud上下文的配置刷新事件
com.alibaba.cloud.nacos.NacosPropertySourceRepository
- 該類是nacos用來保存拉取到的數據的
- 流程:
- 刷新器檢查到配置更新,保存到NacosPropertySourceRepository
- 發起刷新事件
- locate執行,直接讀取NacosPropertySourceRepository
com.alibaba.nacos.client.config.NacosConfigService
- 該類是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.parser
- 該包提供了不少文件類型的轉換器
- 加載數據的時候會根據文件擴展名去查找一個轉換器實例
// 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;
}
- 數據會變成key value的形式,而後轉換成PropertySource
如何配置一個啓動配置類
- 因爲配置上下文是屬於SpringCloud管理的,因此本次的注入跟以往SpringBoot不同
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
- 如何在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;
}
- 該類是對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
簡單分析 EnvironmentChangeEvent
- 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());
}
- 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;
}
踩坑
- 上面的分析簡單分析到那裏,那麼在使用這種配置自動刷新機制有什麼坑呢?
- 使用@RefreshScople的對象,若是把配置中心的某一行屬性刪掉,那麼對應的bean對應的屬性會變爲null,可是使用@ConfigaruationProperties的對象則不會,爲何呢?由於前者是整個bean從新走了一遍生命流程,可是後者只會執行init方法
- 無論使用@RefreshScople和@ConfigaruationProperties都不該該在destory和init方法中執行太重的邏輯,前者會影響服務的可用性,在高併發下會阻塞太多數的請求。後者會影響配置刷新的時延性
最後
- 感謝閱讀完這篇文章的大佬們,若是發現文章中有什麼錯誤的話,請留言,不甚感激!