nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)

@RefreshScope注解的源码分析(基于nacos的讲解)

1.本篇文章主要解答的问题步骤

①.RefreshScope bean对应的BeanDefinition的解析
②.RefreshScope bean创建过程解析
③.nacos的刷新事件解析

2. 功能介绍:

当配置中心的属性发生更改时 该注解可以让bean对应的属性值实时更新

3 使用介绍

@RefreshScope
@Component
@Data()
public class NacosConfig {

    @Value("${server.port}")
    private String port;

}

注意:

①. 配置的类必须要注入到ioc容器中
②.必须要提供get方法,否则不会生效(具体原因 下面解析会说)

4.宏观流程

在这里插入图片描述

5.源码分析

宏观流程解析

1.spring对@RefreshScopre注解的bean做的事情

1.当调用被@RefreshScope注解的bean的属性的get方法时 则先从本地缓存里面获取
2.当本地缓存中 不存在当前bean时 则重新创建 此时 会获取 sping中最新环境配置
3.如果本地缓存中 存在当前 bean则 直接返回对应属性值

2.nacos对@RefreshScopre注解的bean做的事情

1.当配置更改时 nacos服务端会发步一个配置已被更新事件
2.此时 naocs客户端 会接受到这个事件 接受到以后 会再在spring中发布环境配置刷新事件
3.然后 对应的监听器 收到以后 则刷新spring环境配置 以及 清空本地缓存

6.相信很多人看完流程图后 会有很多问题 比如:

1. 为什么调用get方法时 会从缓存里面获取bean 我明明get方法里面什么逻辑也没做呀
2.@RefreshScope注解的bean 是如何创建的 和其他的常规单例bean有什么不一样,缓存是存在哪里的
3.nacos配置中心刷新事件是在哪触发的 如何触发的

7.解答

1. 为什么调用get方法时 会从缓存里面获取bean 我明明get方法里面什么逻辑也没做呀

①答案是 动态代理 没错 此时的bean被spring代理了 并且对get方法做了拦截

②.源码解析

(1)先认识一下@RefreshScope注解的定义在这里插入图片描述

@RefreshScope注解上只有一个核心注解@Scope 并且value为refresh,spring就是根据这个@Scope注解来扫描解析的

