前言
不知道大家在使用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中的兩個參數如下:
-
context:條件上下文,ConditionContext接口類型的,可以用來獲取容器中上下文信息。 -
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?
舉個栗子:假設有這樣一個需求,需要根據運行環境注入不同的Bean,Windows環境和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。
條件判斷在什么時候執行?
條件判斷的執行分為兩個階段,如下:
-
配置類解析階段(
ConfigurationPhase.PARSE_CONFIGURATION):在這個階段會得到一批配置類的信息和一些需要注冊的Bean。 -
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 { }
上述例子會依次按照Condition1、Condition2、Condition3執行。
默認按照先后順序執行,但是當我們需要指定順序呢?很簡單,有如下三種方式:
-
實現 PriorityOrdered接口,指定優先級 -
實現 Ordered接口接口,指定優先級 -
使用 @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中大量使用了這些注解,常見的注解如下:
-
@ConditionalOnBean:當容器中有指定Bean的條件下進行實例化。 -
@ConditionalOnMissingBean:當容器里沒有指定Bean的條件下進行實例化。 -
@ConditionalOnClass:當classpath類路徑下有指定類的條件下進行實例化。 -
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下進行實例化。 -
@ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。 -
@ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。 -
@ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。 -
@ConditionalOnExpression:基於SpEL表達式的條件判斷。 -
@ConditionalOnJava:當JVM版本為指定的版本范圍時觸發實例化。 -
@ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。 -
@ConditionalOnJndi:在JNDI存在的條件下觸發實例化。 -
@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中演變的注解很多,需要着重了解,特別是后期框架整合的時候會大量涉及。
