這類註解都不知道,還好意思說會Spring Boot ?
- 2020 年 10 月 14 日
- 筆記
- JAVA, springboot, 碼猿技術專欄
前言
不知道大家在使用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中演變的註解很多,需要著重了解,特別是後期框架整合的時候會大量涉及。
