@RefreshScope刷新配置文件原理

一、前置知识

在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注解只又套了一个@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对象

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

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

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

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

三、刷新Environment对象

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

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

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

我们一般是使用@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。

  

最后为什么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何时被调用

 

参考文章:

@RefreshScope 刷新机制都不懂,还敢说会?_架构核心技术的博客-CSDN博客_@refreshscope

Spring Cloud 2.2.2 源码之三十九nacos配置动态刷新原理一_王伟王胖胖的博客-CSDN博客_nacos动态刷新原理

@RefreshScope 自动刷新原理(三) - C/C++教程 - 找一找教程网


 

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值