這類注解都不知道,還好意思說會Spring Boot ?


前言

不知道大家在使用Spring Boot開發的日常中有沒有用過@Conditionalxxx注解,比如@ConditionalOnMissingBean。相信看過Spring Boot源碼的朋友一定不陌生。

@Conditionalxxx這類注解表示某種判斷條件成立時才會執行相關操作。掌握該類注解,有助於日常開發,框架的搭建。

今天這篇文章就從前世今生介紹一下該類注解。

Spring Boot 版本

本文基於的Spring Boot的版本是2.3.4.RELEASE

@Conditional

@Conditional注解是從Spring4.0才有的,可以用在任何類型或者方法上面,通過@Conditional注解可以配置一些條件判斷,當所有條件都滿足的時候,被@Conditional標注的目標才會被Spring容器處理。

@Conditional的使用很廣,比如控制某個Bean是否需要注冊,在Spring Boot中的變形很多,比如@ConditionalOnMissingBean@ConditionalOnBean等等,如下:

該注解的源碼其實很簡單,只有一個屬性value,表示判斷的條件(一個或者多個),是org.springframework.context.annotation.Condition類型,源碼如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional {   /**  * All {@link Condition} classes that must {@linkplain Condition#matches match}  * in order for the component to be registered.  */  Class<? extends Condition>[] value(); } 

@Conditional注解實現的原理很簡單,就是通過org.springframework.context.annotation.Condition這個接口判斷是否應該執行操作。

Condition接口

@Conditional注解判斷條件與否取決於value屬性指定的Condition實現,其中有一個matches()方法,返回true表示條件成立,反之不成立,接口如下:

@FunctionalInterface
public interface Condition {  boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); } 

matches中的兩個參數如下:

  1. context:條件上下文, ConditionContext接口類型的,可以用來獲取容器中上下文信息。
  2. metadata:用來獲取被 @Conditional標注的對象上的所有注解信息

ConditionContext接口

這個接口很重要,能夠從中獲取Spring上下文的很多信息,比如ConfigurableListableBeanFactory,源碼如下:

public interface ConditionContext {
  /**  * 返回bean定義注冊器,可以通過注冊器獲取bean定義的各種配置信息  */  BeanDefinitionRegistry getRegistry();   /**  * 返回ConfigurableListableBeanFactory類型的bean工廠,相當於一個ioc容器對象  */  @Nullable  ConfigurableListableBeanFactory getBeanFactory();   /**  * 返回當前spring容器的環境配置信息對象  */  Environment getEnvironment();   /**  * 返回資源加載器  */  ResourceLoader getResourceLoader();   /**  * 返回類加載器  */  @Nullable  ClassLoader getClassLoader(); } 

如何自定義Condition?

舉個栗子:假設有這樣一個需求,需要根據運行環境注入不同的BeanWindows環境和Linux環境注入不同的Bean

實現很簡單,分別定義不同環境的判斷條件,實現org.springframework.context.annotation.Condition即可。

windows環境的判斷條件源碼如下

