Spring Cloud動態配置實現原理與源碼分析

實際項目開發中少不了各種配置,如連接數據庫的配置、連接 Redis 集羣的配置等,通常我們也會爲一個項目部署到每個環境準備不同的配置文件,例如測試環境配置連接測試的數據庫。基本上靜態配置就已經滿足日常需求,但是靜態配置缺少靈活性,一經修改就需要重新構建部署應用,同時也缺少安全性,容易泄漏線上環境的配置,所以我們需要一種更靈活更安全的配置方式:動態配置。

動態配置的使用場景並不是爲了替換靜態配置而出現的,數據庫連接配置這些一般都不會改動,所以數據庫連接這類配置使用靜態配置還是動態配置都沒有多大影響。對於那些變動頻率高的配置,纔會迫切去使用動態配置。例如支付頁面展示的支付方式,當第三方支付公司升級服務時,就可以暫時隱藏掉該支付方式;例如集羣環境下控制哪些節點做哪些事情;例如控制接口降級、路由修改等等。

Spring Cloud動態配置實現原理與源碼分析

 

實現動態配置的方式很簡單,我們可以將配置寫到一個專門用來做動態配置的數據庫,又或者使用其它的持久化存儲方式,然後在代碼中定時查看配置有沒有更新,有更新就替換舊的配置,然後做一些配置更新後的操作。也可以將實現動態配置的邏輯封裝爲一個 jar 包,實現代碼複用。

因爲動態配置有它存在的意義,所以 Spring Cloud 也爲我們封裝了大部分的實現動態配置的邏輯,讓我們使用動態配置更方便。而具體的配置信息存儲在哪、怎麼獲取,這些則交給配置中心去實現,如 Nacos 、 Diamond 、 Disconf 。

本篇從源碼分析 Spring Cloud 實現動態配置的原理。 Spring Cloud 實現動態配置需要結合 Spring 源碼分析。

目錄:

Spring Cloud
@RefreshScope
Spring Cloud

Spring Cloud動態配置的使用方式

在 Spring Cloud 項目中,無論你使用何種配置中心,使用動態配置功能的方式都可以是一種,我們來看一個使用動態配置的例子。

@Component
@ConfigurationProperties(prefix = "sck-demo")
@RefreshScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class DemoProps {
    private String message;
}

DemoProps 類省略了 get 、 set 方法。 DemoProps 類使用 @Component 註解和 @ConfigurationProperties 註解聲明爲用於裝載配置的 bean 。 @RefreshScope 註解則用於聲明該 bean 的 scope 以及代理模式 ScopedProxyMode 。

爲了便於理解,我們將這類用於裝載配置的類稱爲 Properties 類,這類用於裝載配置的 bean 稱爲動態配置 bean 。

我們常見的 scope 有 singleton (單例)、 prototype (原型),當然還有其它的,而今天我們要學習一個新的 scope : refresh 。 @RefreshScope 註解類的源碼如下。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

@RefreshScope 註解也被一個 @Scope 註解註釋,這就相當於是兩個註解的結合使用。如源碼所示,當我們不配置 @RefreshScope 註解的 proxyMode 屬性時,默認使用的代理模式爲 TARGET_CLASS 。

爲什麼使用 @RefreshScope 註解就能讓一個動態配置 bean 實現動態裝載配置呢?這是第一個等待我們從源碼中尋找答案的問題。

使用@RefreshScope可能會遇到的問題

給 Properties 類添加 @RefreshScope 註解的目的是聲明動態配置 Bean 的 scope 爲 refresh ,以及聲明 Bean 的代理模式( ScopedProxyMode )。

代理模式 ScopedProxyMode 的可取值爲:

  • NO :不創建代理類;
  • DEFAULT :其作用通常等於 NO ;
  • INTERFACES :創建一個 JDK 動態代理類來實現目標對象的類的所有接口;
  • TARGET_CLASS :使用 Cglib 爲目標對象的類創建一個代理類,這是 @RefreshScope 使用的默認值;

其中 INTERFACES 代理模式不適用於動態配置 Bean ,因爲 Properties 類沒有實現任何接口,如果強行給 @RefreshScope 註解配置代理模式使用 INTERFACES , Spring 將會拋出異常。

