关于@RereshScope刷新原理
@RereshScope
刷新原理
在配合配置中心修改配置让应用自动刷新配置时,我们要在需要感知配置变化的bean
上面加上@RereshScope
。如果我们不加上这注解,那么有可能无法完成配置自动刷新。那么为什么要加这个注解呢?
前言
版本信息
Spring版本5.2.12,Spring Boot以及Cloud版本如下
<spring-boot.version>2.3.7.RELEASE</spring-boot.version> <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
一、入口
可以看到@RereshScope
是@Scope("refresh")
的派生注解并指定了作用域为refresh
并在默认情况下proxyMode= ScopedProxyMode.TARGET_CLASS
即使用CGLIB生成代理对象。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Scope("refresh") @Documented public @interface RefreshScope { ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; }
ScopedProxyMode
ScopedProxyMode
表示作用域的代理模式,共有以下四个值:
DEFAULT
:通常默认no,取决于组件扫描中定义默认是什么NO
:不使用代理INTERFACES
:使用JDK动态代理TARGET_CLASS
:使用CGLIB动态代理
public enum ScopedProxyMode { /** * Default typically equals {@link #NO}, unless a different default * has been configured at the component-scan instruction level. */ DEFAULT, /** * Do not create a scoped proxy. */ NO, /** * Create a JDK dynamic proxy implementing <i>all</i> interfaces exposed by * the class of the target object. */ INTERFACES, /** * Create a class-based proxy (uses CGLIB). */ TARGET_CLASS; }
二、BeanDefinition
解析
在上文刷新时会执行BeanFacotryPostProcessor
可以对beanDefinition
进行修改or增加,其中配置类解析、类扫描的工作就是在其中执行,而对于一个非ScopedProxyMode.NO
的BeanDefinition
它会解析成一个ScopedProxyFactoryBean
。在@ScopedProxyMode
中默认是ScopedProxyMode.TARGET_CLASS
也就在组件扫描之后被它标记的bean会是ScopedProxyFactoryBean
//ClassPathBeanDefinitionScanner 类扫描代码片段 protected Set<BeanDefinitionHolder> doScan(String... basePackages) { //扫描的BeanDefinition Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); for (String basePackage : basePackages) { Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { //省略初始化代码 if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); //应用作用域代理模式 替换当前的definition definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; } //如果需要生成代理则创建一个ScopedProxy的BeanDefinition static BeanDefinitionHolder applyScopedProxyMode( ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode(); //不需要生成代理 if (scopedProxyMode.equals(ScopedProxyMode.NO)) { return definition; } boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); //创建一个Scope代理对象的BeanDefinition return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); } //createScopedProxy 代码片段 可以看到BeanDefinition是ScopedProxyFactoryBean RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); //ConfigurationClassBeanDefinitionReader为配置类解析后读取bean定义,无论是哪种方法去都是会和上面一样如果不是ScopedProxyMode.NO替换成ScopedProxyFactoryBean
ScopedProxyFactoryBean
-注入代理对象
在Spring中,对于FactoryBean
在依赖注入时,注入的是其getObject()
所返回的对象,在这里就是返回的就是proxy
。ScopedProxyFactoryBean
实现了BeanFactoryAware
那么在这个bean初始化中会调用setBeanFactory()
方法,而在这个方法中,为它创建一个CGLIB
代理对象作为getObject()
的返回值,并使用ScopedObject
来代替被代理对象。而在ScopedObject
默认实现中每次都是从BeanFactory
中获取(重点)。ps:Bean生命周期大致分为实例化、属性填充、初始化以及销毁,而在初始化之前先会执行一些Aware接口。
@Override public Object getObject() { if (this.proxy == null) { throw new FactoryBeanNotInitializedException(); } //返回代理对象 return this.proxy; } @Override public void setBeanFactory(BeanFactory beanFactory) { //...省略其他代码 // Add an introduction that implements only the methods on ScopedObject. //增加一个拦截使用ScopedObject来被代理对象调用方法 ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName()); //委托ScopedObject去执行 pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject)); // Add the AopInfrastructureBean marker to indicate that the scoped proxy // itself is not subject to auto-proxying! Only its target bean is. // AOP复用这个代理对象 pf.addInterface(AopInfrastructureBean.class); //创建代理对象 this.proxy = pf.getProxy(cbf.getBeanClassLoader()); }
ScopedObject
-代理执行时从容器中获取执行目标
作用域对象的AOP
引入的接口。可以将从ScopedProxyFactoryBean
创建的对象强制转换到此接口,从而可以控制访问原始目标对象并通过编程删除目标对象。在默认实现中是每次方法拦截都从容器中获取被代理的目标对象。
public interface ScopedObject extends RawTargetAccess { //返回当前代理对象后面的目标对象 Object getTargetObject(); void removeFromScope(); } public class DefaultScopedObject implements ScopedObject, Serializable { //...省略字段信息和构造器 @Override public Object getTargetObject() { //从容器中获取 return this.beanFactory.getBean(this.targetBeanName); } @Override public void removeFromScope() { this.beanFactory.destroyScopedBean(this.targetBeanName); } }
三、作用域原理
Bean
所用域定义bean
生效的范围,我们用的比较多作用域是单例(single
)和原型(prototype
)。在BeanFactory
获取bean
时(doGetBean
),如果不是单例或者原型bean
的话将交给对应的Socpe
去获取bean
,而创建bean
方式和单例bean
是一样的。其他作用域像request
、session
等等都是属于这一块的扩展:SPI+策略模式。
//AbstractBeanFactory doGetBean()代码片段 String scopeName = mbd.getScope(); //获取对应的scope final Scope scope = this.scopes.get(scopeName); //参数检查省略。。。 try { //使用的对应的Socpe去获取bean 获取不到则使用后面的`ObjectFactory` Object scopedInstance = scope.get(beanName, () -> { //ObjectFactory lambda表达式 怎么创建bean beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
RefreshScope
-缓存管理Bean
RefreshScope
继承GenericScope
每次获取bean
是从自己的缓存(ConcurrentHashMap
)中获取。 如果缓存中bean被销毁了则用objectFactory
创建一个。自身缓存Bean这样可以在配置刷新时销毁并重建,而未更新时使用旧Bean,避免重复创建。因为代理增强逻辑是每次方法执行都要从容器中获取,容器中没有bean的话需要创建一个Bean。
//GenericScope 中获取get实现 public Object get(String name, ObjectFactory<?> objectFactory) { //从缓存中获取 缓存的实现就是ConcurrentHashMap BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); try { return value.getBean(); } catch (RuntimeException var5) { this.errors.put(name, var5); throw var5; } }
private static class BeanLifecycleWrapper { //当前bean对象 private Object bean; //销毁回调 private Runnable callback; //bean名称 private final String name; //bean工厂 private final ObjectFactory<?> objectFactory; //获取 public Object getBean() { //为null则表示未创建或者已经销毁 if (this.bean == null) { synchronized(this.name) { //double check if (this.bean == null) { this.bean = this.objectFactory.getObject(); } } } return this.bean; } //销毁 public void destroy() { if (this.callback != null) { synchronized(this.name) { Runnable callback = this.callback; if (callback != null) { callback.run(); } this.callback = null; //仅仅置为null 因为其他bean引用这个bean this.bean = null; } } } }
四、配置刷新
如何触发
当配置中心刷新配置之后,有两种方式可以动态刷新Bean的配置变量值,(SpringCloud-Bus
还是Nacos
差不多都是这么实现的):
- 向上下文发布一个
RefreshEvent
事件 Http
访问/refresh
这个EndPoint
不管是什么方式,最终都会调用ContextRefresher
这个类的refresh
方法,代码如下。
public synchronized Set<String> refresh() { //刷新环境 Set<String> keys = this.refreshEnvironment(); //刷新bean 其实就是销毁refreshScope中缓存的bean this.scope.refreshAll(); return keys; }
RefreshEventListener-监听事件
监听到RefreshEvent事件通过ContextRefresher
进行刷新
public class RefreshEventListener implements SmartApplicationListener { private ContextRefresher refresh; private AtomicBoolean ready = new AtomicBoolean(false); @Override public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { return ApplicationReadyEvent.class.isAssignableFrom(eventType) || RefreshEvent.class.isAssignableFrom(eventType); } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationReadyEvent) { handle((ApplicationReadyEvent) event); } //处理刷新事件 else if (event instanceof RefreshEvent) { handle((RefreshEvent) event); } } public void handle(RefreshEvent event) { if (this.ready.get()) { // don't handle events before app is ready //通过ContextRefresher进行刷新 Set<String> keys = this.refresh.refresh(); } } }
RefreshEndpoint
RefreshEndpoint
端点也是通过ContextRefresher
进行刷新
@Endpoint(id = "refresh") public class RefreshEndpoint { private ContextRefresher contextRefresher; @WriteOperation public Collection<String> refresh() { Set<String> keys = this.contextRefresher.refresh(); return keys; } }
Nacos Config注册的监听器
Nacos在检测到配置修改时发布RefreshEvent
事件来触发刷新。
//NacosContextRefresher 代码片段 private void registerNacosListener(final String groupKey, final String dataKey) { String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey); Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() { @Override public void innerReceive(String dataId, String group, String configInfo) { refreshCountIncrement(); nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); // todo feature: support single refresh for listening //发布事件 applicationContext.publishEvent( new RefreshEvent(this, null, "Refresh Nacos config")); //debug日志省略 } }); try { configService.addListener(dataKey, groupKey, listener); } //异常处理省略 }
RefreshScope
-刷新时销毁缓存中Bean
销毁其实就是调用下callback
并将Bean置为null而并不是走Bean销毁这一生命周期。
//RefreshScope刷新 public void refreshAll() { //销毁bean super.destroy(); this.context.publishEvent(new RefreshScopeRefreshedEvent()); } //清空缓存 并挨个销毁 public void destroy() { List<Throwable> errors = new ArrayList<Throwable>(); Collection<BeanLifecycleWrapper> wrappers = this.cache.clear(); for (BeanLifecycleWrapper wrapper : wrappers) { try { //加锁 Lock lock = this.locks.get(wrapper.getName()).writeLock(); lock.lock(); try { wrapper.destroy(); } finally { lock.unlock(); } } //异常处理 } //异常处理 } //销毁其实就是调用下callback 销毁只是将置为null 而并不是走Bean销毁这一生命周期 public void destroy() { if (this.callback == null) { return; } synchronized (this.name) { Runnable callback = this.callback; if (callback != null) { callback.run(); } this.callback = null; this.bean = null; } }
五、总结
刷新原理
@RereshScope
会为这个标记的bean
转化为ScopedProxyFactoryBean
,ScopedProxyFactoryBean
生成一个代理对象用于依赖注入。这个代理对象增强逻辑是使其每次方法执行(JDK反射调用、CGLIB FastClass机制方法id调用)时目标对象都从容器中获取,且refresh
作用域的Bean是由RefreshScope
缓存控制,即当配置中心在触发刷新时RefreshScope
会删除Socpe
缓存的Bean,那么下次获取或者是执行时就会用新的Environment
重新创建基于新配置Bean,这样就达到了配置的自动更新。
六、问题
为什么需要生成代理对象?
因为Bean装配是一次性的,假设没有代理的情况下,在另一个bean
注入这个refreshBean
之后就无法改变了,就算refreshBean
刷新时"销毁"(只是RefreshScope
缓存中引用的Bean置为null) 并后面重新生成了,但是之前引用还是老的bean
,这也是为什么没有加@RefreshScope
注解而导致配置自动刷新失效了。所以如果想要代码中配置能够刷新,就需要保证获取配置是通过这个代理对象。