SpringBoot的@Conditional使用 - reflectoring

banq發表於2019-03-18

在開發Spring Boot應用程式時,如果滿足某些條件,我們有時只想將bean或模組載入到應用程式上下文中。然後在測試期間禁用某些bean,或者在執行時環境中對某個屬性做出反應。
Spring引入了@Conditional註釋,允許我們定義自定義條件以應用於應用程式上下文的各個部分。Spring Boot構建於此之上,並提供一些預定義的條件,因此我們不必自己實現它們。
在本教程中,我們將看一些用例,解釋為什麼我們需要條件載入的bean。然後,我們將看到如何應用條件以及Spring Boot提供的條件。為了解決問題,我們還將實現自定義條件。

為什麼我們需要有條件的豆?
Spring應用程式上下文包含一個物件圖,它構成了我們的應用程式在執行時需要的所有bean。Spring的@Conditional註釋允許我們定義將某個bean包含在該物件圖中的條件。
為什麼我們需要在某些條件下包含或排除bean?
根據我的經驗,最常見的用例是某些bean在測試環境中不起作用。它們可能需要連線到遠端系統或測試期間不可用的應用程式伺服器。因此,我們希望模組化我們的測試 以在測試期間排除或替換這些bean。
另一個用例是我們想要啟用或禁用某個跨領域的問題。想象一下,我們已經構建了一個配置安全性的模組。在開發人員測試期間,我們不希望每次都輸入我們的使用者名稱和密碼,因此我們使用一個開關並禁用整個安全模組進行本地測試。
此外,我們可能只想在某些外部資源可用時才載入某些bean ,否則它們將無法工作。例如,我們只想logback.xml在類路徑中找到檔案時配置我們的Logback記錄器。
我們將在下面的討論中看到更多用例。

定義有條件的Bean
在我們定義Spring bean的任何地方,我們都可以選擇新增條件。只有滿足此條件,才會將bean新增到應用程式上下文中。要宣告條件,我們可以使用下面@Conditional...描述的任何註釋。
但首先,讓我們看一下如何將條件應用於某個Spring bean。

如果我們向單個@Bean定義新增條件,則僅在滿足條件時才載入此bean:

@Configuration
class ConditionalBeanConfiguration {

  @Bean
  @Conditional... // <--
  ConditionalBean conditionalBean(){
    return new ConditionalBean();
  };
}

如果我們向Spring新增一個條件@Configuration,那麼只有在滿足條件時才會載入此配置中包含的所有bean:

@Configuration
@Conditional... // <--
class ConditionalConfiguration {
  
  @Bean
  Bean bean(){
    ...
  };
  
}


我們可以新增一個條件到@Component,@Service,@Repository,或@Controller:

@Component
@Conditional... // <--
class ConditionalComponent {
}


預先定義的條件
Spring Boot提供了一些@ConditionalOn...我們可以開箱即用的預定義註釋。讓我們依次看看每一個。

@ConditionalOnProperty
根據我的經驗,@ConditionalOnProperty註釋是Spring Boot專案中最常用的條件註釋。它允許根據特定的環境屬性有條件地載入bean:

@Configuration
@ConditionalOnProperty(
    value="module.enabled", 
    havingValue = "true", 
    matchIfMissing = true)
class CrossCuttingConcernModule {
  ...
}

這個CrossCuttingConcernModule只載入module.enabled屬性取值為true的Bean。如果沒有設定該屬性,它仍將被載入,因為我們已定義matchIfMissing 為true。這樣,我們建立了一個預設載入的模組,直到我們另行決定。
同樣地,我們可能會建立其他模組來解決我們可能希望在某個(測試)環境中禁用的安全性或排程等交叉問題。

@ConditionalOnExpression
如果我們有基於多個屬性的更復雜的條件,我們可以使用@ConditionalOnExpression:

@Configuration
@ConditionalOnExpression(
    "${module.enabled:true} and ${module.submodule.enabled:true}"
)
class SubModule {
  ...
}