/**  * 操作系統的匹配條件,如果是windows系統,則返回true  */ public class WindowsCondition implements Condition {  @Override  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {  //獲取當前環境信息  Environment environment = conditionContext.getEnvironment();  //獲得當前系統名  String property = environment.getProperty("os.name");  //包含Windows則說明是windows系統,返回true  if (property.contains("Windows")){  return true;  }  return false;   } } 

Linux環境判斷源碼如下

/**  * 操作系統的匹配條件,如果是windows系統,則返回true  */ public class LinuxCondition implements Condition {  @Override  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {  Environment environment = conditionContext.getEnvironment();   String property = environment.getProperty("os.name");  if (property.contains("Linux")){  return true;  }  return false;   } } 

配置類中結合@Bean注入不同的Bean,如下

@Configuration
public class CustomConfig {   /**  * 在Windows環境下注入的Bean為winP  * @return  */  @Bean("winP")  @Conditional(value = {WindowsCondition.class})  public Person personWin(){  return new Person();  }   /**  * 在Linux環境下注入的Bean為LinuxP  * @return  */  @Bean("LinuxP")  @Conditional(value = {LinuxCondition.class})  public Person personLinux(){  return new Person();  } 

簡單的測試一下,如下

@SpringBootTest
class SpringbootInterceptApplicationTests {   @Autowired(required = false)  @Qualifier(value = "winP")  private Person winP;   @Autowired(required = false)  @Qualifier(value = "LinuxP")  private Person linP;   @Test  void contextLoads() {  System.out.println(winP);  System.out.println(linP);  } } 

Windows環境下執行單元測試,輸出如下

com.example.springbootintercept.domain.Person@885e7ff
null 

很顯然,判斷生效了,Windows環境下只注入了WINP

條件判斷在什么時候執行?

條件判斷的執行分為兩個階段,如下:

  1. 配置類解析階段(ConfigurationPhase.PARSE_CONFIGURATION):在這個階段會得到一批配置類的信息和一些需要注冊的Bean

  2. Bean注冊階段(ConfigurationPhase.REGISTER_BEAN):將配置類解析階段得到的配置類和需要注冊的Bean注入到容器中。

默認都是配置解析階段,其實也就夠用了,但是在Spring Boot中使用了ConfigurationCondition,這個接口可以自定義執行階段,比如@ConditionalOnMissingBean都是在Bean注冊階段執行,因為需要從容器中判斷Bean。

這個兩個階段有什么不同呢?:其實很簡單的,配置類解析階段只是將需要加載配置類和一些Bean(被@Conditional注解過濾掉之后)收集起來,而Bean注冊階段是將的收集來的Bean和配置類注入到容器中,如果在配置類解析階段執行Condition接口的matches()接口去判斷某些Bean是否存在IOC容器中,這個顯然是不行的,因為這些Bean還未注冊到容器中

什么是配置類,有哪些?:類上被@Component@ComponentScan@Import@ImportResource@Configuration標注的以及類中方法有@Bean的方法。如何判斷配置類,在源碼中有單獨的方法:org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate

ConfigurationCondition接口

這個接口相比於@Condition接口就多了一個getConfigurationPhase()方法,可以自定義執行階段。源碼如下:

public interface ConfigurationCondition extends Condition {
  /**  * 條件判斷的階段,是在解析配置類的時候過濾還是在創建bean的時候過濾  */  ConfigurationPhase getConfigurationPhase();    /**  * 表示階段的枚舉:2個值  */  enum ConfigurationPhase {   /**  * 配置類解析階段,如果條件為false,配置類將不會被解析  */  PARSE_CONFIGURATION,   /**  * bean注冊階段,如果為false,bean將不會被注冊  */  REGISTER_BEAN  } } 

這個接口在需要指定執行階段的時候可以實現,比如需要根據某個Bean是否在IOC容器中來注入指定的Bean,則需要指定執行階段為Bean的注冊階段ConfigurationPhase.REGISTER_BEAN)。

多個Condition的執行順序

@Conditional中的Condition判斷條件可以指定多個,默認是按照先后順序執行,如下:

class Condition1 implements Condition {
 @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  System.out.println(this.getClass().getName());  return true;  } }  class Condition2 implements Condition {  @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  System.out.println(this.getClass().getName());  return true;  } }  class Condition3 implements Condition {  @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  System.out.println(this.getClass().getName());  return true;  } }  @Configuration @Conditional({Condition1.class, Condition2.class, Condition3.class}) public class MainConfig5 { } 

上述例子會依次按照Condition1Condition2Condition3執行。

默認按照先后順序執行,但是當我們需要指定順序呢?很簡單,有如下三種方式:

  1. 實現 PriorityOrdered接口,指定優先級
  2. 實現 Ordered接口接口,指定優先級
  3. 使用 @Order注解來指定優先級

例子如下:

@Order(1) 
class Condition1 implements Condition {  @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  System.out.println(this.getClass().getName());  return true;  } }  class Condition2 implements Condition, Ordered {  @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  System.out.println(this.getClass().getName());  return true;  }   @Override  public int getOrder() {  return 0;  } }  class Condition3 implements Condition, PriorityOrdered {  @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  System.out.println(this.getClass().getName());  return true;  }   @Override  public int getOrder() {  return 1000;  } }  @Configuration @Conditional({Condition1.class, Condition2.class, Condition3.class}) public class MainConfig6 { } 

根據排序的規則,PriorityOrdered的會排在前面,然后會再按照order升序,最后可以順序是:Condtion3->Condtion2->Condtion1

Spring Boot中常用的一些注解

Spring Boot中大量使用了這些注解,常見的注解如下:

  1. @ConditionalOnBean:當容器中有指定Bean的條件下進行實例化。
  2. @ConditionalOnMissingBean:當容器里沒有指定Bean的條件下進行實例化。
  3. @ConditionalOnClass:當classpath類路徑下有指定類的條件下進行實例化。
  4. @ConditionalOnMissingClass:當類路徑下沒有指定類的條件下進行實例化。
  5. @ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。
  6. @ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。
  7. @ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。
  8. @ConditionalOnExpression:基於SpEL表達式的條件判斷。
  9. @ConditionalOnJava:當JVM版本為指定的版本范圍時觸發實例化。
  10. @ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。
  11. @ConditionalOnJndi:在JNDI存在的條件下觸發實例化。
  12. @ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發實例化。

比如在WEB模塊的自動配置類WebMvcAutoConfiguration下有這樣一段代碼:

    @Bean
 @ConditionalOnMissingBean  public InternalResourceViewResolver defaultViewResolver() {  InternalResourceViewResolver resolver = new InternalResourceViewResolver();  resolver.setPrefix(this.mvcProperties.getView().getPrefix());  resolver.setSuffix(this.mvcProperties.getView().getSuffix());  return resolver;  } 

常見的@Bean@ConditionalOnMissingBean注解結合使用,意思是當容器中沒有InternalResourceViewResolver這種類型的Bean才會注入。這樣寫有什么好處呢?好處很明顯,可以讓開發者自定義需要的視圖解析器,如果沒有自定義,則使用默認的,這就是Spring Boot為自定義配置提供的便利。

總結

@Conditional注解在Spring Boot中演變的注解很多,需要着重了解,特別是后期框架整合的時候會大量涉及。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM