• Spring Cloud--@RefreshScope动态刷新的原理


    原文网址:Spring Cloud--@RefreshScope动态刷新的原理_IT利刃出鞘的博客-CSDN博客

    简介

    本文介绍Spring Cloud的@RefreshScope动态刷新的原理。

    原理概述

    Spring的作用域有:single(单例)、prototype(多例)等(详见:Spring--Bean的作用域(scope)--使用/详解_IT利刃出鞘的博客-CSDN博客)。

    SpringCloud新增了一个自定义的作用域:refresh(可以理解为“动态刷新”),改变了Bean的管理方式,使其可以通过外部化配置(.yml或.properties)的刷新,在不需要重启应用的情况下热加载新的外部化配置的值。

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

    1. 需要动态刷新的类标注@RefreshScope 注解(单独管理Bean生命周期)
      1. @RefreshScope 注解标注了@Scope,里边有个默认属性:ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能是再创建一个代理,在每次调用的时候都用它来调用GenericScope的get 方法来获取对象。
      2. 所以在创建Bean时,如果有@RefreshScope,就缓存在一个ScopeMap中(所以带有@RefreshScope的Bean其实一共创建了两个bean)。
    2. 若属性发生变更
      1. 调用 ContextRefresher refresh() => RefreshScope refreshAll() 进行缓存清理
      2. 发送刷新事件通知,调用GenericScope 的destroy() 实现清理缓存。这里将上面的ScopeMap中的Bean清空
    3. 在下一次使用对象时
      1. 调用GenericScope get(String name, ObjectFactory objectFactory) 方法创建一个新的对象,使用最新的外部化配置的值注入类中,达到热加载新值的效果。

    管理Bean的获取

    @RefreshScope注解

    一个Bean想自动加载配置,就要打上@RefreshScope注解,看看这个注解

    1. @Target({ ElementType.TYPE, ElementType.METHOD })
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Scope("refresh")
    4. @Documented
    5. public @interface RefreshScope {
    6. /**
    7. * @see Scope#proxyMode()
    8. * @return proxy mode
    9. */
    10. ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
    11. }

    RefreshScope有一个属性:proxyMode=ScopedProxyMode.TARGET_CLASS,这个是AOP动态代理用。

    上边标注了 @Scope("refresh") ,将Bean的Scope变为refresh。SpringBoot的启动类上我们都会加上@SpringBootApplication注解(里面是一个@ComponentScan),它会扫描包中通过注解注册的Bean,扫描到带有@RefreshScope注解的Bean时,将其BeanDefinition的scope变为refresh。

    创建Bean时区分scope

    创建一个Bean的时候,会去BeanFactory的doGetBean方法创建Bean,不同scope有不同的创建方式:AbstractBeanFactory的doGetBean方法:

    1. protected T doGetBean(final String name,
    2. @Nullable final Class requiredType,
    3. @Nullable final Object[] args, boolean typeCheckOnly)
    4. throws BeansException {
    5. //....
    6. // Create bean instance.
    7. // 单例Bean的创建
    8. if (mbd.isSingleton()) {
    9. sharedInstance = getSingleton(beanName, () -> {
    10. try {
    11. return createBean(beanName, mbd, args);
    12. }
    13. //...
    14. });
    15. bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    16. }
    17. // 原型Bean的创建
    18. else if (mbd.isPrototype()) {
    19. // It's a prototype -> create a new instance.
    20. // ...
    21. try {
    22. prototypeInstance = createBean(beanName, mbd, args);
    23. }
    24. //...
    25. bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
    26. }
    27. else {
    28. // 由上面的RefreshScope注解可以知道,这里scopeName=refresh
    29. String scopeName = mbd.getScope();
    30. // 获取Refresh的Scope对象
    31. final Scope scope = this.scopes.get(scopeName);
    32. if (scope == null) {
    33. throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    34. }
    35. try {
    36. // 让Scope对象去管理Bean
    37. Object scopedInstance = scope.get(beanName, () -> {
    38. beforePrototypeCreation(beanName);
    39. try {
    40. return createBean(beanName, mbd, args);
    41. }
    42. finally {
    43. afterPrototypeCreation(beanName);
    44. }
    45. });
    46. bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    47. }
    48. //...
    49. }
    50. }
    1. 除了单例和原型Bean,其他Scope是由Scope对象处理的
    2. 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象

    这里scope.get获取的Scope对象为RefreshScope。可见,创建Bean还是由IOC来做(createBean方法),但是获取Bean由RefreshScope对象的get方法去获取,其get方法在父类GenericScope中实现。

    RefreshScope获取Bean

    RefreshScope继承了GenericScope类,最终调用的是GenericScope的get方法:

    1. public class GenericScope
    2. implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
    3. public Object get(String name, ObjectFactory < ? > objectFactory) {
    4. // 将Bean缓存下来
    5. BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
    6. this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
    7. try {
    8. // 创建Bean,只会创建一次,后面直接返回创建好的Bean
    9. return value.getBean();
    10. } catch (RuntimeException e) {
    11. this.errors.put(name, e);
    12. throw e;
    13. }
    14. }
    15. // 其他代码
    16. }

    放入缓存

    1. private final ScopeCache cache;
    2. // 对应上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
    3. public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
    4. return (BeanLifecycleWrapper) this.cache.put(name, value);
    5. }

    这里的ScopeCache对象是一个HashMap:

     

    1. public class StandardScopeCache implements ScopeCache {
    2. private final ConcurrentMap cache = new ConcurrentHashMap();
    3. //...
    4. public Object get(String name) {
    5. return this.cache.get(name);
    6. }
    7. // 如果不存在,才会put进去
    8. public Object put(String name, Object value) {
    9. // result若不等于null,表示缓存存在了,不会进行put操作
    10. Object result = this.cache.putIfAbsent(name, value);
    11. if (result != null) {
    12. // 直接返回旧对象
    13. return result;
    14. }
    15. // put成功,返回新对象
    16. return value;
    17. }
    18. }

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

    回到Scope的get方法,接下来就是调用BeanWrapper的getBean方法: 

    1. // 实际Bean对象,缓存下来了
    2. private Object bean;
    3. public Object getBean() {
    4. if (this.bean == null) {
    5. synchronized (this.name) {
    6. if (this.bean == null) {
    7. this.bean = this.objectFactory.getObject();
    8. }
    9. }
    10. }
    11. return this.bean;
    12. }

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

    由此可见,RefreshScope管理了Scope=Refresh的Bean的生命周期。

    重新创建RefreshBean

    配置中心修改配置后,有两种方式可以动态刷新Bean的配置值,(SpringCloud-Bus、Nacos都是这么实现的):

    1. 向上下文发布一个RefreshEvent事件
    2. Http访问/refresh这个EndPoint

    不管是什么方式,都会调用ContextRefresher#refresh()方法.

    1. // 这就是我们上面一直分析的Scope对象(实际上可以看作一个保存refreshBean的Map)
    2. private RefreshScope scope;
    3. public synchronized Set refresh() {
    4. // 更新上下文中Environment外部化配置值
    5. Set keys = refreshEnvironment();
    6. // 调用scope对象的refreshAll方法
    7. this.scope.refreshAll();
    8. return keys;
    9. }

    刷新的方案

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

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

    这里说的刷新Environment对象并重新依赖注入是上边refresh()中这两个方法做的事情:

    1. Set keys = refreshEnvironment();
    2. this.scope.refreshAll();

    刷新Environment对象

    简单介绍如何刷新Environment里的Property值

    1. public synchronized Set refreshEnvironment() {
    2. // 获取刷新配置前的配置信息,对比用
    3. Map before = extract(
    4. this.context.getEnvironment().getPropertySources());
    5. // 刷新Environment
    6. addConfigFilesToEnvironment();
    7. // 这里上下文的Environment已经是新的值了
    8. // 进行新旧对比,结果返回有变化的值
    9. Set keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
    10. this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
    11. return keys;
    12. }

    重点是addConfigFilesToEnvironment方法(刷新Environment)

    1. ConfigurableApplicationContext addConfigFilesToEnvironment() {
    2. ConfigurableApplicationContext capture = null;
    3. try {
    4. // 从上下文拿出Environment对象,copy一份
    5. StandardEnvironment environment = copyEnvironment(
    6. this.context.getEnvironment());
    7. // SpringBoot启动类builder,准备新做一个Spring上下文启动
    8. SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
    9. // banner和web都关闭,因为只是想单纯利用新的Spring上下文构造一个新的Environment
    10. .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
    11. // 传入我们刚刚copy的Environment实例
    12. .environment(environment);
    13. // 启动上下文
    14. capture = builder.run();
    15. // 这个时候,通过上下文SpringIOC的启动,刚刚Environment对象就变成带有最新配置值的Environment了
    16. // 获取旧的外部化配置列表
    17. MutablePropertySources target = this.context.getEnvironment()
    18. .getPropertySources();
    19. String targetName = null;
    20. // 遍历这个最新的Environment外部化配置列表
    21. for (PropertySource source : environment.getPropertySources()) {
    22. String name = source.getName();
    23. if (target.contains(name)) {
    24. targetName = name;
    25. }
    26. // 某些配置源不做替换,读者自行查看源码
    27. // 一般的配置源都会进入if语句
    28. if (!this.standardSources.contains(name)) {
    29. if (target.contains(name)) {
    30. // 用新的配置替换旧的配置
    31. target.replace(name, source);
    32. }
    33. else {
    34. //....
    35. }
    36. }
    37. }
    38. }
    39. //....
    40. }

    这里是SpringBoot启动上下文那种方法,新做了一个Spring上下文。因为Spring启动后会对上下文中的Environment进行初始化,获取最新配置,所以这里利用Spring的启动,获取了最新的Environment对象,然后去替换旧的上下文中的Environment对象中的配置值。

    重新创建RefreshBean

    经过上述动作,此时上下文中的配置值已经是最新的了。回到ContextRefresher的refresh方法,它会调用Scope的refreshAll方法:

    1. public void refreshAll() {
    2. // 销毁Bean
    3. super.destroy();
    4. this.context.publishEvent(new RefreshScopeRefreshedEvent());
    5. }
    6. public void destroy() {
    7. List errors = new ArrayList();
    8. // 缓存清空
    9. Collection wrappers = this.cache.clear();
    10. // ...
    11. }

    还记得上面的“管理Bean的获取”关于缓存的讨论吗,cache变量是一个Map保存着RefreshBean实例,这里直接就将Map清空了。

    回到BeanFactory的doGetBean的流程中,从IOC容器中获取RefreshBean是交给RefreshScope的get方法做的:

    1. public Object get(String name, ObjectFactory objectFactory) {
    2. // 由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例
    3. BeanLifecycleWrapper value = this.cache.put(name,
    4. new BeanLifecycleWrapper(name, objectFactory));
    5. this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
    6. try {
    7. // 在这里是新的BeanLifecycleWrapper实例调用getBean方法
    8. return value.getBean();
    9. }
    10. catch (RuntimeException e) {
    11. this.errors.put(name, e);
    12. throw e;
    13. }
    14. }
    1. public Object getBean() {
    2. // 由于是新的BeanLifecycleWrapper实例,这里一定为null
    3. if (this.bean == null) {
    4. synchronized (this.name) {
    5. if (this.bean == null) {
    6. // 调用IOC容器的createBean,再创建一个Bean出来
    7. this.bean = this.objectFactory.getObject();
    8. }
    9. }
    10. }
    11. return this.bean;
    12. }

    此时RefreshBean被IOC容器重新创建出来了,经过IOC的依赖注入功能,@Value就是一个新的配置了。到这里热加载功能实现基本结束。

    可以看出只要从IOC容器中getBean,那么拿到的RefreshBean一定是带有最新配置值的Bean。

  • 相关阅读:
    clion远程编译
    【C++ 学习 ㉝】- C++11 使用 using 定义别名
    塔望3W消费战略全案丨绿力冬瓜茶 三十年饮料老品牌,两年复兴战全国
    跳转打开新窗口
    【Ant Design Pro】使用ant design pro做为你的开发模板(八)开发第一个完整的后台页面
    碎煤机crusher
    STM32F0单片机基于Hal库温控智能风扇
    温湿度监测技术又进化了,这个操作太牛了!
    详解Kafka 3.0 稳定版新特性
    如何免费试用阿里云的视频画质增强服务50元额度
  • 原文地址:https://blog.csdn.net/feiying0canglang/article/details/129249938