如果module.enabled和module.submodule.enabled 都具價值true,則載入。透過附加:true到屬性,我們告訴Spring true 在未設定屬性的情況下將其用作預設值。我們可以使用Spring Expression Language的完整擴充套件。
這樣,我們可以建立子模組,如果父模組被禁用,則應該禁用這些子模組,但如果啟用了父模組,也可以禁用子模組。

@ConditionalOnBean
有時,我們可能只想在應用程式上下文中某個其他bean可用時才載入bean:

@Configuration
@ConditionalOnBean(OtherModule.class)
class DependantModule {
  ...
}

DependantModule 只有在上下文存在OtherModule 時才載入。
我們也可以定義bean名稱而不是bean類。
這樣,我們可以定義某些模組之間的依賴關係。僅當另一個模組的某個bean可用時才載入一個模組。

@ConditionalOnMissingBean
類似地,如果我們只想在某個其他bean 不在應用程式上下文中時載入bean ,我們就可以使用@ConditionalOnMissingBean:

@Configuration
class OnMissingBeanModule {

  @Bean
  @ConditionalOnMissingBean
  DataSource dataSource() {
    return new InMemoryDataSource();
  }
}

在此示例中,如果還沒有可用的資料來源,我們只會將記憶體中的資料來源注入應用程式上下文。這與Spring Boot在內部提供的測試上下文中的記憶體資料庫非常相似。

@ConditionalOnResource
如果我們想根據類路徑上某個資源可用的事實載入bean,我們可以使用@ConditionalOnResource:

@Configuration
@ConditionalOnResource(resources = "/logback.xml")
class LogbackModule {
  ...
}

如果在類路徑中配置了logback檔案就載入LogbackModule。這樣,我們可能會建立類似的模組,只有在找到相應的配置檔案時才會載入這些模組。

其他條件
上面描述的條件註釋是我們可能在任何Spring Boot應用程式中使用的更常見的註釋。Spring Boot提供了更多的條件註釋。但是,它們並不常見,有些更適合框架開發而不是應用程式開發(Spring Boot大量使用它們)。所以,我們在這裡只是簡單地看一下它們。

@ConditionalOnClass:僅當類路徑上有某個類時才載入bean:

@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
class OnClassModule {
  ...
}


@ConditionalOnMissingClass:僅當某個類不在類路徑上時才載入bean :

@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
class OnMissingClassModule {
  ...
}


@ConditionalOnJndi:僅當透過JNDI提供某個資源時才載入bean:

@Configuration
@ConditionalOnJndi("java:comp/env/foo")
class OnJndiModule {
  ...
}


@ConditionalOnJava:僅在執行特定版本的Java時載入bean:

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class OnJavaModule {
  ...
}


@ConditionalOnSingleCandidate:類似於@ConditionalOnBean,但只有在確定了給定bean類的單個候選項時才會載入bean。可能沒有自動配置之外的用例:

@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class OnSingleCandidateModule {
  ...
}


@ConditionalOnWebApplication:僅當我們在Web應用程式中執行時才載入bean:

@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
  ...
}


@ConditionalOnNotWebApplication:僅當我們沒有在Web應用程式中執行時才載入bean :

@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
  ...
}


@ConditionalOnCloudPlatform:僅當我們在某個雲平臺上執行時才載入bean:

@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
class OnCloudPlatformModule {
  ...
}


自定義條件
除了條件註釋,我們可以建立自己的註釋,並將多個條件與邏輯運算子組合在一起。
想象一下,我們有一些Spring bean本身可以與作業系統對話。只有在我們在相應的作業系統上執行應用程式時才應載入這些bean。
讓我們實現一個條件,只有當我們在unix機器上執行程式碼時才載入bean。為此,我們實現了Spring的Condition 介面:

class OnUnixCondition implements Condition {

  @Override
    public boolean matches(
        ConditionContext context, 
        AnnotatedTypeMetadata metadata) {
        return SystemUtils.IS_OS_LINUX;
    }
}


