On the failure of refresh configuration from the implementation principle of RefreshScope

Posted by sajy2k on Wed, 02 Feb 2022 07:21:22 +0100

preface

In spring IOC, the well-known BeanScope includes singleton and prototype. The scope of the Bean affects the creation method of the Bean. For example, when creating a Bean with Scope=singleton, the IOC will save the instance in a Map to ensure that the Bean has only one instance in an IOC context. Spring cloud adds a scope of refresh range, which also changes the creation method of Bean in a unique way, so that it can hot load the value of the new externalized configuration without restarting the application through the refresh of externalized configuration (. properties).

So how is this scope hot loaded? RefreshScope mainly does the following actions:

  • Manage Bean lifecycle separately
    • When creating a Bean, if it is RefreshScope, it will be cached in a specially managed ScopeMap, so that the life cycle of the Bean whose Scope is Refresh can be managed
  • Recreate Bean
    • After the externalized configuration is refreshed, an action will be triggered to empty the beans in the ScopeMap above. In this way, these beans will be re created by the IOC container and injected into the class with the latest externalized configured values to achieve the effect of hot loading new values

Let's go deep into the source code to verify our above statement.

1. Manage the life cycle of RefreshBean

First of all, if you want a Bean to automatically hot load configuration values, the Bean should be marked with @ RefreshScope annotation. Let's see what this annotation does:

@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;

}

It can be found that RefreshScope has a property proxymode = scopedproxymode TARGET_ Class, this is for AOP dynamic proxy. I'll talk about this later

It can be seen that it is a composite annotation marked with @ Scope("refresh"), which changes the scope of the Bean to the type of refresh. If you mark @ SpringBootApplication annotation on the BootStrap class in SpringBoot (which is a @ ComponentScan), you will scan the annotation driven beans in the package. When you scan the beans marked with RefreshScope annotation, It will change the scope of its BeanDefinition to refresh. What's the use of this?

When creating a Bean, you will go to the doGetBean method of BeanFactory to create a Bean. Different scope s have different creation methods:

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

  //....

  // Create bean instance.
  // Creation of singleton Bean
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      //...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }

  // Creation of prototype 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 {
    // As can be seen from the RefreshScope annotation above, scopeName=refresh here
    String scopeName = mbd.getScope();
    // Get the Scope object of Refresh
    final Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    }
    try {
      // Let the Scope object manage beans
      Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
          return createBean(beanName, mbd, args);
        }
        finally {
          afterPrototypeCreation(beanName);
        }
      });
      bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }
    //...
  }
}
//...
}

//...
}

Here are a few things you can see:

  • The beans of singleton and prototype scope are hard coded and processed separately
  • In addition to singleton and prototype beans, other scopes are handled by Scope objects
  • The specific process of creating a Bean is done by IOC, but the Bean is obtained through the Scope object

Here is Scope The Scope object obtained by get is RefreshScope. It can be seen that the IOC still creates the Bean (createBean method), but the get method of RefreshScope object obtains the Bean, and its get method is implemented in the parent GenericScope:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // Cache Bean
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // When you create a Bean, you will only create it once, and then directly return the created Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

First, wrap the Bean and cache it

private final ScopeCache cache;
// Here go to beanllifecycle wrapper value = this cache. put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
  return (BeanLifecycleWrapper) this.cache.put(name, value);
}

The ScopeCache object here is actually a 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);
  }

  // If it doesn't exist, it will be put in
  public Object put(String name, Object value) {
    // If result is not equal to null, it indicates that the cache exists and will not be put
    Object result = this.cache.putIfAbsent(name, value);
    if (result != null) {
      // Return old object directly
      return result;
    }
    // put succeeded, and a new object is returned
    return value;
  }
}
  • 1

Here is to wrap the Bean into an object and cache it in a Map. If you get the Bean again next time, it will be the old BeanWrapper. Back to the get method of Scope, the next step is to call the getBean method of BeanWrapper:

// The actual Bean object is cached
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;
}

It can be seen that the bean variable in the BeanWrapper is the actual bean. If the first get is definitely empty, the BeanFactory's createBean method will be called to create the bean, which will be saved after creation.

It can be seen that RefreshScope manages the life cycle of beans with Scope=Refresh.

2. Recreate RefreshBean

After the configuration center refreshes the configuration, there are two ways to dynamically refresh the configuration variable value of Bean (spring cloud bus or Nacos are almost implemented in this way):

  • Publish a RefreshEvent event event to the context
  • Http access / refresh this EndPoint

No matter what method is used, the refresh method of ContextRefresher class will eventually be called. Let's analyze the principle of hot load configuration for the entry:

We generally use @ Value and @ ConfigurationProperties to obtain the Value of configuration variables. In IOC, the underlying is to obtain the property Value through the Environment object of the context, and then use dependency injection to reflect Set into the Bean object.

Then, if we update the Property Value in the Environment, then re create the RefreshBean, and then conduct the above dependency injection, will we be able to complete the configuration hot load@ The variable Value of Value can be loaded as the latest.

Refreshing the Environment object and redependency injection mentioned here is what the above two methods do:

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

2.1 refresh Environment object

Here is a brief introduction to how to refresh the Property value in the Environment

public synchronized Set<String> refreshEnvironment() {
  // Get the configuration information before refreshing the configuration for comparison
  Map<String, Object> before = extract(
    this.context.getEnvironment().getPropertySources());
  // Refresh Environment
  addConfigFilesToEnvironment();
  // The Environment of the context here is already the new value
  // Compare the old with the new, and return the changed value
  Set<String> keys = changes(before,
                          extract(this.context.getEnvironment().getPropertySources())).keySet();
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

We focus on the addConfigFilesToEnvironment method to refresh the Environment:

ConfigurableApplicationContext addConfigFilesToEnvironment() {
  ConfigurableApplicationContext capture = null;
  try {
    // Take out the Environment object from the context and copy it
    StandardEnvironment environment = copyEnvironment(
      this.context.getEnvironment());
    // SpringBoot starts the class builder and is ready to start a new Spring context
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
      // Both banner and web are closed because they just want to construct a new Environment simply using the new Spring context
      .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
      // Pass in the Environment instance we just copied
      .environment(environment);
    // Start context
    capture = builder.run();
    // At this time, the Environment object will become the Environment with the latest configuration value just after the context SpringIOC is started
    // Get old externalized configuration list
    MutablePropertySources target = this.context.getEnvironment()
      .getPropertySources();
    String targetName = null;
    // Traverse the latest list of Environment externalized configurations
    for (PropertySource<?> source : environment.getPropertySources()) {
      String name = source.getName();
      if (target.contains(name)) {
        targetName = name;
      }
      // Some configuration sources are not replaced. Readers can check the source code by themselves
      // The general configuration source will enter the if statement
      if (!this.standardSources.contains(name)) {
        if (target.contains(name)) {
          // Replace the old configuration with the new one
          target.replace(name, source);
        }
        else {
          //....
        }
      }
    }
  }
  //....
}

It can be seen that in the final analysis, this is the method of SpringBoot startup context. A new Spring context is created, because after Spring startup, the Environment in the context will be initialized to obtain the latest configuration. Therefore, the purpose of obtaining the latest Environment object is achieved by using Spring startup. Then replace the configuration value in the Environment object in the old context.

2.2 recreate RefreshBean

After the above action of refreshing the Environment object, the configuration value in the context is up-to-date. The train of thought returns to the refresh method of ContextRefresher. Next, the refreshAll method of Scope object will be called:

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

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

Remember the discussion on cache in the section on managing the RefreshBean life cycle above? The cache variable is a Map that holds the RefreshBean instance. Here, the Map is cleared directly.

The idea goes back to the process of doGetBean of BeanFactory. Getting RefreshBean from IOC container is done by the get method of RefreshScope:

public Object getBean() {
  // Since it is a new instance of beanllifecycle wrapper, it must be null here
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        // Create another IOC Bean
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

It can be seen that the RefreshBean is recreated by the IOC container. Through the dependency injection function of IOC, @ Value is a new configuration Value. Here, the hot loading function is basically completed.

According to the above analysis, we can see that as long as we get the Bean from the IOC container every time, the RefreshBean we get must be a Bean with the latest configuration value.

3. Application of dynamic refresh

When we use @ RefreshScope normally, we don't do some getBean operations. Why can we refresh dynamically? Because Spring uses AOP to dynamically proxy the original Bean. Before calling the Bean's method, it will intercept and get the Bean from the IOC container, and then make method calls for the returned new Bean. In this way, the configuration value used has always been the latest effect. Let's analyze the process of AOP dynamic agent.

3.1 dynamic agent RefreshBean

In my other article, The magic of SpringBoot auto assembly As mentioned in, the annotation driven registration Bean of SpringBoot is made by the ConfigurationClassPostProcessor class, in which @ ComponentScan will scan and register the class with @ Componet annotation under the package as Bean, so as to achieve the effect of annotation driven registration Bean. The process of scanning beans and registering them as BeanDefinition is done by the doScan method of ClassPathBeanDefinitionScanner class. Let's first look at the special treatment of RefreshScope beans when scanning beans:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
  Assert.notEmpty(basePackages, "At least one base package must be specified");
  Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
  // Scan all classes under the package where basePackages are located. Classes with @ Componet will be registered as BeanDefinition
  for (String basePackage : basePackages) {
    	//...
      if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        // This is the unified processing of beans in Scope. It is a callback opportunity to change the BeanDefinition
        definitionHolder =
          AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);
        // Register BeanDefinition into IOC container
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

The key lies in the applyscope dproxymode method of AnnotationConfigUtils:

static BeanDefinitionHolder applyScopedProxyMode(
  ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
	// Get proxy mode of Scope
  ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
  // If the agent mode is NO, the agent will not be performed
  if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
    return definition;
  }
  boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
  // Acting
  return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}

Review the @ RefreshScope annotation analyzed at the beginning, and its proxyMode value is scopedproxymode TARGET_ CLASS

public @interface RefreshScope {

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

Therefore, the Bean class marked @ RefreshScope here will enter the createScopedProxy method of ScopedProxyCreator:

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.
  // The key point is that beanClass is set as scopedproxyfactorybean in the constructor class
  RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
  // targetDefinition is the proxy's native 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());
}

Here is a pile of logic to build BeanDefinition. Let's not look at it first, but just focus on one thing. Here, set beanClass of BeanDefinition to scopedproxyfactorybean Class, and the generic processing class GenericScope class of Scope is a BeanDefinitionRegistryPostProcessor, which will be scopedproxyfactorybean for the beanClass just now in the postProcessBeanDefinitionRegistry callback method Class's BeanDefinition does a special processing:

// GenericScope.class
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
  throws BeansException {
  // Gets the names of all beandefinitions
  for (String name : registry.getBeanDefinitionNames()) {
    BeanDefinition definition = registry.getBeanDefinition(name);
    // This is done for the RootBeanDefinition, which is consistent with the above logic
    if (definition instanceof RootBeanDefinition) {
      RootBeanDefinition root = (RootBeanDefinition) definition;
      // Judge beanclass = = scopedproxyfactorybean class
      if (root.getDecoratedDefinition() != null && root.hasBeanClass()
          && root.getBeanClass() == ScopedProxyFactoryBean.class) {
        if (getName().equals(root.getDecoratedDefinition().getBeanDefinition()
                             .getScope())) {
          // Replace BeanClass with 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);
        }
      }
    }
  }
}

From here, we can see that GenericScope changes ScopeBean to the class LockedScopedProxyFactoryBean

However, this class is also a FactoryBean. The getObject method of its parent class ScopedProxyFactoryBean implements the FactoryBean interface. We know that creating a FactoryBean will eventually call its getObject method, and the return value of this method is the Bean instance finally created. Therefore, our focus is on the getObject method:

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

It seems that it has long been dynamic proxy. The global search proxy variable is assigned where. It can be found that ScopedProxyFactoryBean is also a BeanFactoryAware, and its setBeanFactory will be recalled at an earlier time:

public void setBeanFactory(BeanFactory beanFactory) {
  
  // ...
  
	// Here is a key point. The scopedTargetSource variable is a SimpleBeanTargetSource
  // The IOC container is saved in scopedTargetSource
  this.scopedTargetSource.setBeanFactory(beanFactory);

  // Before creating a dynamic proxy, save the information of the dynamic proxy to ProxyFactory
  ProxyFactory pf = new ProxyFactory();
  pf.copyFrom(this);
  // Note that the TargetSource here is the scopedTargetSource just mentioned
  pf.setTargetSource(this.scopedTargetSource);
  
	// ...

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

Remember that scopedTargetSource is the SimpleBeanTargetSource class, which saves the IOC container.

Then call the getProxy method of pf to start dynamic proxy

The following is some logic of AOP, which is not the focus of this article. I've just mentioned it

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

  // ...

  // Choose an "aop" interceptor (used for AOP calls).
  // The MethodInterceptor instance here is the dynamic advised interceptor class
  Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

  // ...
  return callbacks;
}

As we know, CGLib dynamic agent will implement a MethodInterceptor, and every method call of the proxy class is essentially calling the intercept method of MethodInterceptor. Let's take a look at the intercept method of dynamicadisaidedinterceptor class:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  // Remember, the TargetSource we just repeated
  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...
    // The key point is that the getTarget of targetSource is called here
    // The target variable is the class being proxied. When the actual method is called, the corresponding method is called by reflection
    target = targetSource.getTarget();
   // ...
    if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
      // ...
      // The target variable is the class being proxied. When the actual method is called, the corresponding method is called by reflection
      retVal = methodProxy.invoke(target, argsToUse);
    }
    else {
      // We need to create a method invocation...
      // The target variable is the class being proxied. When the actual method is called, the corresponding method is called by reflection
      retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    // The target variable is the class being proxied. When the actual method is called, the corresponding method is called by reflection
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
  }
  // ...
}

