文章目录
前言
@RefreshScope
是SpringCloud引入的注解,这个注解可用做到动态刷新配置的功能,这里我们用来分析一些背后的逻辑
一、demo
启动nacos
startup.cmd -m standalone
本地启动Nacos
启动后访问地址: http://localhost:8848/nacos/index.html#/login
启动项目打印出来的age是33
在nacos管理台改下配置再调用看看
这个请求调用到上面的SampleController ,可用看到被 @RefreshScope
注解修饰了,里面的变量会跟着动态刷新(从Spring Environment中拿到数据)
二、RefreshScope动态刷新配置机制整体介绍
上面的例子可用看到,一个@RefreshScope
注解就能实现动态刷新的功能,这也体现出来Spring的强大,虽然用起来简单,但是这里背后涉及到的知识点、逻辑还是蛮多的(涉及到Spring SpringCloud spring-cloud-alibaba nacos)这里简单概括下,后面再具体分析
三、Spring容器注册@RefreshScope
注解修饰bean流程分析
对于demo中的SampleController
,这个类是要被注入到容器里面,这是Spring干的事,我们来具体分析下
首先我们要知道ClassPathBeanDefinitionScanner
这个类,doScan方法扫描指定路径下的类注册到Spring容器,在扫描接收后会判断筛选出来的类是否有Scope注解,是否重新生成BeanDefinition
也就是Spring容器扫描到@RefreshScope
注解修饰的类,注册到容器的bean对应的beanClass是ScopedProxyFactoryBean
(这是一个工厂bean 获取这个bean实例的时候拿到是生成的代理对象)另外我们注意下这个bean 的scope
属性,默认是SCOPE_DEFAULT
@RefreshScope
注解生成的bean这个属性是 refresh
这个scope属性取的是scope注解的value值,RefreshScope注解对应的是refresh
这里要提到这个Scope属性,是因为后面getBean的流程跟这个有关
三、Spring容器注册getBean流程简单介绍
在AbstractBeanFactory中doGetBean定义了获取bean的逻辑,这里面根据BeanDefinition的Scope属性有三个分支
分别是Scope属性为 singleton或者为空字符串,Scope属性为prototype,再就是这两种情况之外的value
上面说到RefreshScope注解修饰的类注册到容器对应的scope属性是refresh,这就是第三种情况
这种情况下根据scope的值到spring容器中拿到对应的Scope类,通过Scope类的get方法获取对象
private final Map<String, Scope> scopes = new LinkedHashMap<>(8);
也是在AbstractBeanFactory
这里类里面定义了一个Map来放Scope类,那么问题又来了,这个Map里面的数据(Scope类)从哪里来呢,这就是后面要讲的了
四、spring-cloud-commons 引入RefreshScope类
spring-cloud-commons 是spring-cloud项目都会引入的组件(这个demo用到了spring-cloud-alibaba)
SpringCloud项目实际也是扩展了SpringBoot,我们知道SpringBoot启动后会读取spring.factories文件,解析EnableAutoConfiguration 对应配置类注册一些bean到Spring容器,这里就注册了一个RefreshScope
bean,看这个类名就知道跟上面提到 RefreshScope
注解修饰的bean有关系(是的都叫RefreshScope这个名字,一个是注解,一个是类,类是用来获取对应对象的工具类)
我们先看看RefreshScope这个类的继承体系
往上继承了GenericScope
这个类,其实主要干活的就是这个类,是一个通用的Scope处理类
这个类实现了BeanFactoryPostProcessor
接口,在容器初始化前调用postProcessBeanFactory
方法,把自己注册到Spring存Scope对象的Map中,这个name吗由子类提供(GenericScope )
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
beanFactory.registerScope(this.name, this);
setSerializationId(beanFactory);
}
所以回到前面的问题,如果getBean对应的bean Scope既不是单例,也不是原型,要从Spring缓存Scope的Map中找到对应处理的Scope类,并调用get方法创建实例,spring-cloud-commons 引入的RefreshScope类处理的就是Scope属性为refresh的bean
上面的GenericScope还实现了BeanDefinitionRegistryPostProcessor
这个接口,这个也是在Spring容器启动refresh流程中执行,
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
for (String name : registry.getBeanDefinitionNames()) {
BeanDefinition definition = registry.getBeanDefinition(name);
if (definition instanceof RootBeanDefinition) {
RootBeanDefinition root = (RootBeanDefinition) definition;
if (root.getDecoratedDefinition() != null && root.hasBeanClass()
&& root.getBeanClass() == ScopedProxyFactoryBean.class) {
if (getName().equals(root.getDecoratedDefinition().getBeanDefinition().getScope())) {
root.setBeanClass(LockedScopedProxyFactoryBean.class);
root.getConstructorArgumentValues().addGenericArgumentValue(this);
// surprising that a scoped proxy bean definition is not already
// marked as synthetic?
root.setSynthetic(true);
}
}
}
}
}
这里替换了下RefreshScope注解修饰类在容器中Bean 的beanClass,由原来的ScopedProxyFactoryBean
替换成LockedScopedProxyFactoryBean
LockedScopedProxyFactoryBean 是 ScopedProxyFactoryBean 子类,改下这个类就是为了扩展一些功能,ScopedProxyFactoryBean 是工厂bean,在setBeanFactory
方法里面生成了getObect所需的代理类,在LockedScopedProxyFactoryBean
子类中往这个代理类中加了一个过滤器,在访问业务方法前加锁
五、spring-cloud-commons 引入RefreshEventListener监听类
同样是在 RefreshAutoConfiguration配置文件类,向Spring容器注册了RefreshEventListener监听器类
@Bean
public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
return new RefreshEventListener(contextRefresher);
}
这个类监听到了容器中的RefreshEvent
事件后,调用handle方法
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
调用 ContextRefresher
refresh
方法
清空了GenericScope get方法创建的缓存实例
我们回头再看看getBean的流程
将表达式传给GenericScope
get方法获取实例,如果是第一次调用,肯定要走一遍bean创建流程,
如果第二次调用put方法拿到的是第一次调用时缓存的对象,后面value.getBean获取对象也不用再走一遍bean的创建流程
当接收到RefreshEvent
事件后清空调cache里面的数据,再有请求进来,拿到的BeanLifecycleWrapper 里面都是空的调用value.getBean需要再走一遍bean创建的流程,在这之前 还调用refreshEnvironment
刷新了Spring的Environment信息,所有再次创建bean bean里面绑定的属性值就是最新的啦
这里RefreshEventListener
监听RefreshEvent
事件做到了动态刷新容器中带有RefreshScope
注解的bean
那么接下来我们就要了解下在哪里触发这个事件了
六、spring-cloud-alibaba-nacos-config 引入NacosContextRefresher
NacosContextRefresher这是一个监听器,在NacosConfigAutoConfiguration
配置类中引入,监听ApplicationReadyEvent
事件
在nacos配置发生变化后就发布RefreshEvent事件,RefreshEventListener 监听到这个事件更新Spring 的Environment
七、SpringBoot启动完成后发布ApplicationReadyEvent事件
这个ApplicationReadyEvent
方法在SpringBoot启动过程中最后调用(SpringApplication run方法最后调用),这时候Spring容器也创建完成了
发布ApplicationReadyEvent
事件后,NacosContextRefresher
监听这个事件 再发送RefreshEvent
事件到RefreshEventListener
监听器,这样就完成了RefreshScope
动态刷新的整个流程