@RereshScope刷新原理

2,183 阅读8分钟

@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.NOBeanDefinition它会解析成一个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()所返回的对象,在这里就是返回的就是proxyScopedProxyFactoryBean实现了BeanFactoryAware那么在这个bean初始化中会调用setBeanFactory()方法,而在这个方法中,为它创建一个CGLIB代理对象作为getObject()的返回值,并使用ScopedObject来代替被代理对象。而在ScopedObject默认实现中拦截方法执行的target每次都是从容器中获取(重点)。 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());
}

todo :AOP 实现逻辑需要理清楚

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);
	}
}

三、作用域原理

每次都从容器中获取,Spring 容器不是有单例池嘛,那还是不是同一个bean,配置还是检测不到?

Bean所用域定义bean生效的范围,我们用的比较多作用域是单例(single)和原型(prototype )。在BeanFactory获取bean时(doGetBean),如果不是单例或者原型bean的话将交给对应的Socpe去获取bean,而创建bean方式和单例bean是一样的。其他作用域像requestsession等等都是属于这一块的扩展: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转化为ScopedProxyFactoryBeanScopedProxyFactoryBean生成一个代理对象用于依赖注入。这个代理对象增强逻辑是使其每次方法执行(JDK反射调用、CGLIB FastClass机制方法id调用)时目标对象都从容器中获取,且refresh作用域的Bean是RefreshScope缓存控制,即当配置中心在触发刷新时RefreshScope会删除Socpe缓存的Bean,那么下次获取或者是执行时就会用新的Environment重新创建基于新配置Bean,这样就达到了配置的自动更新。

六、问题

为什么需要生成代理对象?

因为Bean装配是一次性的,假设没有代理的情况下,在另一个bean注入这个refreshBean之后就无法改变了,就算refreshBean刷新时"销毁"(只是RefreshScope缓存中引用的Bean置为null) 并后面重新生成了,但是之前引用还是老的bean,这也是为什么没有加@RefreshScope注解而导致配置自动刷新失效了。所以如果想要代码中配置能够刷新,就需要保证获取配置是通过这个代理对象。

如何让数据源DataSoure根据配置中心刷新?

下次补上