Let's look at the key point directly. The return value of the getTarget() method of Targetsource is the class being proxied. What logic does this getTarget do? Looking back, we know that the implementation class of Targetsource here is SimpleBeanTargetSource:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {

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

Here we have already announced the answer. The original Bean, which is called @RefreshScope, will be Spring AOP dynamic proxy. Before calling each method, it will call the getBean method in IOC to get the real original Bean, and the original Bean will be stored in the Map of GenericScope object. When the refresh refresh the configuration, it will empty the cache. Before calling the class method, go to IOC to get the bean, and then go to genericscope to check the cache. If it is found that there is no bean cache, a bean will be re created from the IOC container. When the dependency injection configuration attribute value is injected, the latest value will be injected, so as to achieve the role of dynamic refresh.

3.2 Refresh dynamic refresh failure

In most scenarios, the RefreshBean dynamic Refresh will not fail, but the author has the problem of failure when using WebFilter when using WebFlux. Look at the essence from the phenomenon here. If your application Refresh also fails, the probability is similar to me. Knowing the principle, it will be very simple to solve this problem.

The key point here is to see how your Bean is loaded and used. Here are two common ways to use it:

  • Controller: in the controller, @ Value dependency injects an externalized configuration Value. Can it be refreshed dynamically?
  • @Autowired: depend on injecting any Bean. There is an externalized configuration Value @ Value in the injected Bean. Can it be refreshed dynamically?

