首頁 > 軟體

@RereshScope重新整理的原理詳解

2022-12-05 14:00:04

在配合設定中心修改設定讓應用自動重新整理設定時,我們要在需要感知設定變化的bean上面加上@RereshScope。如果我們不加上這注解,那麼有可能無法完成設定自動重新整理。

一、入口

可以看到@RereshScope@Scope("refresh")(bean的作用域)的派生註解並指定了作用域為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;
}

二、設定類解析

在上文重新整理時會執行BeanFacotryPostProcessorbeanDefinition修改和增加,其中設定類解析、類掃描的工作就是在其中執行,而對於一個ScopedProxyMode.NO它會解析成一個ScopedProxyFactoryBean

//ConfigurationClassBeanDefinitionReader設定類解析程式碼片段
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

//如果需要生產代理則建立一個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);
	return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}

//createScopedProxy 程式碼片段 可以看到BeanDefinition是ScopedProxyFactoryBean
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);

ScopedProxyFactoryBean-生成代理物件

FactoryBean在注入時返回的是getObject()所返回的物件,在這裡就是返回的就是proxyScopedProxyFactoryBean實現了BeanFactoryAware那麼在這個bean初始化中會呼叫setBeanFactory()方法,而在這個方法中,為它建立一個CGLIB代理物件作為getObject()的返回值,並使用ScopedObject來代替被代理物件。而在ScopedObject預設實現中每次都是從BeanFactory中獲取(重點)。

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

三、作用域原理

BeanFactory獲取bean時(doGetBean),如果**不是單例或者原型bean**將交給對應的Socpebean,而建立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繼承GenericScope每次獲取bean是從自己的快取(ConcurrentHashMap)中獲取。 如果快取中bean被銷燬了則用objectFactory建立一個。

//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() {
            if (this.bean == null) {
                synchronized(this.name) {
                    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
                    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;
}
//RefreshScope重新整理
public void refreshAll() {
     super.destroy();
     this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

五、總結

@RereshScope會為這個標記的bean生成ScopedProxyFactoryBeanScopedProxyFactoryBean生成一個代理物件使每次方法呼叫的目標物件都從容器中獲取,而獲取refresh作用域的Bean是RefreshScope快取中獲取。當設定中心在觸發重新整理時RefreshScope會刪除Socpe快取的Bean,然後下次獲取時就會用新的Environment建立設定修改後的Bean,這樣就達到了設定的自動更新。

六、問題

為什麼需要生成代理物件?

因為Bean裝配是一次性的,假設沒有代理的情況下,在另一個bean注入這個refreshBean之後就無法改變了,就算refreshBean銷燬(在快取中置為null)並後面重新生成了,但是之前參照還是老的bean,這也是為什麼沒有加@RefreshScope註解而導致設定自動重新整理失效了。

到此這篇關於@RereshScope重新整理的原理詳解的文章就介紹到這了,更多相關@RereshScope重新整理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com