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.宏观流程

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第1张图片

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注解的定义nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第2张图片

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第3张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第4张图片

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第5张图片

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

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

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第6张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第7张图片

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方法

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第8张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第9张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第10张图片

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

####上面生成代理对象的逻辑找到了 那么拦截的地方在哪了 我们可以看一下下面的调用栈
nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第11张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第12张图片

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第13张图片

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

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第14张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第15张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第16张图片

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

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

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

首先让我们了解一下 NacosContextRefresher类nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第17张图片

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

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第18张图片

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第19张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第20张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第21张图片

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

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第22张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第23张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第24张图片
nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第25张图片

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

nacos配置动态刷新源码分析(也就是@RefreshScope的工作原理)_第26张图片

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

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

你可能感兴趣的:(spring源码解析,springboot源码解析,java,spring,java,springboot,spring,cloud)