Spring 中 @EnableXXX 註解的套路

mghio發表於2022-05-22

前言

在 Spring 框架中有很多實用的功能,不需要寫大量的配置程式碼,只需新增幾個註解即可開啟。 其中一個重要原因是那些 @EnableXXX 註解,它可以讓你通過在配置類加上簡單的註解來快速地開啟諸如事務管理(@EnableTransactionManagement)、Spring MVC(@EnableWebMvc)或定時任務(@EnableScheduling)等功能。這些看起來簡單的註解語句提供了很多功能,但它們的內部機制從表面上看卻不太明顯。 一方面,對於使用者來說用這麼少的程式碼獲得這麼多實用的功能是很好的,但另一方面,如果你不瞭解某個東西的內部是如何工作的,就會使除錯和解決問題更加困難。

設計目標

Spring 框架中那些 @EnableXXX 註解的設計目標是允許使用者用最少的程式碼來開啟複雜使用的功能。 此外,使用者必須能夠使用簡單的預設值,或者允許手動配置該程式碼。最後,程式碼的複雜性要向框架使用者隱藏掉。 簡而言之,讓使用者設定大量的 Bean,並選擇性地配置它們,而不必知道這些 Bean 的細節(或真正被設定的內容)。下面來看看具體的幾個例子:

@EnableScheduling (匯入一個 @Configuration 類)

首先要知道的是,@EnableXXX 註解並不神奇。實際上在 BeanFactory 中並不知道這些註解的具體內容,而且在 BeanFactory 類中,核心功能和特定註解(如 @EnableWebMvc)或它們所存放的 jar 包(如 spring-web)之間沒有任何依賴關係。 讓我們看一下 @EnableScheduling,下面看看它是如何工作的。 定義一個 SchedulingConfig 配置類,如下所示:

@Configuration
@EnableScheduling
public class SchedulingConfig {
  // some bean in here
}

上面的內容沒有什麼特別之處。只是一個用 @EnableScheduling 註釋的標準 Java 配置。@EnableScheduling 讓你以設定的頻率執行某些方法。例如,你可以每 10 分鐘執行 BankService.transferMoneyToMghio()。 @EnableScheduling 註解原始碼如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

上面的 EnableScheduling 註解,我們可以看到它只是一個標準的類級註解(@Target/@Retention),應該包含在 JavaDocs 中(@Documented),但是它有一個 Spring 特有的註解(@Import)。 @Import 是將一切聯絡起來的關鍵。 在這種情況下,由於我們的 SchedulingConfig 被註解為 @EnableScheduling,當 BeanFactory 解析檔案時(內部是ConfigurationClassPostProcessor 在解析它),它也會發現 @Import(SchedulingConfiguration.class) 註解,它將匯入該值中定義的類。 在這個註解中,就是 SchedulingConfiguration。

這裡匯入是什麼意思呢?在這種情況下,它只是被當作另一個 Spring Bean。 SchedulingConfiguration 實際上被註解為@Configuration,所以 BeanFactory 會把它看作是另一個配置類,所有在該類中定義的 Bean 都會被拉入你的應用上下文,就像你自己定義了另一個 @Configuration 類一樣。 如果我們檢查 SchedulingConfiguration,我們可以看到它只定義了一個Bean(一個Post Processor),它負責我們上面描述的排程工作,原始碼如下:

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

    @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }

}

也許你會問,如果想配置 SchedulingConfiguration 中定義的 bean 呢? 這裡也只是在處理普通的Bean。 所以你對其它 Bean 所使用的機制也適用於此。 在這種情況下,ScheduledAnnotationBeanPostProcessor 使用一個標準的 Spring Bean 生命週期(postProcessAfterInitialization)來發現應用程式上下文何時被重新整理。 當符合條件時,它會檢查是否有任何 Bean 實現了 SchedulingConfigurer,如果有,就使用這些 Bean 來配置自己。 其實這一點並不明細(在 IDE 中也不太容易找到),但它與 BeanFactory 是完全分離的,而且是一個相當常見的模式,一個 Bean 被用來配置另一個 Bean。 而現在我們可以把所有的點連線起來,它(在某種程度上)很容易找到(你可以 Google 一下文件或閱讀一下 JavaDocs)。

@EnableTransactionManagement(匯入一個 ImportSelector)

在上一個示例中,我們討論了像 @EnableScheduling 這樣的註解如何使用 @Import 來匯入另一個 @Configuration 類並使其所有的 Bean 對你的應用程式可用(和可配置)。但是如果你想根據某些配置載入不同的 Bean 集,會發生什麼呢? @EnableTransactionManagement 就是一個很好的例子。TransactioConfig 定義如下:

@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class TransactioConfig {
    // some bean in here
} 

再一次,上面沒有什麼特別之處。只是一個用@EnableTransactionManagement註釋的標準Java配置。唯一與之前的例子有些不同的是,使用者為註釋指定了一個引數(mode=AdviceMode.ASPECTJ)。 @EnableTransactionManagement註解本身看起來像這樣。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default Ordered.LOWEST_PRECEDENCE;
}

和前面一樣,一個相當標準的註解,儘管這次它有一些引數。 然而,正如前文提到,@Import 註解是將一切聯絡在一起的關鍵,這一點再次得到證實。 但區別在於,這次我們匯入的是 TransactionManagementConfigurationSelector 這個類,通過原始碼可以發現,其實它不是一個被 @Configuration 註解的類。 TransactionManagementConfigurationSelector 是一個實現ImportSelector 的類。 ImportSelector 的目的是讓你的程式碼選擇在執行時載入哪些配置類。 它有一個方法,接收關於註解的一些後設資料,並返回一個類名陣列。 在這種情況下,TransactionManagementConfigurationSelector 會檢視模式並根據模式返回一些類。其中的 selectImports 方法原始碼如下:

@Override
protected String[] selectImports(AdviceMode adviceMode) {
 switch (adviceMode) {
   case PROXY:
     return new String[] {AutoProxyRegistrar.class.getName(),
                          ProxyTransactionManagementConfiguration.class.getName()};
   case ASPECTJ:
     return new String[] {determineTransactionAspectClass()};
   default:
     return null;
 }
}

這些類中的大多數是 @Configuration(例如 ProxyTransactionManagementConfiguration),通過前文介紹我們知道它們會像前面一樣工作。 對於 @Configuration 類,它們被載入和配置的方式與我們之前看到的完全一樣。 所以簡而言之,我們可以使用 @Import 和 @Configuration 類來載入一套標準的 Bean,或者使用 @Import 和 ImportSelector 來載入一套在執行時決定的 Bean。

@EnableAspectJAutoProxy (在 Bean 定義層匯入)

@Import 支援的最後一種情況,即當你想直接處理 BeanRegistry(工廠)時。如果你需要操作Bean Factory或者在Bean定義層處理Bean,那麼這種情況就適合你,它與上面的情況非常相似。 你的 AspectJProxyConfig 可能看起來像。

@Configuration
@EnableAspectJAutoProxy 
public class AspectJProxyConfig {
  // some bean in here
}

再一次,上面定義沒有什麼特別的東西。只是一個用 @EnableAspectJAutoProxy 註釋的標準 Java 配置。 下面是@EnableAspectJAutoProxy 的原始碼。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

    boolean proxyTargetClass() default false;

    boolean exposeProxy() default false;
}

和前面一樣,@Import 是關鍵,但這次它指向 AspectJAutoProxyRegistrar,它既沒有 @Configuration 註解,也沒有實現 ImportSelector 介面。 這次使用的是實現了 ImportBeanDefinitionRegistrar。 這個介面提供了對 Bean 註冊中心(Bean Registry)和註解後設資料的訪問,因此我們可以在執行時根據註解中的引數來操作 Bean 登錄檔。 如果你仔細看過前面的示例,你可以看到我們忽略的類也是 ImportBeanDefinitionRegistrar。 在 @Configuration 類不夠用的時候,這些類會直接操作 BeanFactory。

所以現在我們已經涵蓋了 @EnableXXX 註解使用 @Import 將各種 Bean 引入你的應用上下文的所有不同方式。 它們要麼直接引入一組 @Configuration 類,這些類中的所有 Bean 都被匯入到你的應用上下文中。 或者它們引入一個 ImportSelector 介面實現類,在執行時選擇一組 @Configuration 類並將這些 Bean 匯入到你的應用上下文中。 最後,他們引入一個ImportBeanDefinitionRegistrars,可以直接與 BeanFactory 在 BeanDefinition 級別上合作。

結論

總的來說,個人認為這種將 Bean 匯入應用上下文的方法很好,因為它使框架使用者的使用某個功能非常容易。不幸的是,它模糊瞭如何找到可用的選項以及如何配置它們。 此外,它沒有直接利用 IDE 的優勢,所以很難知道哪些 Bean 正在被建立(以及為什麼)。 然而,現在我們知道了 @Import 註解,我們可以使用 IDE 來挖掘一下每個註解及其相關的配置類,並瞭解哪些 Bean 正在被建立,它們如何被新增到你的應用上下文中,以及如何配置它們。 希望對你有幫助~

相關文章