我們只是使用Apache Commons的SystemUtils類來確定我們是否在類似unix的系統上執行。如果需要,我們可以包含更復雜的邏輯,它使用有關當前應用程式上下文(ConditionContext)或有關注釋類(AnnotatedTypeMetadata)的資訊。
現在可以將條件與Spring的@Conditional註釋結合使用了:

@Bean
@Conditional(OnUnixCondition.class)
UnixBean unixBean() {
  return new UnixBean();
}


將條件與OR結合:
如果我們想要將多個條件與邏輯“OR”運算子組合成一個條件,我們可以擴充套件AnyNestedCondition:

class OnWindowsOrUnixCondition extends AnyNestedCondition {

  OnWindowsOrUnixCondition() {
    super(ConfigurationPhase.REGISTER_BEAN);
  }

  @Conditional(OnWindowsCondition.class)
  static class OnWindows {}

  @Conditional(OnUnixCondition.class)
  static class OnUnix {}

}

在這裡,我們建立了一個條件,如果應用程式在Windows或unix上執行,則滿足該條件。
在AnyNestedCondition父類將評估@Conditional的方法說明和使用OR運算子將它們結合起來。
我們可以像任何其他條件一樣使用這個條件:

@Bean
@Conditional(OnWindowsOrUnixCondition.class)
WindowsOrUnixBean windowsOrUnixBean() {
  return new WindowsOrUnixBean();
}


注:你AnyNestedCondition還是AllNestedConditions不工作?
檢查ConfigurationPhase傳入的引數super()。如果要將組合條件應用於@Configurationbean,請使用該值 PARSE_CONFIGURATION。如果要將條件應用於簡單bean,請使用REGISTER_BEAN上面的示例中所示。Spring Boot需要進行區分,以便它可以在應用程式上下文啟動期間的適當時間應用條件。


將條件與AND結合起來:
如果我們想要將條件與“AND”邏輯結合起來,我們可以簡單地@Conditional...在單個bean上使用多個 註釋。它們將自動與邏輯“AND”運算子組合,這樣如果至少有一個條件失敗,則不會載入bean:

@Bean
@ConditionalOnUnix
@Conditional(OnWindowsCondition.class)
WindowsAndUnixBean windowsAndUnixBean() {
  return new WindowsAndUnixBean();
}

這個bean永遠不應該載入,除非有人建立了我不知道的Windows / Unix混合。
請注意,@Conditional註釋不能在單個方法或類上多次使用。因此,如果我們想以這種方式組合多個註釋,我們必須使用@ConditionalOn...沒有此限制的自定義註釋。下面,我們將探討如何建立@ConditionalOnUnix註釋。
或者,如果我們想將條件與AND組合成一個 @Conditional註釋,我們可以擴充套件Spring Boot的AllNestedConditions 類,其工作方式與AnyNestedConditions上述完全相同。

結合條件與NOT:
與AnyNestedCondition和類似AllNestedConditions,NoneNestedCondition如果組合條件中的NONE匹配,我們可以擴充套件到僅載入bean。

定義定製的@ ConditionalOn ...註釋
我們可以為任何條件建立自定義註釋。我們只需要使用以下方法對此註釋進行元註釋@Conditional:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnUnix {}


當我們用新的註釋註釋bean時,Spring將評估使用這個元註釋:

@Bean
@ConditionalOnUnix
LinuxBean linuxBean(){
  return new LinuxBean();
}


結論
透過@Conditional註釋和建立自定義@Conditional... 註釋的可能性,Spring已經為我們提供了很多控制應用程式上下文內容的能力。
春天引導建立在最重要的是透過將一些方便的@ConditionalOn...註解表,並透過允許我們使用條件相結合AllNestedConditions,AnyNestedCondition或NoneNestedCondition。這些工具允許我們模組化我們的生產程式碼以及我們的測試
然而,權力是責任,所以我們應該注意不要在條件下亂丟我們的應用程式上下文,以免我們忘記何時載入。
本文的程式碼可以在github上找到

相關文章