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