3.2.1 use of controller

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

}

Involving some knowledge of spring MVC, the default readers here have relevant knowledge of spring MVC

The controller we write will be routed by DispatcherServlet, and the route of this path @ RequestMapping. In this way, the RequestMappingHandlerMapping class will find the corresponding controller method and call the test method above through reflection. Then we must get the TestController class, This operation is performed by the getHandlerInternal method of its parent class AbstractHandlerMethodMapping:

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
  String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  this.mappingRegistry.acquireReadLock();
  try {
    // Get the method that needs to be called by reflection according to the request
    HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
    // Key: handlermethod Createwithresolvedbean() gets the class that calls the method
    return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
  }
  finally {
    this.mappingRegistry.releaseReadLock();
  }
}

Here, let's take a direct look at the key method createWithResolvedBean of handlerMethod:

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;
    // The Bean will be obtained by using the beanName of testController
    handler = this.beanFactory.getBean(beanName);
  }
  return new HandlerMethod(this, handler);
}

If the Controller is annotated with @ RefreshScope, the return value of getBean here is the CGLib dynamic proxy object. According to the above process, the effect of dynamic refresh can be achieved naturally

3.2.2 use of @ 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;
  }
}

In this way, dynamic refresh can also be achieved. When the Test class dependency is injected into the PropertiesSource object, the CGLib dynamic proxy object will be injected.

