@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
注解而导致配置自动刷新失效了。所以如果想要代码中配置能够刷新,就需要保证获取配置是通过这个代理对象。
如何让数据源DataSoure根据配置中心刷新?
下次补上
转载自:https://juejin.cn/post/7172890205425238030