從RefreshScope實現原理看刷新配置失效問題

前言

在SpringIOC中,我們熟知的BeanScope有單例(singleton)、原型(prototype), Bean的Scope影響了Bean的創建方式,例如創建Scope=singleton的Bean時,IOC會保存實例在一個Map中,保證這個Bean在一個IOC上下文有且僅有一個實例。SpringCloud新增了一個refresh範圍的scope,同樣用了一種獨特的方式改變了Bean的創建方式,使得其可以通過外部化配置(.properties)的刷新,在應用不需要重啓的情況下熱加載新的外部化配置的值。

那麼這個scope是如何做到熱加載的呢?RefreshScope主要做了以下動作:

  • 單獨管理Bean生命週期
    • 創建Bean的時候如果是RefreshScope就緩存在一個專門管理的ScopeMap中,這樣就可以管理Scope是Refresh的Bean的生命週期了
  • 重新創建Bean
    • 外部化配置刷新之後,會觸發一個動作,這個動作將上面的ScopeMap中的Bean清空,這樣,這些Bean就會重新被IOC容器創建一次,使用最新的外部化配置的值注入類中,達到熱加載新值的效果

下面我們深入源碼,來驗證我們上述的講法。

1. 管理RefreshBean的生命週期

首先,若想要一個Bean可以自動熱加載配置值,這個Bean要被打上@RefreshScope註解,那麼就看看這個註解做了什麼:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {

	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

可以發現RefreshScope有一個屬性 proxyMode=ScopedProxyMode.TARGET_CLASS,這個是AOP動態代理用,之後會再來提這個

可以看出其是一個複合註解,被標註了 @Scope("refresh") ,其將Bean的Scope變爲refresh這個類型,在SpringBoot中BootStrap類上打上@SpringBootApplication註解(裏面是一個@ComponentScan),就會掃描包中的註解驅動Bean,掃描到打上RefreshScope註解的Bean的時候,就會將其的BeanDefinition的scope變爲refresh,這有什麼用呢?

創建一個Bean的時候,會去BeanFactory的doGetBean方法創建Bean,不同scope有不同的創建方式:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                          @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

  //....

  // Create bean instance.
  // 單例Bean的創建
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      //...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }

  // 原型Bean的創建
  else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
		// ...
    try {
      prototypeInstance = createBean(beanName, mbd, args);
    }
    //...
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
  }

  else {
    // 由上面的RefreshScope註解可以知道,這裏scopeName=refresh
    String scopeName = mbd.getScope();
    // 獲取Refresh的Scope對象
    final Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    }
    try {
      // 讓Scope對象去管理Bean
      Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
          return createBean(beanName, mbd, args);
        }
        finally {
          afterPrototypeCreation(beanName);
        }
      });
      bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }
    //...
  }
}
//...
}

//...
}

這裏可以看到幾件事情:

  • 單例和原型scope的Bean是硬編碼單獨處理的
  • 除了單例和原型Bean,其他Scope是由Scope對象處理的
  • 具體創建Bean的過程都是由IOC做的,只不過Bean的獲取是通過Scope對象

這裏scope.get獲取的Scope對象爲RefreshScope,可以看到,創建Bean還是由IOC來做(createBean方法),但是獲取Bean,都由RefreshScope對象的get方法去獲取,其get方法在父類GenericScope中實現:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 將Bean緩存下來
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 創建Bean,只會創建一次,後面直接返回創建好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

首先這裏將Bean包裝起來緩存下來

private final ScopeCache cache;
// 這裏進入上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
  return (BeanLifecycleWrapper) this.cache.put(name, value);
}

這裏的ScopeCache對象其實就是一個HashMap:

public class StandardScopeCache implements ScopeCache {

  private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();

  //...

  public Object get(String name) {
    return this.cache.get(name);
  }

  // 如果不存在,纔會put進去
  public Object put(String name, Object value) {
    // result若不等於null,表示緩存存在了,不會進行put操作
    Object result = this.cache.putIfAbsent(name, value);
    if (result != null) {
      // 直接返回舊對象
      return result;
    }
    // put成功,返回新對象
    return value;
  }
}

這裏就是將Bean包裝成一個對象,緩存在一個Map中,下次如果再GetBean,還是那個舊的BeanWrapper。回到Scope的get方法,接下來就是調用BeanWrapper的getBean方法:

// 實際Bean對象,緩存下來了
private Object bean;

public Object getBean() {
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

可以看出來,BeanWrapper中的bean變量即爲實際Bean,如果第一次get肯定爲空,就會調用BeanFactory的createBean方法創建Bean,創建出來之後就會一直保存下來。

由此可見,RefreshScope管理了Scope=Refresh的Bean的生命週期。

2. 重新創建RefreshBean

當配置中心刷新配置之後,有兩種方式可以動態刷新Bean的配置變量值,(SpringCloud-Bus還是Nacos差不多都是這麼實現的):

  • 向上下文發佈一個RefreshEvent事件
  • Http訪問/refresh這個EndPoint

不管是什麼方式,最終都會調用ContextRefresher這個類的refresh方法,那麼我們由此爲入口來分析一下,熱加載配置的原理:

// 這就是我們上面一直分析的Scope對象(實際上可以看作一個保存refreshBean的Map)
private RefreshScope scope;

public synchronized Set<String> refresh() {
  // 更新上下文中Environment外部化配置值
  Set<String> keys = refreshEnvironment();
  // 調用scope對象的refreshAll方法
  this.scope.refreshAll();
  return keys;
}

我們一般是使用@Value、@ConfigurationProperties去獲取配置變量值,其底層在IOC中則是通過上下文的Environment對象去獲取property值,然後依賴注入利用反射Set到Bean對象中去的。

那麼如果我們更新Environment裏的Property值,然後重新創建一次RefreshBean,再進行一次上述的依賴注入,是不是就能完成配置熱加載了呢?@Value的變量值就可以加載爲最新的了。

這裏說的刷新Environment對象並重新依賴注入則爲上述兩個方法做的事情:

  • Set keys = refreshEnvironment();
  • this.scope.refreshAll();

2.1 刷新Environment對象

下面簡單介紹一下如何刷新Environment裏的Property值

public synchronized Set<String> refreshEnvironment() {
  // 獲取刷新配置前的配置信息,對比用
  Map<String, Object> before = extract(
    this.context.getEnvironment().getPropertySources());
  // 刷新Environment
  addConfigFilesToEnvironment();
  // 這裏上下文的Environment已經是新的值了
  // 進行新舊對比,結果返回有變化的值
  Set<String> keys = changes(before,
                          extract(this.context.getEnvironment().getPropertySources())).keySet();
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

我們的重點在addConfigFilesToEnvironment方法,刷新Environment:

ConfigurableApplicationContext addConfigFilesToEnvironment() {
  ConfigurableApplicationContext capture = null;
  try {
    // 從上下文拿出Environment對象,copy一份
    StandardEnvironment environment = copyEnvironment(
      this.context.getEnvironment());
    // SpringBoot啓動類builder,準備新做一個Spring上下文啓動
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
      // banner和web都關閉,因爲只是想單純利用新的Spring上下文構造一個新的Environment
      .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
      // 傳入我們剛剛copy的Environment實例
      .environment(environment);
    // 啓動上下文
    capture = builder.run();
    // 這個時候,通過上下文SpringIOC的啓動,剛剛Environment對象就變成帶有最新配置值的Environment了
    // 獲取舊的外部化配置列表
    MutablePropertySources target = this.context.getEnvironment()
      .getPropertySources();
    String targetName = null;
    // 遍歷這個最新的Environment外部化配置列表
    for (PropertySource<?> source : environment.getPropertySources()) {
      String name = source.getName();
      if (target.contains(name)) {
        targetName = name;
      }
      // 某些配置源不做替換,讀者自行查看源碼
      // 一般的配置源都會進入if語句
      if (!this.standardSources.contains(name)) {
        if (target.contains(name)) {
          // 用新的配置替換舊的配置
          target.replace(name, source);
        }
        else {
          //....
        }
      }
    }
  }
  //....
}

可以看到,這裏歸根結底就是SpringBoot啓動上下文那種方法,新做了一個Spring上下文,因爲Spring啓動後會對上下文中的Environment進行初始化,獲取最新配置,所以這裏利用Spring的啓動,達到了獲取最新的Environment對象的目的。然後去替換舊的上下文中的Environment對象中的配置值即可。

2.2 重新創建RefreshBean

經過上述刷新Environment對象的動作,此時上下文中的配置值已經是最新的了。思路回到ContextRefresher的refresh方法,接下來會調用Scope對象的refreshAll方法:

public void refreshAll() {
  // 銷燬Bean
  super.destroy();
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  // 緩存清空
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  // ...
}

還記得上面的管理RefreshBean生命週期那一節關於緩存的討論嗎,cache變量是一個Map保存着RefreshBean實例,這裏直接就將Map清空了。

思路回到BeanFactory的doGetBean的流程中,從IOC容器中獲取RefreshBean是交給RefreshScope的get方法做的:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 由於剛剛清空了緩存Map,這裏就會put一個新的BeanLifecycleWrapper實例
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 在這裏是新的BeanLifecycleWrapper實例調用getBean方法
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}
public Object getBean() {
  // 由於是新的BeanLifecycleWrapper實例,這裏一定爲null
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        // 調用IOC容器的createBean,再創建一個Bean出來
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

可以看到,此時RefreshBean被IOC容器重新創建一個出來了,經過IOC的依賴注入功能,@Value的就是一個新的配置值了。到這裏熱加載功能實現基本結束。

根據以上分析,我們可以看出只要每次我們都從IOC容器中getBean,那麼拿到的RefreshBean一定是帶有最新配置值的Bean。

3. 動態刷新的應用

在我們正常使用@RefreshScope的時候,也沒有做一些getBean的操作,爲什麼也可以動態刷新呢?因爲Spring利用AOP動態代理了原先的Bean,在調用Bean的方法前,會攔截並從IOC容器中getBean,然後針對返回的新Bean做方法調用,這樣就達到了使用的配置值一直是最新的效果了。下面我們來分析分析這AOP動態代理的過程。

3.1 動態代理RefreshBean

在本人另一篇文章, SpringBoot自動裝配的魔法 中有講到,SpringBoot的註解驅動註冊Bean是由ConfigurationClassPostProcessor類來做的,其中@ComponentScan會掃描並註冊包下帶有@Componet註解的類爲Bean,達到一個註解驅動註冊Bean的效果。而掃描Bean並註冊爲BeanDefinition這一過程是由ClassPathBeanDefinitionScanner類的doScan方法去做的,我們先來看看掃描Bean的時候,對於RefreshScope的Bean有什麼特殊處理:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
  Assert.notEmpty(basePackages, "At least one base package must be specified");
  Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
  // 掃描basePackages所在的包下的所有的類,帶@Componet的類都會被註冊爲BeanDefinition
  for (String basePackage : basePackages) {
    	//...
      if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        // 這裏就是Scope的Bean的統一處理,是一個改變BeanDefinition的回調機會
        definitionHolder =
          AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);
        // 註冊BeanDefinition到IOC容器中
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

其中關鍵在於AnnotationConfigUtils的applyScopedProxyMode方法:

static BeanDefinitionHolder applyScopedProxyMode(
  ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
	// 獲取Scope的代理模式
  ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
  // 如果代理模式爲NO,就不進行代理了
  if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
    return definition;
  }
  boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
  // 進行代理
  return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}

回顧開頭分析的@RefreshScope註解,其proxyMode值爲ScopedProxyMode.TARGET_CLASS

public @interface RefreshScope {

  /**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
  ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

所以這裏被打上@RefreshScope的Bean類會進入接下來ScopedProxyCreator的createScopedProxy方法:

public static BeanDefinitionHolder createScopedProxy(
  BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) {

  return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass);
}
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
                                                     BeanDefinitionRegistry registry, boolean proxyTargetClass) {

  // ...

  // Create a scoped proxy definition for the original bean name,
  // "hiding" the target bean in an internal target definition.
  // 重點,這裏構造函數中將beanClass設置爲了ScopedProxyFactoryBean.class
  RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
  // targetDefinition是被代理的原生Bean
  proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
  
  // ...

  // Return the scoped proxy definition as primary bean definition
  // (potentially an inner bean).
  return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}

這裏一堆構建BeanDefinition的邏輯,先不看,只關注一件事,這裏將BeanDefinition的beanClass設置爲了ScopedProxyFactoryBean.class,而Scope的通用處理類GenericScope類是一個BeanDefinitionRegistryPostProcessor,其在postProcessBeanDefinitionRegistry回調方法中會針對剛剛那個beanClass爲ScopedProxyFactoryBean.class的BeanDefinition做一個特殊的處理:

// GenericScope.class
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
  throws BeansException {
  // 獲取所有BeanDefinition的名稱
  for (String name : registry.getBeanDefinitionNames()) {
    BeanDefinition definition = registry.getBeanDefinition(name);
    // 針對RootBeanDefinition這個BeanDefinition來做,這和上面的邏輯吻合
    if (definition instanceof RootBeanDefinition) {
      RootBeanDefinition root = (RootBeanDefinition) definition;
      // 判斷BeanClass == ScopedProxyFactoryBean.class
      if (root.getDecoratedDefinition() != null && root.hasBeanClass()
          && root.getBeanClass() == ScopedProxyFactoryBean.class) {
        if (getName().equals(root.getDecoratedDefinition().getBeanDefinition()
                             .getScope())) {
          // 將BeanClass換爲LockedScopedProxyFactoryBean
          root.setBeanClass(LockedScopedProxyFactoryBean.class);
          root.getConstructorArgumentValues().addGenericArgumentValue(this);
          // surprising that a scoped proxy bean definition is not already
          // marked as synthetic?
          root.setSynthetic(true);
        }
      }
    }
  }
}

到這裏可以知道,GenericScope將ScopeBean,變爲LockedScopedProxyFactoryBean這個類
在這裏插入圖片描述

然而這個類又是一個FactoryBean,由其父類ScopedProxyFactoryBean的getObject方法實現FactoryBean接口,我們知道,創建一個FactoryBean,其實最終會調用其getObject方法,這個方法的返回值纔是最終被創建出來的Bean實例,所以我們的重點就在getObject方法中:

public Object getObject() {
  if (this.proxy == null) {
    throw new FactoryBeanNotInitializedException();
  }
  return this.proxy;
}

似乎早就被動態代理好了,全局搜索proxy變量在哪裏被賦值。可以發現,ScopedProxyFactoryBean還是一個BeanFactoryAware,其setBeanFactory會在比較早的時機被回調:

public void setBeanFactory(BeanFactory beanFactory) {
  
  // ...
  
	// 這裏是一個比較關鍵的點,scopedTargetSource變量是一個SimpleBeanTargetSource
  // scopedTargetSource中保存了IOC容器
  this.scopedTargetSource.setBeanFactory(beanFactory);

  // 創建動態代理前,將動態代理的信息都保存到ProxyFactory中
  ProxyFactory pf = new ProxyFactory();
  pf.copyFrom(this);
  // 注意,這裏的TargetSource就是剛剛說的scopedTargetSource
  pf.setTargetSource(this.scopedTargetSource);
  
	// ...

  this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}

這裏先記住,scopedTargetSource是SimpleBeanTargetSource這個類就行,其保存了IOC容器。

接着調用pf的getProxy方法開始進行動態代理

下面就是AOP的一些邏輯了,不是本篇文章討論的重點,一筆帶過

private Callback[] getCallbacks(Class<?> rootClass) throws Exception {

  // ...

  // Choose an "aop" interceptor (used for AOP calls).
  // 這裏的MethodInterceptor實例是DynamicAdvisedInterceptor這個類
  Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

  // ...
  return callbacks;
}

我們知道,CGLib動態代理都會實現一個MethodInterceptor,被代理的類的每一個方法調用實質上都是在調用MethodInterceptor的intercept方法,那麼我們看看DynamicAdvisedInterceptor這個類的intercept方法:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  // 記得嗎,剛剛我們反覆強調的TargetSource
  TargetSource targetSource = this.advised.getTargetSource();
  try {
    // ...
    // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
    // 重點,這裏調用了targetSource的getTarget
    // target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
    target = targetSource.getTarget();
   // ...
    if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
      // ...
      // target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
      retVal = methodProxy.invoke(target, argsToUse);
    }
    else {
      // We need to create a method invocation...
      // target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
      retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    // target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
  }
  // ...
}

直接看重點,targetSource的getTarget()方法的返回值是被代理的類,那麼這個getTarget做了什麼邏輯呢?回顧上面來看我們知道這裏TargetSource的實現類是SimpleBeanTargetSource:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {

  @Override
  public Object getTarget() throws Exception {
    // 從IOC中getBean
    return getBeanFactory().getBean(getTargetBeanName());
  }
}

到這裏就已經揭曉了答案,原來被打上@RefreshScope的Bean都會被Spring做AOP動態代理,每次調用方法之前,都會去IOC中調用getBean方法獲取真正的原始Bean,而原始Bean又被存放在GenericScope對象中的Map裏,在refresh刷新配置的時候會清空緩存Map,在刷新配置的時候,調用類方法前去IOC獲取Bean,然後到GenericScope查看緩存,發現沒有這個Bean緩存就會重新從IOC容器創建一份Bean,依賴注入配置屬性值的時候注入的就是最新的值了,這樣就能達到動態刷新的作用。

3.2 Refresh動態刷新失效問題

大部分場景下RefreshBean動態刷新是不會失效的,但是筆者在使用WebFlux的時候,用到WebFilter過濾器時出現了失效的問題。這裏從現象看本質,如果你的應用Refresh也失效了,大概率也是跟我差不多問題,知道了原理,解決這個問題會變得十分簡單。

這裏重點要看你的Bean是如何被加載使用的,這裏看看常見的兩個方式是如何使用的:

  • Controller:在Controller中,@Value依賴注入一個外部化配置值,是否能得到動態刷新呢?
  • @Autowired:依賴注入任意一個Bean,注入的Bean中有@Value一個外部化配置值,是否能得到動態刷新呢?

3.2.1 Controller的使用

@RestController
@RefreshScope
public class TestController {
  
  @Value("${test.refresh}")
  private boolean test;
  
  @GetMapping("/get/refresh/test/value")
  public boolean test() {
    return test;
  }

}

涉及一些SpringMVC的知識,這裏默認讀者具有SpringMVC的相關知識

我們寫的Controller都會被DispatcherServlet進行路由,而這種路徑的路由@RequestMapping這種方式則是由RequestMappingHandlerMapping這個類去找到對應的controller方法,通過反射調用方法從而調用我們上面的test方法,那麼就肯定要拿到TestController這個類,這個操作是由其父類AbstractHandlerMethodMapping的getHandlerInternal方法做的:

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
  String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  this.mappingRegistry.acquireReadLock();
  try {
    // 根據請求拿到需要反射調用的方法
    HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
    // 關鍵:handlerMethod.createWithResolvedBean()獲取調用方法的類
    return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
  }
  finally {
    this.mappingRegistry.releaseReadLock();
  }
}

這裏直接看handlerMethod的關鍵方法createWithResolvedBean:

public HandlerMethod createWithResolvedBean() {
  Object handler = this.bean;
  if (this.bean instanceof String) {
    Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
    String beanName = (String) this.bean;
    // 會使用testController這個beanName獲取Bean
    handler = this.beanFactory.getBean(beanName);
  }
  return new HandlerMethod(this, handler);
}

如果Controller被打上@RefreshScope註解,這裏getBean的返回值就是CGLib動態代理對象,根據以上流程走自然可以達到動態刷新的效果

3.2.2 @Autowired的使用

@Component
public class Test {

  @Autowired
  private PropertiesSource propertiesSource;

  public boolean test(){
    propertiesSource.getValue();
  }
}

@Service
@RefreshScope
public class PropertiesSource {

  @Value("${test.refresh}")
  private boolean test;

  public boolean getValue(){
    return test;
  }
}

這樣使用也可以達到動態刷新的作用,Test類依賴注入PropertiesSource對象時會注入CGLib動態代理對象。

3.2.3 失效問題

從上面兩個使用場景來看,RefreshBean被正確加載爲CGLib動態代理對象就能正常動態刷新,那麼什麼時候會不正常加載呢?這裏舉一個我在3.2小節開頭舉的例子,WebFilter。

在WebFlux中,組裝WebFilter是在WebHttpHandlerBuilder類的applicationContext方法中:

public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
  WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder(
    context.getBean(WEB_HANDLER_BEAN_NAME, WebHandler.class), context);

  // 獲取上下文中 Class爲WebFilter的所有Bean
  List<WebFilter> webFilters = context
    .getBeanProvider(WebFilter.class)
    .orderedStream()
    .collect(Collectors.toList());
  builder.filters(filters -> filters.addAll(webFilters));

  //...

  return builder;
}

這裏組裝加載WebFilter的時候會獲取IOC中WebFilter類型的Bean,這裏我Debug給大家看看這個List列表
在這裏插入圖片描述

怎麼會有兩個重複的呢?

思路回到第三節討論的掃描並註冊Bean入口doScan中的createScopedProxy方法,其中針對特定Scope做一個BeanDefinition的修改:

public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
                                                     BeanDefinitionRegistry registry, boolean proxyTargetClass) {

  // 原始Bean的名稱
  String originalBeanName = definition.getBeanName();
  BeanDefinition targetDefinition = definition.getBeanDefinition();
  // 加了前綴的Bean名稱
  String targetBeanName = getTargetBeanName(originalBeanName);
	
  // ...

  // Register the target bean as separate bean in the factory.
  // 這裏會註冊原始Bean,以targetBeanName爲beanName
  registry.registerBeanDefinition(targetBeanName, targetDefinition);

  // Return the scoped proxy definition as primary bean definition
  // (potentially an inner bean).
  // 註冊會生成代理Bean的FactoryBean
  return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}

可以看到,其實這裏依然會將原生Bean註冊到IOC容器中,只不過beanName是被加了前綴的,這裏我debug看看加了什麼前綴(假設我的beanName是gatewayPropertiesSource)
在這裏插入圖片描述

可以看到,這裏註冊了兩個Bean,一個是原始名稱的FactoryBean,用於生成CGLib的動態代理Bean,一個是被加上 scopedTarget. 前綴的原生Bean,爲什麼要註冊兩個Bean呢?因爲後面一個Bean是被Scope對象的Map緩存所管理的,後面那個Bean則是我們的Bean本尊,動態代理Bean調用方法要獲取的Bean就是這個對象,從哪裏可以看出來呢?回顧3.1節的最後,getObject關鍵方法:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {

  @Override
  public Object getTarget() throws Exception {
    // getTargetBeanName,這裏是加了前綴的名稱
    return getBeanFactory().getBean(getTargetBeanName());
  }
}

在這裏插入圖片描述
到這裏我們可以知道,其實這個加了前綴的Bean纔是我們最終使用的Bean,配置動態刷新的時候變的不是CGLib動態代理的Bean,而是裏面加了前綴的Bean,這個Bean被Scope對象的Map所管理,當配置刷新時Map清空,動態代理的Bean調用方法之前先去IOC中獲取加了前綴的Bean,這個加了前綴的Bean又是refresh的scope,又會去Scope中的Map尋找,發現找不到,就又在IOC容器中創建一個新的加了前綴的Bean,供後續使用,達到動態刷新的效果。

回到我們3.2.3節開頭講的失效問題,這裏組裝WebFilter的時候由於會獲取類型爲WebFilter的所有Bean,所以會把動態代理的Bean和前綴Bean全組裝起來,在我們Web應用裏就會使用到前綴Bean本尊,由於這個Bean不像那個動態代理Bean一樣,每一次方法調用都會去IOC容器getBean,所以這個Bean是一成不變的,就算配置刷新,這個Bean實例還是那個舊的值,導致失效問題。

3.3 更優雅的動態刷新

我們知道,如果是@RefreshScope的話,每次都要去IOC獲取一下Bean,感覺調用鏈路比較長,那麼是否能不打@RefreshScope也能動態刷新屬性值呢?這裏還有一種方式達到動態刷新的目的,那就是@ConfigurationProperties註解,具體用法不過多贅述,這裏詳細講講ConfigurationProperties是如何做到動態刷新的。

思路回到我們第二節講的,ContextRefresher這個類,當配置刷新的時候,這個類的refresh方法會被調用:

public synchronized Set<String> refresh() {
  // 刷新環境
  Set<String> keys = refreshEnvironment();
  this.scope.refreshAll();
  return keys;
}

關鍵在refreshEnvironment方法:

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();
  // 發佈一個EnvironmentChangeEvent事件
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

熟悉的方法,這裏我們的關注點在於其最後會發佈一個EnvironmentChangeEvent事件,這個事件是由ConfigurationPropertiesRebinder這個類來監聽的,看名字就知道,這個類負責重新設置ConfigurationProperties值。重點關注這個類的onApplicationEvent方法:

public void onApplicationEvent(EnvironmentChangeEvent event) {
  if (this.applicationContext.equals(event.getSource())
      // Backwards compatible
      || event.getKeys().equals(event.getSource())) {
    // 重新設置值
    rebind();
  }
}

在這個方法消費EnvironmentChangeEvent這個事件,進入rebind方法:

public void rebind() {
  this.errors.clear();
  // beans保存着所有ConfigurationProperties實例
  for (String name : this.beans.getBeanNames()) {
    rebind(name);
  }
}

beans實則是一個Map,我們直接debug看看這個Map保存了什麼
在這裏插入圖片描述

可以看到,這裏保存了54個ConfigurationProperties類實例,其中我們的ConfigurationProperties類就是上面的greyProperties
在這裏插入圖片描述

繼續往下走,看看rebind對這個實例做了什麼:

public boolean rebind(String name) {
  //...
  if (this.applicationContext != null) {
    try {
      // 從上下文中獲取這個Bean
      Object bean = this.applicationContext.getBean(name);
      if (AopUtils.isAopProxy(bean)) {
        bean = ProxyUtils.getTargetObject(bean);
      }
      if (bean != null) {
        // TODO: determine a more general approach to fix this.
        // see https://github.com/spring-cloud/spring-cloud-commons/issues/571
        if (getNeverRefreshable().contains(bean.getClass().getName())) {
          return false; // ignore
        }
        // 調用這個Bean的destroy流程
        this.applicationContext.getAutowireCapableBeanFactory()
          .destroyBean(bean);
        // 調用這個Bean的初始化流程
        this.applicationContext.getAutowireCapableBeanFactory()
          .initializeBean(bean, name);
        return true;
      }
    }
    //...
  }
  return false;
}

重點在於調用上下文的initializeBean方法初始化Bean:

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
  
  //...
  
  // 調用aware回調
  invokeAwareMethods(beanName, bean);

  Object wrappedBean = bean;
  if (mbd == null || !mbd.isSynthetic()) {
    // 重點,調用BeanPostProcessors回調
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
  }

  //...

  return wrappedBean;
}

其中的重點在於applyBeanPostProcessorsBeforeInitialization方法:

public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
  throws BeansException {

  Object result = existingBean;
  // BeanPostProcessor的回調
  for (BeanPostProcessor processor : getBeanPostProcessors()) {
    Object current = processor.postProcessBeforeInitialization(result, beanName);
    if (current == null) {
      return result;
    }
    result = current;
  }
  return result;
}

在BeanPostProcessor的回調中,會有一個ConfigurationPropertiesBindingPostProcessor這個BeanPostProcessor在postProcessBeforeInitialization方法中利用Binder對象,將最新的配置值反射調用set方法注入到ConfigurationProperties這樣的Bean裏,達到動態刷新的效果。這裏限於篇幅問題,就不繼續深入研究了,感興趣的小夥伴可以跟着思路去看看源碼。

這裏我們看到,ConfigurationProperties這種方式不需要一直去IOC容器裏getBean,也不需要動態代理,沒有以上那樣的失效問題,從始至終就只有一個Bean的單例,這種方式個人感覺比較優雅,建議最好使用這種方式去裝配外部化配置信息。

但有些場景又需要@RefreshScope這種方式,因爲@Value可以解析一些複雜的表達式,比如設置默認值,或者把配置值處理成一個List比較方便:
在這裏插入圖片描述

總之,@Value這種比較靈活,但如果配置值多了,比如外部化配置有幾十個,ConfigurationProperties又比較好了,只需要寫一個前綴就可以裝配前綴下的所有外部化配置。兩者各有好處,但建議最好優先考慮一下ConfigurationProperties,使用@RefreshScope也可以,知道原理也就能避免一些失效問題了。

4. 總結

我們總結一下,當不正常獲取RefreshScope的Bean時,動態刷新會失效。例如直接從IOC容器中獲取所有某類型的Bean,把beanName帶前綴的那個Bean直接拿來使用了。到此,我們可以知道只有使用被動態代理增強過的那個Bean,纔可以有動態刷新的效果。

3.2.3節這種WebFilter這種情況告訴我們,不要在這種方式加載使用Bean的地方用@RefreshScope,會導致Filter鏈重複,你會得到兩個一摸一樣的Filter並且還不知道,潛在影響性能。解決方法就是把要動態刷新的配置值抽出來變成一個類,在這個類上打上@RefreshScope註解,然後在WebFilter中使用@Autowired這種方式依賴注入剛剛那個配置值的專門類,很好的解決了失效問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章