3.2.3 failure problems

From the above two usage scenarios, RefreshBean can be dynamically refreshed normally when it is correctly loaded as a CGLib dynamic proxy object. When will it not be loaded normally? Here is an example I gave at the beginning of Section 3.2, WebFilter.

In WebFlux, WebFilter is assembled in the applicationContext method of WebHttpHandlerBuilder class:

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

  // Get all beans whose Class is WebFilter in the context
  List<WebFilter> webFilters = context
    .getBeanProvider(WebFilter.class)
    .orderedStream()
    .collect(Collectors.toList());
  builder.filters(filters -> filters.addAll(webFilters));

  //...

  return builder;
}

When the WebFilter is assembled and loaded here, the Bean of WebFilter type in IOC will be obtained. Here I Debug to show you the List
 

How can there be two repetitions?

The idea goes back to the method of scanning and registering the createScopedProxy in the Bean entry doScan discussed in Section 3, in which a modification of the BeanDefinition is made for a specific Scope:

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

  // The name of the original Bean
  String originalBeanName = definition.getBeanName();
  BeanDefinition targetDefinition = definition.getBeanDefinition();
  // Prefixed Bean name
  String targetBeanName = getTargetBeanName(originalBeanName);
	
  // ...

  // Register the target bean as separate bean in the factory.
  // The original Bean will be registered here, with targetBeanName as beanName
  registry.registerBeanDefinition(targetBeanName, targetDefinition);

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

You can see that the native Bean will still be registered in the IOC container, but the beanName is prefixed. Here I debug to see what prefix is added (assuming my beanName is gatewayPropertiesSource)

As you can see, two beans are registered here. One is the FactoryBean with the original name, which is used to generate the dynamic proxy Bean of CGLib, and the other is added with scopedTarget Why register two native beans with prefix? Because the latter Bean is managed by the Map cache of the Scope object, and the latter Bean is our own Bean. The Bean to be obtained by the calling method of the dynamic proxy Bean is this object. Where can we see it? At the end of Section 3.1, the key methods of getObject are as follows:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {

  @Override
  public Object getTarget() throws Exception {
    // getTargetBeanName, here is the prefixed name
    return getBeanFactory().getBean(getTargetBeanName());
  }
}


From here, we can know that the prefixed Bean is the one we finally use. When configuring dynamic refresh, it is not the CGLib dynamic proxy Bean, but the prefixed Bean. This Bean is managed by the Map of the scope object. When configuring refresh, the Map is cleared. Before calling the method, the dynamic proxy Bean goes to the IOC to obtain the prefixed Bean, The prefixed Bean is also the scope of refresh, and will go to the Map in the scope to find it. If it cannot be found, it will create a new prefixed Bean in the IOC container for subsequent use to achieve the effect of dynamic refresh.

Returning to the failure problem mentioned at the beginning of section 3.2.3, when assembling WebFilter here, because all beans of type WebFilter will be obtained, the dynamic proxy Bean and prefix Bean will be fully assembled, and the prefix Bean will be used in our Web application. Because this Bean is different from that dynamic proxy Bean, every method call will go to the IOC container getBean, Therefore, the Bean remains unchanged. Even if the configuration is refreshed, the Bean instance is still the old value, resulting in failure.

3.3 more elegant dynamic refresh

We know that if it's @ RefreshScope, you have to go to the IOC to get the Bean every time. It feels that the call link is relatively long. Can you dynamically refresh the attribute value without hitting @ RefreshScope? There is another way to achieve the purpose of dynamic refresh, that is, the @ ConfigurationProperties annotation. The specific usage will not be repeated. Here is a detailed description of how ConfigurationProperties achieve dynamic refresh.

