一、前置知识

在Spring中bean的作用域(scope)常用的有两种,单例(singleton)、原型(prototype),Bean的Scope影响了Bean的管理方式,例如创建Scope=singleton的Bean时,IOC会将这些Bean实例保存在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。而在SpringCloud中为其新添加了一种作用域为refresh,改变了Bean的管理方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。

  • 那这个scope是如何做到热加载的呢?先说结论:
    因为可以单独管理Bean的创建和销毁 创建Bean的时候如果scope为refresh,这个Bean就缓存在一个专门管理这类scope的map中, 当外部配置更改过后,会触发一个刷新动作,这个动作将上面的map中的Bean清空,这样,当再次用到这个Bean的时候,这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果 下面我们深入源码,来验证我们上述的讲法。

二、@RefreshScope探究

RefreshScope注解要引入什么依赖 注解refreshscope原理_热加载

 可以看到@RefreshScope注解只又套了一个@Scope("refresh"),也就意味着被@RefreshScope注解类作用域会变为refresh,并且其proxyMode属性设置为了TARGET_CLASS,如果是TARGET_CLASS,ioc会为其创建一个代理对象。这里为什么设置成TARGET_CLASS后面再介绍

// 单例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 {
    // 1、由上面的RefreshScope注解可以知道,这里scopeName=refresh
    String scopeName = mbd.getScope();
    // 2、获取RefreshScope对象
    final Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    }
    try {
      // 3、让Scope对象去管理Bean
      Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
          return createBean(beanName, mbd, args);
        }
        finally {
          afterPrototypeCreation(beanName);
        }
      });
      bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }

 而在SpringBoot启动时,把Bean扫描到IOC容器中,不同scope有不同的创建方式,在AbstractBeanFactory#doGetBean方法中,创建scope为refresh的Bean的逻辑就会走最下面的else逻辑。
这里可以得出几个结论:

  • 单例和原型scope的Bean是硬编码单独处理的
  • 除了单例和原型Bean,其他Scope是由Scope对象处理的
  • 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象

RefreshScope注解要引入什么依赖 注解refreshscope原理_作用域_02

通过debug,this.scopes有四类scope,另外3个不常用的scope也对应上了,我们可以看到,返回的是RefreshScope对象,那这个RefreshScope是什么时候加载进来的呢?其实是通过RefreshAutoConfiguration自动装配进来的,不是本文重点,提一下。(这里可以发现我们可以自定义scope,不过一般开发中用不上)

RefreshScope注解要引入什么依赖 注解refreshscope原理_spring cloud_03

 下面我们看下scope.get,前面我们知道这个scope为RefreshScope,所以我们去RefreshScope里面去找get方法,发现没有对其实现,而RefreshScope继承了GenericScope,GenericScope的get如下:

RefreshScope注解要引入什么依赖 注解refreshscope原理_spring cloud_04

这里就是将Bean包装成一个BeanLifecycleWrapper对象,缓存在一个Map中,下次如果再getBean,还是那个旧的BeanLifecycleWrapper

RefreshScope注解要引入什么依赖 注解refreshscope原理_spring cloud_05

 可以看出来,BeanLifecycleWrapper中的bean变量即为实际Bean,第一次get肯定为空,就会调用BeanFactory的createBean方法创建Bean,创建出来之后就会一直保存下来。

三、刷新Environment对象

当配置中心更改配置之后,有两种方式可以动态刷新Bean的配置变量值

  • 向上下文发布一个RefreshEvent事件
  • Http访问/actuator/refresh(springboot2.0之前为/refresh,springboot2.0之后默认没有开启refresh端点,需配置)

不管是什么方式,最终都会调用ContextRefresher这个类的refresh方法,那么我们由此为入口来分析一下,热加载配置的原理:

RefreshScope注解要引入什么依赖 注解refreshscope原理_spring_06

我们一般是使用@Value、@ConfigurationProperties去获取配置变量值,其底层在IOC中则是通过上下文的Environment对象去获取property值,然后依赖注入利用反射Set到Bean对象中去的。

那么如果我们更新Environment里的Property值,然后重新创建一次RefreshBean,再进行一次上述的依赖注入,是不是就能完成配置热加载了呢?@Value的变量值就可以加载为最新的了。

下面说一下几个核心方法

  • refreshEnvironment()方法对比新老配置,返回有变化的配置keys,其中有个重点方法addConfigFilesToEnvironment(),通过名字可判断将最新配置加入到环境变量中
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);
       //设置一个监听器,监听环境改变
	   builder.application()
				.setListeners(Arrays.asList(new BootstrapApplicationListener(),
							new ConfigFileApplicationListener()));
    // 启动上下文
    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对象中的配置值即可。

  • refreshAll()

    这里调用了destroy()就将上文的this.cache(实际就是个map)清空了。 

思路回到sopce.get这里,由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例,value.getBean()方法中也会重新去createBean。

RefreshScope注解要引入什么依赖 注解refreshscope原理_作用域_07

RefreshScope注解要引入什么依赖 注解refreshscope原理_spring_08

  

最后为什么proxyMode属性设置为了TARGET_CLASS?

首先我们要知道ScopedProxyMode的作用:ScopedProxyMode是一个枚举类,该类共定义了四个枚举值,分别为NO、DEFAULT、INTERFACE、TARGET_CLASS,其中DEFAULT和NO的作用是一样的。INTERFACES代表要使用JDK的动态代理来创建代理对象,TARGET_CLASS代表要使用CGLIB来创建代理对象。比如下面这个场景:

@Component
@RefreshScope      
public class Config {
}

@Component
public class UserService {
    @Autowired
    private Config config;
}

@RestController
public class TestController {
    @Autowired
    Config config;
  

}

我们知道对象都是 @Autowired 或者 @Resource 注入进去的,那就会出现一个问题,refresh bean 被销毁重建后,其它类依赖的这个bean 怎么更新?也就是UserService怎么去更新Config对象,答案是代理对象

首先从代码上解释来说UserService持有的是Config的一个代理bean,而代理bean才持有真正Config的bean。而在refersh的时候是销毁是代理bean持有的bean,代理bean是不会被销毁的,然后再次通过代理bean创建新的Config bean即可。也就是说,这个Config的bean在 ioc容器里已经不是原始的类,而是一个代理对象。

下篇文章会通过一个demo演示refresh何时被调用