當我們配置 @RefreshScope 的 proxyMode 屬性使用默認的 TARGET_CLASS 代理模式時,我們可能會遇到獲取該 Bean 的屬性爲 Null 的情況,這是因爲我們在其它 Bean 中使用 @Resource 或 @Autowired 註解方式引用的對象是動態代理對象,即使用 Cglib 生成的動態代理類的實例。所以我們只能通過 get 方法去獲取對象的字段的值,這是我們在使用動態配置時需要注意的。

當我們配置 @RefreshScope 的 proxyMode 屬性使用 NO 或者 DEFAULT 代理模式時,如果使用 @Resource 或 @Autowired 註解方式方式引用對象,那麼動態配置就會失效,也就是動態修改配置後拿到的還是舊的配置。這是因爲 @RefreshScope 註解會將 Bean 的 scope 聲明爲 refresh ,所以對象不是單例的。

當配置改變時, Spring Cloud 的實現是將動態配置 Bean 銷燬再創建新的 Bean ,由於是在單例的 Bean 中使用 @Resource 或 @Autowired 註解方式引用該對象,單例 Bean 在初始化時就已經爲字段賦值,在單例 Bean 的生命週期內都不會再刷新 bean 字段的引用,所以單例 Bean 就會一直引用一箇舊的動態配置 bean ,自然就無法感知配置改變了。

爲什麼調用代理對象的 get 方法就能獲取到新的配置,以及當配置改變時 Spring Cloud 的實現是將動態配置 Bean 銷燬再創建新的 Bean 這句怎麼理解?這是第二個等待我們從源碼中尋找答案的問題。

我們將帶着這兩個問題從源碼中尋找答案。

從源碼分析Spring Cloud動態配置的實現原理

根據前面的分析,我們不妨假設:當使用 @RefreshScope 註解配置 Properties 類的代理模式爲 TARGET_CLASS 時,被 @RefreshScope 聲明的動態配置 bean 將會是一個特殊的動態代理對象,在每次調用該動態代理對象的方法時,都是根據目標對象的 beanName 或者類型從 bean 工廠中獲取 bean ,而 bean 不是單例的,所以每次獲取都創建新的。這樣也就能解釋得清爲什麼使用 @Resource 或 @Autowired 註解如果注入的對象是代理對象就能通過 get 方法獲取到字段的最新值。

首先,我們可以在代碼中添加如下配置,將 cglib 生成的動態代理輸出到文件。

public class App{
    static {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/tmp");
    }
}

以前面例子的 DemoProps 類爲例, cglib 生成的動態代理類如下:

public class DemoProps$$EnhancerBySpringCGLIB$$593bbd8b extends DemoProps 
            implements ScopedObject, Serializable,
            AopInfrastructureBean, SpringProxy, 
            Advised, Factory {
            // .......
}

因爲沒什麼特別的,所以代碼就省略了。我們只需要記住, Spring 爲使用 @RefreshScope 聲明且代理模式爲 TARGET_CLASS 的類生成的動態代理類實現了 Advised 接口( AOP 的“通知”或者說是“增強”)。

從 cglib 生成的動態代理類找不到突破口,那麼我們只能從 Spring 掃描 bean 開始了,看下哪些地方使用到 @RefreshScope 註解。 Spring 掃描 bean 的源碼在 ClassPathBeanDefinitionScanner 類的 doScan 方法,源碼如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

Spring 掃描 bean 就是將被 @Component 這類註解註釋的類掃描出來並生成 BeanDefinition , Spring 在創建 bean 時就是根據 BeanDefinition 創建的。 doScan 方法掃描生成 BeanDefinition 之後還會將 BeanDefinition 註冊到 bena 工廠,只有註冊到 bean 工廠 bean 才能被創建出來。

如上圖中畫線代碼所示, Spring 在將 BeanDefinition 註冊到工廠之前,會先解析 BeanDefinition 獲取 bean 的 scope 和 ScopedProxyMode ,即 ScopeMetadata 。最後根據代理模式 ScopedProxyMode 判斷是否需要爲該 BeanDefinition 生成代理類的 BeanDefinition 。 AnnotationConfigUtils 的 applyScopedProxyMode 方法的源碼如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

如源碼所示,當 Bean 的 ScopedProxyMode 不爲 NO 時,該方法會爲當前 bean 類生成一個代理類,並返回代理類的 BeanDefinition ,最後 doScan 方法中註冊的 BeanDefinition 將是代理類的 BeanDefinition ,所以在其它 bean 中使用 @Resource 或 @Autowired 註解所引用的動態配置 bean 其實是它的代理對象。

ScopedProxyMode 的源碼如下。

public class ScopeMetadata {
	private String scopeName = BeanDefinition.SCOPE_SINGLETON;
	private ScopedProxyMode scopedProxyMode = ScopedProxyMode.NO;
}

從 ScopeMetadata 類的源碼可以看出,當 bean 沒有被 @Scope 註解聲明時,默認的 scope 爲 singleton (單例),當 bean 沒有被 @RefreshScope 註解聲明時,默認使用的 ScopedProxyMode 爲 NO 。

被 @RefreshScope 註解聲明的 bean ,其 scope 爲 refresh ,默認使用的 ScopedProxyMode 爲 TARGET_CLASS 。所以 AnnotationConfigUtils 的 applyScopedProxyMode 方法將調用 ScopedProxyCreator 的 createScopedProxy 方法爲 bean 的類創建一個代理類,併爲該代理類創建 BeanDefinition ,源碼如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

注意看圖中畫線的代碼,該方法會創建一個新的 BeanDefinition ,該 BeanDefinition 的 bean 類型爲 ScopedProxyFactoryBean ,並且爲該 bean 注入屬性 targetBeanName , targetBeanName 爲目標 bean 的 beanName ,最後返回該 BeanDefinition 。

截圖中少了部分代碼,原來的 BeanDefinition 在該方法的後面會註冊到 bean 工廠,但使用的是 getTargetBeanName 方法返回的 beanName ,就是將原來的 beanName 加上前綴 scopedTarget. 。也就是說原來的 BeanDefinition 被換了個名稱註冊到 bean 工廠了, beanName 爲 scopedTarget.[原來的beanName] 。

ScopedProxyFactoryBean 是一個 FactoryBean<?> ,所以我們重點關注它的 getObject 方法返回的代理對象。 ScopedProxyFactoryBean 的 getObject 方法源碼如下。

public class ScopedProxyFactoryBean extends ProxyConfig
		implements FactoryBean<Object>, 
		BeanFactoryAware, AopInfrastructureBean {
    @Override
	public Object getObject() {
		return this.proxy;
	}
}

getObject 方法返回 this.proxy ,這個 proxy 是什麼時候創建的?

前面我們查看 cglib 生成的代理類發現其實現了一個 Advised 接口,這個 Advised 接口有一個 getTargetSource 方法。

public interface Advised extends TargetClassAware {
    TargetSource getTargetSource();
    // 其它省略
}

我們在 ScopedProxyFactoryBean 類中也發現一個 TargetSource , TargetSource 是一個接口,其中有一個 getTarget 方法我們要重點關注。

public interface TargetSource extends TargetClassAware {
    Object getTarget() throws Exception;
    // 其它省略
}

ScopedProxyFactoryBean 類的 TargetSource 字段類型爲 SimpleBeanTargetSource 。

public class ScopedProxyFactoryBean extends ProxyConfig
		implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {

	private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
	private String targetBeanName;
	
	public void setTargetBeanName(String targetBeanName) {
		this.targetBeanName = targetBeanName;
		this.scopedTargetSource.setTargetBeanName(targetBeanName);
	}
}

SimpleBeanTargetSource 的源碼如下:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
	@Override
	public Object getTarget() throws Exception {
		return getBeanFactory().getBean(getTargetBeanName());
	}
}

SimpleBeanTargetSource 的 getTarget 方法返回一個從 bean 工廠中根據目標 beanName 獲取的 bean ,這跟我們的猜想很符合,我們繼續關注這個 SimpleBeanTargetSource 是怎麼被使用的。

ScopedProxyFactoryBean 實現 BeanFactoryAware 接口, xxxAware 接口的方法在 bean 被實例化且注入屬性完成之後,在調用 bean 的初始化方法之前被調用,代理對象實際是在 setBeanFactory 方法中創建的。 setBeanFactory 方法源碼如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

通過 ProxyFactory 代理工廠創建的代理類都會實現 Advised 接口,使用 cglib 生成的代理類我們也已經看過了。

所以,當代理對象的 getXxx 方法被調用時,會被方法攔截器攔截,然後走切面邏輯。那麼我們就可以通過在方法攔截器的 invoke 方法或者通知方法( AOP 的“通知”)中調用代理對象的 getTargetSource 方法獲取 ScopedProxyFactoryBean 的 setBeanFactory 方法中爲代理對象注入的 TargetSource 對象,然後調用 TargetSource 對象的 getTarget 方法從 bean 工廠中獲取目標 bean ,再通過反射調用目標 bean 的 getXxx 方法。通過這種方式是可以實現動態配置的,這離我們的猜測已經很接近了。

前面分析了這麼多的代碼還只是 Spring 的源碼,要想證實假設,我們還需要分析 Spring Cloud 實現動態配置的源碼。源碼在 spring-cloud-context 模塊的 autoconfigure 包下,如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

RefreshAutoConfiguration 類就是自動配置 Spring Cloud 動態配置的配置類,這個配置類會往容器中注入兩個與實現動態配置密切相關的 bean 。

// 非完整代碼
public class RefreshAutoConfiguration {

    @Bean
	@ConditionalOnMissingBean(RefreshScope.class)
	public static RefreshScope refreshScope() {
		return new RefreshScope();
	}

    @Bean
	@ConditionalOnMissingBean
	public ContextRefresher contextRefresher(ConfigurableApplicationContext context,
			RefreshScope scope) {
		return new ContextRefresher(context, scope);
	}
}

RefreshScope 與 ContextRefresher 是 Spring Cloud 實現動態配置的兩個關鍵類。

Spring Cloud動態配置實現原理與源碼分析

 

 

  • ContextRefresher :負責刷新環境 Environment ;
  • RefreshScope :負責銷燬 @RefreshScope 聲明的動態配置 bean ,即調用 bean 生命週期的銷燬方法;

Spring Cloud 負責更新環境 Environment 以及創建新的動態配置 bean ,而判斷配置是否改變,以及怎麼獲取新的配置則是由第三方框架實現的,如 nacos 。

假設我們自己實現接入註冊中心,使用 mysql 作爲註冊中心,那麼我們需要做的就是定時從 mysql 查詢配置,然後對比配置有沒有改變,如果改變了,那就調用 ContextRefresher 的 refresh 方法,其它的就可以交由 Spring Cloud 去完成。

ContextRefresher 的 refresh 方法實現更新環境 Environment ,並調用 RefreshScope 的 refreshAll 方法使舊的動態配置 bean 無效。 refresh 方法的源碼如下:

public class ContextRefresher {
    public synchronized Set<String> refresh() {
        // 更新環境`Environment`
		Set<String> keys = refreshEnvironment();
		// 調用`RefreshScope`的`refreshAll`方法
		this.scope.refreshAll();
		return keys;
	}
}

refreshEnvironment 方法的實現比較複雜,我們不展開分析。 refreshEnvironment 方法通過創建一個新的 ConfigurableApplicationContext 去獲取新的 Environment ,然後將新的 Environment 的 PropertySource<?> 替換當前 Environment 的,這樣就實現了環境刷新。但由於是通過創建一個新的 ConfigurableApplicationContext 方式加載新的配置,所以 refreshEnvironment 方法的執行會很耗時,不過這種方式也確實巧妙。

refreshEnvironment 更新完 Environment 後會發送一個 EnvironmentChangeEvent 事件,該事件會攜帶更新的配置項的 key 。

如果是監聽 EnvironmentChangeEvent 事件感知配置改變,那麼我們需要注意,在監聽到 EnvironmentChangeEvent 事件時,調用動態配置 bean 的代理對象的 getXxx 方法獲取到的字段的值還是舊的,因爲 RefreshScope 的 refreshAll 方法還沒有被調用。

你可能會有疑問,被 @RefreshScope 聲明的 bean 不是單例的嗎?是因爲緩存, RefreshScope 會緩存動態配置 bean ,避免每調用一個 getXxx 方法都創建一個新的動態配置 bean 。

RefreshScope 類與前面分析的 ScopedProxyFactoryBean 類還有一層關係。 RefreshScope 繼承 GenericScope ,而 GenericScope 實現了 BeanDefinitionRegistryPostProcessor 接口, postProcessBeanDefinitionRegistry 方法的源碼如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

postProcessBeanDefinitionRegistry 方法將所有的 scope 爲 refresh 且 bean 類型爲 ScopedProxyFactoryBean 的 BeanDefinition 都找出來,並且將 bean 類型全部替換爲 LockedScopedProxyFactoryBean 。 LockedScopedProxyFactoryBean 是 ScopedProxyFactoryBean 的子類,重寫了 setBeanFactory 方法,源碼如下。

public static class LockedScopedProxyFactoryBean<S extends GenericScope>
			extends ScopedProxyFactoryBean implements MethodInterceptor {
	
	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		super.setBeanFactory(beanFactory);
		Object proxy = getObject();
		if (proxy instanceof Advised) {
			Advised advised = (Advised) proxy;
			advised.addAdvice(0, this);
		}
	}
	// .....
}

setBeanFactory 方法調用父類的 setBeanFactory 方法完成代理對象的創建。

LockedScopedProxyFactoryBean 還實現了 MethodInterceptor 接口,所以 LockedScopedProxyFactoryBean 還是一個方法攔截器。 MethodInterceptor 的 invoke 方法會優先 Advised 被調用。 LockedScopedProxyFactoryBean 的 invoke 方法的源碼如下圖所示。

Spring Cloud動態配置實現原理與源碼分析

 

 

invoke 方法首先獲取代理對象,然後通過反射調用目標方法,而在調用目標方法時,傳入的目標對象是通過代理對象的 TargetSource 獲取的,也就是從 bean 工廠中根據目標 beanName 獲取的。

RefreshScope 的 refreshAll 源碼如下:

public class RefreshScope extends GenericScope implements ApplicationContextAware,
		ApplicationListener<ContextRefreshedEvent>, Ordered {
    public void refreshAll() {
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}
}

refreshAll 調用 destroy 方法“銷燬”舊的動態配置 bean ,然後發送一個 RefreshScopeRefreshedEvent 事件,如果監聽 RefreshScopeRefreshedEvent 事件實現感知配置改變,那麼在監聽到 RefreshScopeRefreshedEvent 事件時,就可以調用動態配置 bean 的代理對象的 getXxx 方法獲取最新的配置。

RefreshScope 的 refreshAll 方法並非真的銷燬 bean ,也沒有調用 bean 的生命週期的銷燬方法,只是清空下緩存的 bean 。

RefreshScope 的 refreshAll 方法執行後,當動態配置 bean 的代理對象的 getXxx 方法下一次被調用時,先取得代理對象的 TargetSource 對象,再調用 TargetSource 對象的 getTarget 方法獲取目標 bean ,最後反射調用目標 bean 的 getXxx 方法。由於緩存已經不存在,調用 TargetSource 對象的 getTarget 方法就會從 bean 工廠中獲取,就會創建新的動態配置 bean ,而在創建新的 bean 時,在實例化 bean 以及完成屬性注入之後,在調用 bean 的初始化方法之前,會調用一些 BeanPostProcessor 爲 bean 加工,而爲 @ConfigurationProperties 註解聲明的 bean 的屬性賦值的工作則由 ConfigurationPropertiesBindingPostProcessor 完成。

ConfigurationPropertiesBindingPostProcessor 從 Environment 中獲取配置通過反射賦值給 bean 的字段。

總結,回答兩個問題

Spring Cloud 動態配置的實現原理我們已經從分析源碼的過程中瞭解,如果看懂源碼分析部分,那麼文章前面提到的兩個問題也就有了答案。

第一個問題:爲什麼使用 @RefreshScope 註解就能實現動態刷新配置?

使用 @RefreshScope 註解聲明的 bean ,其 scope 爲 refresh ,每次從 bean 工廠拿這類 bean 都會是一個新的 bean 。

第二個問題:爲什麼調用代理對象的 get 方法就能獲取到新的配置,以及當配置改變時 Spring Cloud 的實現是將動態配置 Bean 銷燬再創建新的 Bean 這句怎麼理解?

這與 bean 的生命週期有關, bean 中的字段只會在 bean 創建階段賦值一次,後續不會改變,如果引用的是代理對象,那麼當調用代理對象的方法時,方法攔截器先從代理對象拿到 TargetSource ,然後調用 TargetSource 對象的 getTarget 方法從 bean 工廠獲取目標 bean ,最後再通過反射調用目標 bean 的方法,以此實現 bean 的動態更新。

Spring Cloud 的實現並非真的將動態配置 Bean 銷燬,而是清除爲提升性能所緩存的動態配置 Bean 。當配置改變時,清除緩存後,下次就會從 Bean 工廠獲取新的 Bean 。 Spring 在創建 Bean 時,由 ConfigurationPropertiesBindingPostProcessor 這個 BeanPostProcessor 從 Environment 中獲取配置通過反射賦值給 bean 的字段。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章