The idea goes back to the class ContextRefresher in Section 2. When refreshing is configured, the refresh method of this class will be called:

public synchronized Set<String> refresh() {
  // Refresh environment
  Set<String> keys = refreshEnvironment();
  this.scope.refreshAll();
  return keys;
}

The key lies in the refreshEnvironment method:

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();
  // Publish an EnvironmentChangeEvent event event
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

Familiar methods. Our focus here is that it will publish an EnvironmentChangeEvent event at the end. This event is monitored by the ConfigurationPropertiesRebinder class. According to the name, this class is responsible for resetting the ConfigurationProperties value. Focus on the onApplicationEvent method of this class:

public void onApplicationEvent(EnvironmentChangeEvent event) {
  if (this.applicationContext.equals(event.getSource())
      // Backwards compatible
      || event.getKeys().equals(event.getSource())) {
    // Reset value
    rebind();
  }
}

Consume the EnvironmentChangeEvent event event in this method and enter the rebind method:

public void rebind() {
  this.errors.clear();
  // beans holds all ConfigurationProperties instances
  for (String name : this.beans.getBeanNames()) {
    rebind(name);
  }
}

beans is actually a Map. Let's debug directly to see what the Map saves

As you can see, there are 54 instances of ConfigurationProperties saved here, of which our ConfigurationProperties class is the above greyProperties

Go on to see what rebind has done to this instance:

public boolean rebind(String name) {
  //...
  if (this.applicationContext != null) {
    try {
      // Get the Bean from the context
      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
        }
        // Call the destroy process of this Bean
        this.applicationContext.getAutowireCapableBeanFactory()
          .destroyBean(bean);
        // Call the initialization process of this Bean
        this.applicationContext.getAutowireCapableBeanFactory()
          .initializeBean(bean, name);
        return true;
      }
    }
    //...
  }
  return false;
}

The key point is to call the initializeBean method of the context to initialize the Bean:

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

  Object wrappedBean = bean;
  if (mbd == null || !mbd.isSynthetic()) {
    // Focus on calling the BeanPostProcessors callback
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
  }

  //...

  return wrappedBean;
}

The focus is on the applyBeanPostProcessorsBeforeInitialization method:

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

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

In the callback of BeanPostProcessor, there will be a ConfigurationPropertiesBindingPostProcessor. This BeanPostProcessor uses the Binder object in the postProcessBeforeInitialization method to inject the latest configuration value reflection call set method into beans such as ConfigurationProperties to achieve the effect of dynamic refresh. Here is limited to space, so we won't continue to study it in depth. Interested partners can follow their ideas to see the source code.

Here we can see that ConfigurationProperties does not need to go all the way to getBean in IOC container, nor does it need dynamic proxy. There is no failure problem like the above. There is only one single instance of Bean from beginning to end. This method feels elegant. It is recommended to use this method to assemble externalized configuration information.

However, some scenarios need @ RefreshScope, because @ Value can parse some complex expressions, such as setting default values or processing configuration values into a List, which is more convenient:

In short, @ Value is more flexible, but if there are more configuration values, such as dozens of externalized configurations, and ConfigurationProperties are better, you can assemble all externalized configurations under the prefix by writing only one prefix. Both have their own advantages, but it is recommended to give priority to ConfigurationProperties. You can also use @ RefreshScope. Knowing the principle can avoid some failure problems.

4. Summary

To sum up, when the Bean of RefreshScope is obtained abnormally, the dynamic refresh will fail. For example, directly obtain all beans of a certain type from the IOC container, and directly use the Bean with the prefix beanName. At this point, we can know that only the Bean enhanced by dynamic proxy can have the effect of dynamic refresh.

The WebFilter in section 3.2.3 tells us not to use @ RefreshScope where beans are loaded in this way, which will lead to duplicate Filter chains. You will get two identical filters that you don't know yet, potentially affecting performance. The solution is to extract the configuration value to be dynamically refreshed into a class, mark @ RefreshScope annotation on this class, and then use @ Autowired in WebFilter to rely on the special class that injects the configuration value just now, which solves the failure problem well.

 

Topics: Spring Spring Boot Spring Cloud