@RefreshScope 自动刷新原理(三)

前言

文本已收录至个人GitHub仓库,欢迎Star:github.com/bin39232820…
种一棵树最好的时间是十年前,其次是如今
我知道不少人不玩qq了,可是怀旧一下,欢迎加入六脉神剑Java菜鸟学习群,群聊号码:549684836 鼓励你们在技术的路上写博客git

絮叨

上篇文章和你们分析了 Nacos 的配置中心原理,分析了客户端的原理 还有服务端的原理,那么接下来就是咱们要配合这个@RefreshScope这个注解来完成咱们的自动配置github

BeanScope

在SpringIOC中,咱们熟知的BeanScope有单例(singleton)、原型(prototype), Bean的Scope影响了Bean的管理方式,例如建立Scope=singleton的Bean时,IOC会保存实例在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。SpringCloud新增了一个refresh范围的scope,一样用了一种独特的方式改变了Bean的管理方式,使得其能够经过外部化配置(.properties)的刷新,在应用不须要重启的状况下热加载新的外部化配置的值。web

那么这个scope是如何作到热加载的呢?RefreshScope主要作了如下动做:缓存

单独管理Bean生命周期 建立Bean的时候若是是RefreshScope就缓存在一个专门管理的ScopeMap中,这样就能够管理Scope是Refresh的Bean的生命周期了 从新建立Bean 外部化配置刷新以后,会触发一个动做,这个动做将上面的ScopeMap中的Bean清空,这样,这些Bean就会从新被IOC容器建立一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果 下面咱们深刻源码,来验证咱们上述的讲法。bash

管理RefreshBean的生命周期

首先,若想要一个Bean能够自动热加载配置值,这个Bean要被打上@RefreshScope注解,那么就看看这个注解作了什么:app

@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,这有什么用呢?post

建立一个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); } //... } } //... } //... } 复制代码

这里能够看到几件事情:ui

  • 单例和原型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包装起来缓存下来

这里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;
  }
}

复制代码
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的生命周期。

从新建立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();

刷新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对象中的配置值便可。

从新建立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。

结尾

这块目前为止,咱们就了解完成了,小六六其实也是学习了个大概,不少一点一点的细节并非那么清晰,为了之后继续学习作准备吧

平常求赞

好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是真粉

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见

六脉神剑 | 文 【原创】若是本篇博客有任何错误,请批评指教,不胜感激 !

相关文章
相关标签/搜索