程序员L札记

V1

2022/02/02阅读:142主题:橙心

Spring Cloud 配置动态刷新原理

Spring Cloud 配置动态刷新原理

spring cloud 版本2020.0.0

前言

距离上一篇《Spring Cloud Config Client源码分析之加载外部化配置》已经有一段时间没有更新了,这一篇应该是2022年春节前最后一篇了。本篇我们一起走进spring cloud配置动态刷新的原理。在上一篇的结尾,提到在实际的生产环境spring cloud config会结合spring cloud bus一起使用。但是本篇呢,我们只探究spring cloud抽象出的ContextRefresher接口规范,以手动的方式,通过/actuator/refresh的接口api来刷新配置。

基本使用

在开始之前呢,先简单介绍一下基本用法,以方便还没有用过的同学学习和理解。

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

配置bootstrap.yml

management:
  endpoint:
    refresh:
      enabled: true #默认true
  endpoints:
    web:
      exposure:
        include: '*' #默认情况下只对外暴露了health和info端点

spring:
  application:
    name: ms-client-a
  cloud:
    config:
#      discovery:
#        enabled: true
#        service-id: ms-config-server
      uri: http://localhost:8080/
      profile: dev
      label: master

编码

在需要动态刷新的类上添加@RefreshScope注解并通过@value注解引用配置文件中的配置

@RestController
@RequestMapping("/adviser/loss")
@RefreshScope
public class AdviserLossController {

    @Value("${custom.name}")
    private String name;

    @Value("${custom.age}")
    private Integer age;

 @GetMapping("/test")
    public String test(){
        return name + ":" + age;
    }
}

配置git远程文件

custom:
  name: 张三
  age: 20

启动项目

首次启动时,config client会请求config server加载远程配置

测试

首次请求/adviser/loss/test接口,会发现返回=》张三:18,然后修改git上的配置,将年龄修改为20,再次请求/adviser/loss/test接口,发现配置并未更新。那么此时就需要我们手动刷新配置了:发送http post /actuator/refresh接口,此时再次请求/adviser/loss/test接口,会发现返回=》张三:20,说明配置被成功刷新。

源码分析

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

}

该注解上添加了@Scope("refresh")注解指明了作用域名为refresh

RefreshScope

public class RefreshScope extends GenericScope implements ApplicationContextAware,
  ApplicationListener<ContextRefreshedEvent>, Ordered 
{
  // 省略代码。。。。。。
}

该类继承自GenericScope,其核心方法都在父类GenericScope中,而GenericScope继承BeanFactoryPostProcessor

GenericScope

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
  throws BeansException 
{
 this.beanFactory = beanFactory;
 // 放入到缓存中
 beanFactory.registerScope(this.name, this);
 setSerializationId(beanFactory);
}

注册scope,名字为refresh,值为RefreshScope实例。

添加了@RefreshScope注解的Bean对象会被@ComponentScan注解扫描到,核心代码在ClassPathBeanDefinitionScanner类中的doScan方法中:

ClassPathBeanDefinitionScanner

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
 Assert.notEmpty(basePackages, "At least one base package must be specified");
 Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
 for (String basePackage : basePackages) {
  Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
  for (BeanDefinition candidate : candidates) {
   // 关键代码
   ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
   candidate.setScope(scopeMetadata.getScopeName());
   String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
   if (candidate instanceof AbstractBeanDefinition) {
    postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
   }
   if (candidate instanceof AnnotatedBeanDefinition) {
    AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
   }
   if (checkCandidate(beanName, candidate)) {
    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
    definitionHolder =
      AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
    beanDefinitions.add(definitionHolder);
    registerBeanDefinition(definitionHolder, this.registry);
   }
  }
 }
 return beanDefinitions;
}

核心代码在this.scopeMetadataResolver.resolveScopeMetadata(candidate)这一行,这里就不展开讲了,希望同学自己去扩展阅读相关源码。

注册的Scope将会在AbstractBeanFactory#doGetBean方法中调用,该方法中会先拿到当前BeanDefinition中定义的Scope,通过scopeNameMap集合中拿到Scope类,最后调用Scopeget方法获取实例对象。

AbstractBeanFactory

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

   throws BeansException 
{
 // 省略部分代码
 String scopeName = mbd.getScope();
 if (!StringUtils.hasLength(scopeName)) {
  throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
 }
 // 1.从缓存中取,在beanFactory的registerScope方法调用时放入缓存
 Scope scope = this.scopes.get(scopeName);
 if (scope == null) {
  throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
 }
 try {
  // 2.调用scope的get方法创建bean,通过匿名内部类创建ObjectFactory对象并实现getObject方法。在GenericScope的get方法会回调此处的实现,创建一个bean。
  Object scopedInstance = scope.get(beanName, () -> {
   beforePrototypeCreation(beanName);
   try {
    return createBean(beanName, mbd, args);
   }
   finally {
    afterPrototypeCreation(beanName);
   }
  });
  beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
 }
 catch (IllegalStateException ex) {
  throw new ScopeNotActiveException(beanName, scopeName, ex);
 }

 // 省略部分代码
}

在2处,最终会调用RefreshScope#get方法:

GenericScope

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

先是将ObjectFactory实例对象包装成BeanLifecycleWrapper放入缓存中(缓存中没有则放入,有则返回原对象),然后调用BeanLifecycleWrappergetBean方法:

GenericScope@BeanLifecycleWrapper

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

如果bean对象等于null,则通过this.objectFactory.getObject(),又回到AbstractBeanFactorydoGetBean方法中,创建一个bean并返回。

至此已经分析完了RefreshScope类和@RefreshScope注解的来龙去脉,下面分析下Refresh端点触发时机。

Refresh端点触发时机

当调用/actuator/refresh端点时,执行如下refresh:

@Endpoint(id = "refresh")
public class RefreshEndpoint {

 private ContextRefresher contextRefresher;

 public RefreshEndpoint(ContextRefresher contextRefresher) {
  this.contextRefresher = contextRefresher;
 }

 @WriteOperation
 public Collection<String> refresh() {
  Set<String> keys = this.contextRefresher.refresh();
  return keys;
 }

}

RefreshEndpoint是在RefreshEndpointAutoConfiguration类中创建并配置的。内部会通过调用ContextRefresher#refresh方法:

public synchronized Set<String> refresh() {
 //1.刷新环境
 Set<String> keys = refreshEnvironment();
 //2.清空缓存
 this.scope.refreshAll();
 return keys;
}
public synchronized Set<String> refreshEnvironment() {
 //1:提取配置信息修改之前的值,排除systemEnvironment、systemProperties、jndiProperties、servletConfigInitParams、servletContextInitParams、configurationProperties相关配置  
 Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
 //2:重新加载读取配置信息,调用内部的抽象方法,由具体的子类实现
 updateEnvironment();
 //3:获取所有改变的配置
 Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
 //4:发布EnvironmentChangeEvent事件
 this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
 return keys;
}

在2处,重新加载配置,在当前的版本,有2中实现方式:一种是兼容旧版本的实现,一种是新版本的实现,这两种实现分别在RefreshAutoConfiguration中配置:

RefreshAutoConfiguration

//兼容旧版本的实现
@Bean
@ConditionalOnMissingBean
@ConditionalOnBootstrapEnabled
public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope,
  RefreshProperties properties)
 
{
 return new LegacyContextRefresher(context, scope, properties);
}

//新版本的实现
@Bean
@ConditionalOnMissingBean
@ConditionalOnBootstrapDisabled
public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplicationContext context,
  RefreshScope scope, RefreshProperties properties)
 
{
 return new ConfigDataContextRefresher(context, scope, properties);
}

旧版本的实现其原理是:内部启动一个非web环境的SpringBoot应用,重新读取配置信息,用新配置替换旧配置,这个很容易理解。 新版本的实现方式,我也跟踪了源码,也在网上找了很多资料,但是目前网上的都是基于旧版本的分析。关于新版本的实现属实不太理解(功力尚浅),我个人也在学习和总结,后续如果有新的进展,我也会及时更新的,如果您有相关资料或见解,也烦请指教。

在4处发布一个EnvironmentChangeEvent事件,查看该事件的监听器:

ConfigurationPropertiesRebinder

@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
 if (this.applicationContext.equals(event.getSource())
   // Backwards compatible
   || event.getKeys().equals(event.getSource())) {
  rebind();
 }
}
@ManagedOperation
public void rebind() {
 this.errors.clear();
 //遍历所有的配置类(带有@ConfigurationProperties注解的类)
 for (String name : this.beans.getBeanNames()) {
  //对每一个bean进行重新绑定  
  rebind(name);
 }
}
@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);
   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     this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
//初始化Bean  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;
}

补充

ConfigurationPropertiesRebinder类中的beans是通过构造函数传过来的,接下来先查看这个对象是如何被构造的:

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ConfigurationPropertiesBindingPostProcessor.class)
public class ConfigurationPropertiesRebinderAutoConfiguration
  implements ApplicationContextAwareSmartInitializingSingleton 
{

 private ApplicationContext context;

 @Override
 public void setApplicationContext(ApplicationContext applicationContext) {
  this.context = applicationContext;
 }

 @Bean
 @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
 public static ConfigurationPropertiesBeans configurationPropertiesBeans() {
  return new ConfigurationPropertiesBeans();
 }

 @Bean
 @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
 public ConfigurationPropertiesRebinder configurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
  ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder(beans);
  return rebinder;
 }

 @Override
 public void afterSingletonsInstantiated() {
  // After all beans are initialized explicitly rebind beans from the parent
  // so that changes during the initialization of the current context are
  // reflected. In particular this can be important when low level services like
  // decryption are bootstrapped in the parent, but need to change their
  // configuration before the child context is processed.
  if (this.context.getParent() != null) {
   // TODO: make this optional? (E.g. when creating child contexts that prefer to
   // be isolated.)
   ConfigurationPropertiesRebinder rebinder = this.context.getBean(ConfigurationPropertiesRebinder.class);
   for (String name : this.context.getParent().getBeanDefinitionNames()) {
    rebinder.rebind(name);
   }
  }
 }

}

在这个自动配置类中创建了ConfigurationPropertiesRebinder并且将ConfigurationPropertiesBeans 注入。ConfigurationPropertiesBeans是个BeanPostProcessor处理器:

@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessorApplicationContextAware {
 //省略部分代码...
 private Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();
 
 @Override
 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  if (isRefreshScoped(beanName)) {
   return bean;
  }
  ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean.get(this.applicationContext, bean,
    beanName);
  if (propertiesBean != null) {
   //添加有@ConfigurationProperties注解的bean都将保存在该集合中
   this.beans.put(beanName, propertiesBean);
  }
  return bean;
 }

 private boolean isRefreshScoped(String beanName) {
   if (this.refreshScope == null && !this.refreshScopeInitialized) {
    this.refreshScopeInitialized = true;
    for (String scope : this.beanFactory.getRegisteredScopeNames()) {
     if (this.beanFactory.getRegisteredScope(
       scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) {
      this.refreshScope = scope;
      break;
     }
    }
   }
   if (beanName == null || this.refreshScope == null) {
    return false;
   }
   return this.beanFactory.containsBeanDefinition(beanName)
     && this.refreshScope.equals(this.beanFactory.getBeanDefinition(beanName).getScope());
  }

 //省略部分代码...
}

BeanPostProcessorpostProcessBeforeInitialization方法执行,在初始化bean的时候执行,在这里就会判断当前的Bean是否是RefreshScope Bean。

isRefreshScoped方法中遍历注册的所有Scope并且判断是否是有RefreshScope,先从注册的所有Scope中查找RefreshScope,如果不是返回false,如果是则返回true。如果isRefreshScoped方法返回的false就判断当前Bean是否有@ConfigurationProperties注解如果有会被包装成ConfigurationPropertiesBean存入当前的beans集合中(当有refresh发生时会重新绑定这些bean)。接下来继续进入到上面的ConfigurationPropertiesRebinder#rebind方法中。

RefreshScope刷新处理

回到ContextRefresherrefresh方法:

public synchronized Set<String> refresh() {
 Set<String> keys = refreshEnvironment();
 //清空缓存
 this.scope.refreshAll();
 return keys;
}

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() {
 //调用父类GenericScope的destroy方法
 super.destroy();
 //发布RefreshScopeRefreshedEvent事件,我们可以写个监听程序监听该事件
 this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

GenericScope

@Override
public void destroy() {
 List<Throwable> errors = new ArrayList<Throwable>();
 //清空缓存
 Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
 for (BeanLifecycleWrapper wrapper : wrappers) {
  try {
   Lock lock = this.locks.get(wrapper.getName()).writeLock();
   lock.lock();
   try {
    //清空上次创建的对象信息
    wrapper.destroy();
   }
   finally {
    lock.unlock();
   }
  }
  catch (RuntimeException e) {
   errors.add(e);
  }
 }
 if (!errors.isEmpty()) {
  throw wrapIfNecessary(errors.get(0));
 }
 this.errors.clear();
}
public void destroy() {
 if (this.callback == null) {
  return;
 }
 synchronized (this.name) {
  Runnable callback = this.callback;
  if (callback != null) {
   callback.run();
  }
  this.callback = null;
  this.bean = null;
 }
}

当前清空了缓存对象后,下次再进入注入的时候会再次调用ObjectFacotry#getObject方法创建新的对象。

总结

当触发了refresh后,所有的带有@ConfigurationProperties注解的Bean都会自动的刷新并不需要@RefreshScope注解。而有@RefreshScope注解的一般在应用在非配置类上,有成员属性使用@Value注解的,如下:

@RestController
@RequestMapping("/refreshBeanProp")
@RefreshScope
public class RefreshScopeBeanPropController {
    
  @Value("${custom}")
  private String custom ;
    
  @GetMapping("/get")
  public String get() {
    return custom ;
  }
    
}

在此种情况下,调用/actuator/refresh 可使custom动态刷新,在ContextRefresher#refresh中将缓存的Bean清空了后重新生成Bean。

欢迎关注我的公众号:程序员L札记

更多原创文章,请扫码关注我的微信公众号
更多原创文章,请扫码关注我的微信公众号

分类:

后端

标签:

Java

作者介绍

程序员L札记
V1