(2) 在看一下 解析@RefreshScope的地方(代码位置:ClassPathBeanDefinitionScanner#doScan)

在这里插入图片描述

1.第276行代码是扫描当前的类路径(值为ComponentScan注解的value值) 找到所有在指定路径下符合条件的类
2.第278行是 查询指定类上有没有@Scope注解 如果有则将value值设置到bean定义中 然后注册到ioc容器中
3. @RefreshScope注解上有@Scope注解 所以 在这里也可以被扫描到(具体的 可以打断点进去看)
4. 第290行 主要是对当前bean定义在做一层封装 具体看下图

在这里插入图片描述

1.接着上面的图的逻辑跟进来 发现就是又创建了一个RootBean定义 然后设置了类型为ScopedProxyFactoryBean(这个类很重要) 主要是关注这个ScopedProxyFactoryBean类(后面会用到)
2.对我们的bean做了动态代理的就是这个类(ScopedProxyFactoryBean)是嫌疑犯之一 后面会重点介绍

现在又引入了一个新的问题 为什么要设置成ScopedProxyFactoryBean类 上面说到 是为了做代理 那么它又是在哪里被引用的 又是做了哪些代理 这个就不得不说到 RefreshScope 这个类了注意这个是类嗷 不是注解 看下图的RefreshScope类层级结构

在这里插入图片描述

1.可以看到RefreshScope类继承了GeneriScopre(这个类是重点)

GeneriScopre绝对的重点类(做缓存的就是它) 下面是GenScope的类定义图

先前介绍:BeanDefinitionRegistryPostProcessor的作用以及触发时机

1.作用:允许在所有常规bean注册之前 对ioc容器中的bean定义 做进一步修改
2.触发时机:从上面一句话也能看到是在所有常规bean注册之前
3.代码触发初始位置是在:AbstractApplicationContext#refresh方法中调用invokeBeanFactoryPostProcessors方法 其实BeanDefinitionRegistryPostProcessor是来自BeanFactoryPostProcessor的扩展 也就是继承了BeanFactoryPostProcessor接口
4.该类只有一个方法postProcessBeanDefinitionRegistry

在这里插入图片描述

从上面的介绍以及图中的红框 也大致猜出来了 重点是在BeanDefinitionRegistryPostProcessor类中 所以下面我们来看看GenericScope对postProcessBeanDefinitionRegistry的实现

在这里插入图片描述

1.第248行 判断进来的bean定义的类是否是ScopedProxyFactoryBean类(这是一开始扫描的时候要设置类为ScopedProxyFactoryBean的原因第一个原因)
2.因为RefreshScope继承了GenericScope并且默认构造器中 修改了name属性 然后 看下面的调用栈其实这里是从RefreshScope进来的 所以第249行的getName是Refresh字符串
3.如果类型是ScopedProxyFactoryBean并且当前bean的Scope为Refresh 则将对应的bean class改为LockedScopedProxyFactoryBean类(动态代理的最后一个嫌疑犯来了)

前面铺垫的差不多了 到这里ScopedProxyFactoryBean以及LockedScopedProxyFactoryBean两个做动态代理最最重要的两个类来了 下面就是开始对第一个问题答案做最终的解答

老规矩 先介绍一下这两个类

1.ScopedProxyFactoryBean
先前介绍:1.BeanFactoryAware 的作用和触发时机

1.作用:为指定的提供一个beanFactory实例 只有一个方法setBeanFactory 并且传入了一个当前beanFactory实例
2.触发时机:在指定bean实例化 之后 初始化之前 代码位置:AbstractAutowireCapableBeanFactory#initializeBean的第一行 调用invokeAwareMethods方法

在这里插入图片描述

看完先前介绍 也差不多明白了 这个类核心逻辑是在beanFactory#setBeanFactory方法中 所以 一起来看看吧(这个方法也就是生成代理对象的逻辑)

在这里插入图片描述

1.215行之前 都是在设置各种接口什么
2.215行就是真正的生成代理
3.第95行我圈出了一个SimpleBeanTargetSource 这个类里面有一个getTarget 这个方法也就是代理之后真正执行的逻辑 具体看下图

在这里插入图片描述

1.也就是 再调用一次ioc的getBean方法 重新创建一个bean
2.到这里可以透露一下 刷新机制 就是先删除之前的bean 之后再创建新的

####上面生成代理对象的逻辑找到了 那么拦截的地方在哪了 我们可以看一下下面的调用栈
在这里插入图片描述

1.可以看到我圈出来的地方 很惊奇的发现当前断点是从LockedScopedProxyFactoryBean这个类里面进来的 那么你们也差不多猜到了 这个LockedScopedProxyFactoryBean肯定是继承了ScopedProxyFactoryBean
2.一般要对方法拦截 那么肯定会实现MethodInterceptor这个接口 而我们在ScopedProxyFactoryBean这个类没有看见 那么肯定就在LockedScopedProxyFactoryBean这个类里面了 因此让我们在看一下LockedScopedProxyFactoryBean这个类吧 看下图

在这里插入图片描述

果然LockedScopedProxyFactoryBean这个类实现了MethodInterceptor接口 所以 拦截的地方就在这个类了 所以 接下来重点看一下MethodInterceptor#invoke方法

在这里插入图片描述

1.从调用栈第二个圈的地方可以看出我们当前调用的这个对象 是一个代理对象
2.482行里面的advised.getTargetSource().getTarget()这里就是代理增强的代码的调用
也就是会调用到我在调用栈里第一个圈的地方 这个方法的路径看下图

在这里插入图片描述

1.重点在35行 打断点的地方 会调用一次 beanFaacctory的getBean方法
2.这getBean里面的逻辑是下一个问题的答案 这里先简单说一下
3.这里的getBean方法要走的逻辑是 先判断当前本地缓存里面有没有当前bean对象
如果有的话 则直接取 如果没有的话 则直接重新创建对象 重新获取spring环境里的最新配置值
4.那么什么时候会没有呢 当配置中心发起配置更新事件 此时客户端对应的监听事件会清空缓存 并且更新spring环境 具体过程往下看

2.@RefreshScope注解的bean 是如何创建的 缓存是存在哪里的

1.首先看下图 位置在AbstractBeanFactory#doGetBean里

在这里插入图片描述

1.这一段代码是被@scope注解的对象的创建逻辑
2.可以看到 362行 在创建之前(也就是在调用beanFactory的createBean之前) 会先调用RefreshScope的get方法 传入了两个参数 一个beanName 还有一个lambda表达式 这个表达式是ObjectFactory类型的 看到下面就知道了
3.其实 这个get方法是GenericScope提供的 RefreshScope类并没有重写 所以 我们直接来看GenericScope#get方法

在这里插入图片描述

1.看第二个参数 就可以得知 刚刚上张图 传进来的lambda表达式 是个ObjectFactory类型的 也就是一个创建对象的工厂类
2.重点来了 看第176行 一进来首先就把beanName和 对应的工厂对象放进一个BeanLifecycleWrapper类 然后放入到this.cache里面 这个是真正的缓存类
3.第179就是真正的创建对象了 然后我们来看BeanLifecycleWrapper#getBean方法这个类 重点的逻辑就在这里面

在这里插入图片描述

1.这一段逻辑就是真正的核心逻辑了
2.先判断当前这个缓存里面是不是为null 如果不是就从对应的ObjectFactory
工厂中重新创建
3.如果不为null 则直接返回当前bean

这下@RefreshScope的对象如何创建的 缓存又是那个类 这个问题解决了 接下来就是解析 缓存里的对象是什么时候清空的 该解决第三个问题了

3.nacos配置中心刷新事件是在哪触发的 如何触发的

首先让我们了解一下 NacosContextRefresher类在这里插入图片描述

1.这个类监听了ApplicationReadyEvent事件 这个是重点
2.那么既然实现了ApplicationReadyEvent的监听器 那么onApplicationEvent方法里的逻辑则是重点

这个ApplicationReadyEvent是什么时候触发的 onApplicationEvent里面的逻辑是怎么实现的 我们接的往下看

在这之前 我本来用的是jdk17+springboot3 但是一开始说spring源码的时候没有问题 但是当说 第三方start库(如nacos)时 就会发现出现问题了 因为jdk17很多依赖路径的改变还有spring-boot3丢弃了读取spring.factory文件的自动配置类逻辑 所以 导致
很多第三方自动配置加载不了 所以 下面的项目我换成了jdk8+springboot2.2.6版本了

在这里插入图片描述

1.由调用栈可知ApplicationReadyEvent这个事件是在spring 开始running的时候发起的

那onApplicationEvent里面的逻辑是什么呢? 下图只贴了核心的逻辑

在这里插入图片描述

1 由调用栈可知 这块逻辑是从NacosContextRefresher#setApplicationContext中进来的
2 注意看:134行 这是向nacos客户端里面增加了一个响应RefreshEvent事件的监听器 那么又是谁监听了这个事件了 继续往下看

在这里插入图片描述

从从68行代码可知该事件是由RefreshEventListener监听的 然后继续跟下去

在这里插入图片描述

1.看调用栈 可知 最终会调用到ContextRefresher#refresh方法 也就是上图断点的地方
2.85行 是刷新spring环境
3.86是刷新所有被@RefreshScope对象注解的bean属性值
4.从调用栈右边的圈出的地方 这个值 就是当前配置改变的值 本例中我改的是server.port

刷新spring环境的逻辑这里暂时不说 这里就是更新Environment里的配置值 我们主要说一下 86行代码this.scope.refreshAll()

在这里插入图片描述

从上面的代码走下来 发现是调用了父类的destroy方法 所以也就是调用了GenericScope的destroy方法 继续跟下去

在这里插入图片描述

1.重点来了 发现是直接清空了所有的本地缓存 所以现在应该也明白了 现在清空完以后 当下次调用对应的get方法时 被拦截之后 发现本地缓存里没有 此时就会创建一个新的bean 而在清空本地缓存之前 spring环境配置也都已经被更新了 所以 这时 新的bean的所有属性值都是最新值

在这里插入图片描述
在这里插入图片描述

1.然后当第二次调用的时候 进入put方法里面发现 先判断当前 beanName的key缓存里面有没有 如果有的话 就返回以前的 如果没有的话则返回新的
2.之后就又回到了 下面的逻辑 因为此时缓存为空 所以 就重新创建了新的bean

在这里插入图片描述

8.至此 @RefreshScope的原理就讲完了 其实也就是利用了缓存失效时 就会重新获取 以及 事件驱动 来完成的

9.这个原理的主要思想 就是 如果有缓存就走缓存 没有缓存就重新创建一次 之后在加入缓存 然后 每调用一次就重复这个逻辑

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值