溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么進(jìn)行SpringCloud配置刷新機(jī)制的簡(jiǎn)單分析

發(fā)布時(shí)間:2021-12-02 15:47:45 來源:億速云 閱讀:135 作者:柒染 欄目:云計(jì)算

今天就跟大家聊聊有關(guān)怎么進(jìn)行SpringCloud配置刷新機(jī)制的簡(jiǎn)單分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

SpringCloud Nacos

  1. 主要分為SpringCloud Nacos的設(shè)計(jì)思路

  2. 簡(jiǎn)單分析一下觸發(fā)刷新事件后發(fā)生的過程以及一些踩坑經(jīng)驗(yàn)

org.springframework.cloud.bootstrap.config.PropertySourceLocator
  1. 這是一個(gè)SpringCloud提供的啟動(dòng)器加載配置類,實(shí)現(xiàn)locate,注入到上下文中即可發(fā)現(xiàn)配置

/**
 * @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實(shí)現(xiàn)的配置發(fā)現(xiàn)類

  1. org.springframework.core.env.PropertySource

  • 改類為springcloud抽象出來表達(dá)屬性源的類

  • com.alibaba.cloud.nacos.client.NacosPropertySource / nacos實(shí)現(xiàn)了這個(gè)類,并賦予了其他屬性

/**
 * 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配置的服務(wù)類,http協(xié)議,訪問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();
 // 構(gòu)建一個(gè)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");
 }
    // 構(gòu)建一個(gè)復(fù)合數(shù)據(jù)源
 CompositePropertySource composite = new CompositePropertySource(
   NACOS_PROPERTY_SOURCE_NAME);
    // 加載共享的配置
 loadSharedConfiguration(composite);
 // 加載擴(kuò)展配置
 loadExtConfiguration(composite);
 // 加載應(yīng)用配置,應(yīng)用配置的優(yōu)先級(jí)是最高,所以這里放在最后面來做,是因?yàn)樘砑优渲玫牡胤蕉际莂ddFirst,所以最先的反而優(yōu)先級(jí)最后
 loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

 return composite;
}
  1. 每次nacos檢查到配置更新的時(shí)候就會(huì)觸發(fā)上下文配置刷新,就會(huì)調(diào)取locate這個(gè)方法

org.springframework.cloud.endpoint.event.RefreshEvent
  1. 該事件為spring cloud內(nèi)置的事件,用于刷新配置

com.alibaba.cloud.nacos.refresh.NacosRefreshHistory
  1. 該類用于nacos刷新歷史的存放,用來保存每次拉取的配置的md5值,用于比較配置是否需要刷新

com.alibaba.cloud.nacos.refresh.NacosContextRefresher
  1. 該類是Nacos用來管理一些內(nèi)部監(jiān)聽器的,主要是配置刷新的時(shí)候可以出發(fā)回調(diào),并且發(fā)出spring cloud上下文的配置刷新事件

com.alibaba.cloud.nacos.NacosPropertySourceRepository
  1. 該類是nacos用來保存拉取到的數(shù)據(jù)的

  2. 流程:

  • 刷新器檢查到配置更新,保存到NacosPropertySourceRepository

  • 發(fā)起刷新事件

  • locate執(zhí)行,直接讀取NacosPropertySourceRepository

com.alibaba.nacos.client.config.NacosConfigService
  1. 該類是nacos的主要刷新配置服務(wù)類

  2. com.alibaba.nacos.client.config.impl.ClientWorker

  • 該類是服務(wù)類里主要的客戶端,協(xié)議是HTTP

  • clientWorker啟動(dòng)的時(shí)候會(huì)初始化2個(gè)線程池,1個(gè)用于定時(shí)檢查配置,1個(gè)用于輔助檢查

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

  • 該類用于長(zhǎng)輪詢?nèi)蝿?wù)

  • com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5比對(duì)MD5之后開始刷新配置

com.alibaba.cloud.nacos.parser
  1. 該包提供了很多文件類型的轉(zhuǎn)換器

  2. 加載數(shù)據(jù)的時(shí)候會(huì)根據(jù)文件擴(kuò)展名去查找一個(gè)轉(zhuǎn)換器實(shí)例

// 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. 數(shù)據(jù)會(huì)變成key value的形式,然后轉(zhuǎn)換成PropertySource

如何配置一個(gè)啟動(dòng)配置類
  1. 由于配置上下文是屬于SpringCloud管理的,所以本次的注入跟以往SpringBoot不一樣

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
  1. 如何在SpringCloud和SpringBoot共享一個(gè)bean呢(舉個(gè)例子)

@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();
}
關(guān)于刷新機(jī)制的流程
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. 該類是對(duì)RefreshEvent監(jiān)聽的處理

  2. 直接定位到org.springframework.cloud.context.refresh.ContextRefresher#refreshEnvironment,這個(gè)方法是主要的刷新配置的方法,具體做的事:

  • 歸并得到刷新之前的配置key value

  • org.springframework.cloud.context.refresh.ContextRefresher#addConfigFilesToEnvironment 模擬一個(gè)新的SpringApplication,觸發(fā)大部分的SpringBoot啟動(dòng)流程,因此也會(huì)觸發(fā)讀取配置,于是就會(huì)觸發(fā)上文所講的Locator,然后得到一個(gè)新的Spring應(yīng)用,從中獲取新的聚合配置源,與舊的Spring應(yīng)用配置源進(jìn)行比較,并且把本次變更的配置放置到舊的去,然后把新的Spring應(yīng)用關(guān)閉

  • 比較新舊配置,把配置拿出來,觸發(fā)一個(gè)事件org.springframework.cloud.context.environment.EnvironmentChangeEvent

  • 跳出該方法棧后,執(zhí)行org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll

簡(jiǎn)單分析 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對(duì)象
   if (AopUtils.isAopProxy(bean)) {
    bean = ProxyUtils.getTargetObject(bean);
   }
   if (bean != null) {
    // 重新觸發(fā)銷毀和初始化的周期方法
    this.applicationContext.getAutowireCapableBeanFactory()
      .destroyBean(bean);
       // 因?yàn)橛|發(fā)初始化生命周期,就可以觸發(fā)
       // 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;
}
  • 該方法時(shí)接受到事件后,對(duì)一些bean進(jìn)行屬性重綁定,具體哪些Bean呢?

  • org.springframework.cloud.context.properties.ConfigurationPropertiesBeans#postProcessBeforeInitialization 該方法會(huì)在Spring refresh上下文時(shí)候執(zhí)行的bean生命后期里的其中一個(gè)后置處理器,它會(huì)檢查注解 @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;
}
簡(jiǎn)單分析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()

  • 對(duì)BeanLifecycleWrapper實(shí)例集合進(jìn)行銷毀

  • BeanLifecycleWrapper是什么?

private static class BeanLifecycleWrapper {
    // bean的名字
 private final String name;
    // 獲取bean
 private final ObjectFactory<?> objectFactory;
    // 真正的實(shí)例
 private Object bean;
    // 銷毀函數(shù)
 private Runnable callback;

  • BeanLifecycleWrapper是怎么構(gòu)造的?

@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在創(chuàng)建bean的某一個(gè)分支代碼,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);
}
  • 銷毀完之后呢?其實(shí)就是把BeanLifecycleWrapper綁定的bean變成了null,那配置怎么刷新呢?@RefreshScope標(biāo)記的對(duì)象一開始就是被初始化為代理對(duì)象,然后在執(zhí)行它的@Value的屬性的get操作的時(shí)候,會(huì)進(jìn)入代理方法,代理方法里會(huì)去獲取Target,這里就會(huì)觸發(fā) org.springframework.cloud.context.scope.GenericScope#get

public Object getBean() {
 if (this.bean == null) {
  synchronized (this.name) {
   if (this.bean == null) {
       // 因?yàn)閎ean為空,所以會(huì)觸發(fā)一次bean的重新初始化,走了一遍生命周期流程所以配置又回來了
    this.bean = this.objectFactory.getObject();
   }
  }
 }
 return this.bean;
}
踩坑
  1. 上面的分析簡(jiǎn)單分析到那里,那么在使用這種配置自動(dòng)刷新機(jī)制有什么坑呢?

  • 使用@RefreshScople的對(duì)象,如果把配置中心的某一行屬性刪掉,那么對(duì)應(yīng)的bean對(duì)應(yīng)的屬性會(huì)變?yōu)閚ull,但是使用@ConfigaruationProperties的對(duì)象則不會(huì),為什么呢?因?yàn)榍罢呤钦麄€(gè)bean重新走了一遍生命流程,但是后者只會(huì)執(zhí)行init方法

  • 不管使用@RefreshScople和@ConfigaruationProperties都不應(yīng)該在destory和init方法中執(zhí)行過重的邏輯,前者會(huì)影響服務(wù)的可用性,在高并發(fā)下會(huì)阻塞太多數(shù)的請(qǐng)求。后者會(huì)影響配置刷新的時(shí)延性

看完上述內(nèi)容,你們對(duì)怎么進(jìn)行SpringCloud配置刷新機(jī)制的簡(jiǎn)單分析有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI