Spring @Profile註解使用和原始碼解析

2023-04-13 18:00:39

介紹

在之前的文章中,寫了一篇使用Spring @Profile實現開發環境,測試環境,生產環境的切換,之前的文章是使用SpringBoot專案搭建,實現了不同環境資料來源的切換,在我們實際開發中,會分為dev,test,prod等環境,他們之間數獨立的,今天進來詳解介紹Spring @Profile的原理。

# Spring註解@Profile實現開發環境,測試環境,生產環境的切換

使用

帶有@Profile的註解的bean的不會被註冊進IOC容器,需要為其設定環境變數啟用,才能註冊進IOC容器,如下通過setActiveProfiles設定了dev值,那麼這三個值所對應的Bean會被註冊進IOC容器。當然,我們在實際使用中,不會這樣去做,使用SpringBoot的話,我們一般是使用yml,在yml中設定spring.profiles.active,也可以通過設定jvm引數。

通過Environment設定profile

我們可以直接通過Environment來設定環境屬性,這是比較原生的方法。

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getEnvironment().setActiveProfiles("dev");

通過JVM引數設定

可以通過JVM引數來設定環境變數的值,在開發中,這種方式也是使用得比較普遍。

SpringBoot通過yml進行設定

在SpringBoot專案中,我們得設定項一般都是設定在yml檔案中,這樣就能和程式碼分開,並且也能進行動態設定。

從上面我們看出可以通過好幾種方式進行設定,但是他們最終其實都是將環境變數設定進Environment中,這樣,spring在後續得流程裡面,就能從Environment中獲取環境變數,然後進行相應的邏輯處理。

原始碼解析

BeanDefinition註冊

首先,需要註冊bean的元資訊BeanDefinition,不過對於@Profile標註的方法,如果環境變數中有對應的變數值,那麼就能註冊,沒有的話則不會進行註冊,我們來看關鍵的程式碼,在ConfigurationClassBeanDefinitionReader中,有一個shouldSkip判斷,它會篩選出符合的bean,不符合條件的bean則被加入skippedBeanMethods集合中,不會被註冊。

private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
	ConfigurationClass configClass = beanMethod.getConfigurationClass();
	MethodMetadata metadata = beanMethod.getMetadata();
	String methodName = metadata.getMethodName();
		// Do we need to mark the bean as skipped by its condition?
	if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
            configClass.skippedBeanMethods.add(methodName);
            return;
	}
            if (configClass.skippedBeanMethods.contains(methodName)) {
            return;
	}
}

shouldSkip原始碼

在shouldSkip中,會使用Condition介面,@Profile使用的是ProfileCondition,然後呼叫matches方法。

    public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationCondition.ConfigurationPhase phase) {
        for (Condition condition : conditions) {
            ConfigurationCondition.ConfigurationPhase requiredPhase = null;
            if (condition instanceof ConfigurationCondition configurationCondition) {
                requiredPhase = configurationCondition.getConfigurationPhase();
            }
            if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
                return true;
            }
        }
        return false;
    }

ProfileCondition匹配

在ProfileCondition的matches方法中,主要就是去Environment中尋找環境變數,然後解析@Profile註解設定的value值,如果Environment中啟用的設定中包含當前的設定,包含則能為true,不包含則為false,如上通過setActiveProfiles設定Environment中啟用的設定為dev,當前傳過來的設定為dev,那麼就能匹配上,就能裝配進IOC容器。

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

從原始碼可以看出,其最核心的思想就是是否註冊bean的元資訊BeanDefinition,因為只有註冊了BeanDefinition,後續才能為建立bean提供後設資料支援,判斷是否註冊bean元資訊,主要就是從Environment中取出profiles的值,然後和@Profile註解設定的值進行匹配,匹配得上就註冊,bean不上就不註冊。

總結

上面我們對@Profile的使用做了詳細的介紹,並對它的核心原始碼進行解剖,無非就是判斷是否要註冊BeanDefinition,如果我們需要做一些環境隔離的工作,使用@Profile還是比較不錯的。

今天的分享就到這裡,感謝你的觀看,下期見!