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

愛撒謊的男孩發表於2020-10-14

前言

不知道大家在使用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中演變的註解很多,需要著重瞭解,特別是後期框架整合的時候會大量涉及。

相關文章