簡介
@Conditional
是Spring4新提供的注解,它的作用是按照一定的條件進行判斷,滿足條件給容器注冊Bean。
作用
條件裝配,滿足Conditional
指定的條件,則進行組件注入
根注解與派生注解
@Conditional是一個根注解,這個根注解下面派生了許多派生注解
- @ConditionalOnCloudPlatform
- 不常用
- 當指定的雲平台處於活動狀態時匹配才能注入
- @ConditionalOnBean
- 常用
- 當容器中存在指定的Bean時,在執行指定操作
- @ConditionalOnMissingBean
- 常用
- 當容器中不存在指定的Bean時,才進行指定事情
- @ConditionalOnWarDeployment
- 不常用
- 當應用程序是傳統的 WAR 部署時匹配,進行某些事情;對於帶有嵌入式服務器的應用程序,此條件將返回 false
- @Profile
- 常用
- 當指定的配置文件處於活動狀態的時候(即當前程序使用了這個配置文件),才進行指定事情
- 與@Configuration一起使用,詳情參考Spring Boot 注解——@Configuration
- @ConditionalOnNotWebApplication
- 不常用
- 當應用程序不是 Web 應用程序時,執行某些操作。
- @ConditionalOnWebApplication
- 不常用
- 當應用程序是 Web 應用程序時,執行某些操作。 默認情況下,任何 Web 應用程序都會匹配,但可以使用type()屬性縮小范圍
- @ConditionalOnMissingClass
- 不常用(對於開發框架,應該會比較常用)
- 僅當指定的類不在類路徑上時才匹配。
- @ConditionalOnClass
- 不常用(對於開發框架,應該會比較常用)
- 僅當指定的類在類路徑上時才匹配。可以在@Configuration類上安全地指定value()因為在加載類之前使用 ASM 解析注釋元數據。 放置在@Bean方法上時需要格外小心,考慮在單獨的Configuration類中隔離條件,特別是如果方法的返回類型與target of the condition的target of the condition匹配
- 簡單來說就是,如果指定的類在項目中存在,就會執行指定操作,如果不存在,就不會執行
- @ConditionalOnExpression
- 不常用
- 取決於SpEL表達式值的條件元素的配置注釋。
- @ConditionalOnResource
- 常用
- 當且僅當項目中存在某個資源的時候,才執行相關操作
- @ConditionalOnRepositoryType
- 不常用
- 僅在啟用特定類型的 Spring Data 存儲庫時匹配
- @ConditionalOnProperty
- 常用
- 當指定的屬性上有特定的值時,才執行相關操作
- @ConditionalOnJava
- 不常用
- 根據運行應用程序的 JVM 版本匹配
- @ConditionalOnSingleCandidate
- 常用
- 僅當指定類的 bean 已包含在BeanFactory並且可以確定單個候選時才匹配。如果BeanFactory中已經包含多個匹配的 bean 實例但已經定義了一個主要候選者,則條件也將匹配; 本質上,如果自動裝配具有定義類型的 bean 將成功,則條件匹配。該條件只能匹配到目前為止已由應用程序上下文處理的 bean 定義,因此,強烈建議僅在自動配置類上使用此條件。 如果候選 bean 可能是由另一個自動配置創建的,請確保使用此條件的那個 bean 之后運行
- 簡單理解:表示當指定Bean在容器中只有一個,或者雖然有多個但是指定首選Bean。
- @ConditionalOnEnabledResourceChain
- 不常用
- 檢查是否啟用了 Spring 資源處理鏈。 如果ResourceProperties.Chain.getEnabled()為true或webjars-locator-core在類路徑上,則webjars-locator-core 。
常用注解源碼解析和具體使用方法
@Conditional
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* 必須匹配才能注冊組件的所有Conditions
*/
Class<? extends Condition>[] value();
}
Conditional
是一個根注解,一系列的判斷條件需要自己去實現,根據源碼可以看到,@Conditional
擁有一個value屬性,接收的參數類型是Class數組
,這個Class類型繼承Condition接口,也就是說這個條件判斷需要自己編程實現
@FunctionalInterface
public interface Condition {
/**
* 確定條件是否匹配。
* @param context 條件上下文
* @param metadata 被檢查的class或method的元數據
* @return true如果條件匹配並且組件可以被注冊,或false否決注釋組件的注冊
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
自己編寫的類,需要繼承Condition這個接口,如果matches返回的是true,則注入容器,反之,不注入
示例
- 普通實體類
public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
- 配置類
@Configuration
public class BeanConfig {
/**
* 如果WindowsCondition的實現方法返回true,則注入這個bean
*/
@Conditional({WindowsCondition.class})
@Bean(name = "bill")
public Person person1(){
return new Person("Bill Gates",62);
}
/**
* 如果LinuxCondition的實現方法返回true,則注入這個bean
*/
@Conditional({LinuxCondition.class})
@Bean("linus")
public Person person2(){
return new Person("Linus",48);
}
}
- 繼承Condition接口
public class WindowsCondition implements Condition {
/**
* @param conditionContext:判斷條件能使用的上下文環境
* @param annotatedTypeMetadata:注解所在位置的注釋信息
* */
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//獲取ioc使用的beanFactory
ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
//獲取類加載器
ClassLoader classLoader = conditionContext.getClassLoader();
//獲取當前環境信息
Environment environment = conditionContext.getEnvironment();
//獲取bean定義的注冊類
BeanDefinitionRegistry registry = conditionContext.getRegistry();
//獲得當前系統名
String property = environment.getProperty("os.name");
//包含Windows則說明是windows系統,返回true
if (property.contains("Windows")){
return true;
}
return false;
}
}
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("Linux")){
return true;
}
return false;
}
}
- 測試類
@Test
public void test1(){
String osName = applicationContext.getEnvironment().getProperty("os.name");
System.out.println("當前系統為:" + osName);
Map<String, Person> map = applicationContext.getBeansOfType(Person.class);
System.out.println(map);
}
運行結果如下:
@ConditionalOnBean
源碼分析
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 使用父注解@Conditional並自行實現Condition接口
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
/**
* 應該檢查的 bean 的類型。 當BeanFactory中包含所有指定類的 bean 時,條件匹配
* @return 要檢查的 bean 的類型
*/
Class<?>[] value() default {};
/**
* 應該檢查的 bean 的類型。 當BeanFactory中包含所有指定類的 bean 時,條件匹配
* @return @return 要檢查的 bean 的類型名稱
*/
String[] type() default {};
/**
* 裝飾應檢查的 bean 的注釋類型。 當在BeanFactory bean 上定義了所有指定的注釋時,條件匹配
* @return 要檢查的類級注釋類型
*/
Class<? extends Annotation>[] annotation() default {};
/**
* 要檢查的 bean 的名稱。 當所有指定的 bean 名稱都包含在BeanFactory中時,條件匹配
* @return 要檢查的 bean 的名稱
*/
String[] name() default {};
/**
* 決定是否應考慮應用程序上下文層次結構(父上下文)的策略
* @return the search strategy
*/
SearchStrategy search() default SearchStrategy.ALL;
/**
* 可能在其泛型參數中包含指定 bean 類型的其他類。
* 例如,聲明 value = Name.class和 parameterizedContainer = NameRegistration.class
* 將檢測Name和NameRegistration<Name>。
* @return 容器類型
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {};
}
以上就是@ConditionalOnBean
注解的具體實現,可以看的出,這個注解實際使用了父注解@Conditional
和一個處理類OnBeanCondition
,真正的判斷邏輯就在這個OnBeanCondition
中。
首先看這個類頭,OnBeanCondition
中並沒有實現Condition
,但是他繼承了一個叫FilteringSpringBootCondition
的類,在進入這個類看看
FilteringSpringBootCondition
這個類也沒有實現Condition
這個類,不過他也繼承了一個叫SpringBootsCondition
的類
進入SpringBootCondition
這個類后,終於實現了Condition
,實現Condition
這個接口,那必定實現這個matches
方法,下面我們就詳細看一下這個方法
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 獲取當前注解標注的類名或方法名
String classOrMethodName = getClassOrMethodName(metadata);
try {
// 主要的判斷邏輯,返回是否允許注入組件
ConditionOutcome outcome = getMatchOutcome(context, metadata);
// 日志記錄
logOutcome(classOrMethodName, outcome);
// 日志記錄
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
+ ex.getMessage() + " not found. Make sure your own configuration does not rely on "
+ "that class. This can also happen if you are "
+ "@ComponentScanning a springframework package (e.g. if you "
+ "put a @ComponentScan in the default package by mistake)", ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
}
}
這里看看第7行最重要的邏輯
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
MergedAnnotations annotations = metadata.getAnnotations();
// 判斷是否標注了@ConditionalOnBean
if (annotations.isPresent(ConditionalOnBean.class)) {
// 獲取注解上的屬性和對應的值
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
// 在容器中查詢,放回是否具有對應注解的類,MatchResult是由多個map和list組成,
// 是判斷容器里面的類,是否有注解上面的標記。
// 重要邏輯
MatchResult matchResult = getMatchingBeans(context, spec);
// 判斷是否存在由@ConditionalOnBean標注的類,如果有,返回true,沒有返回,false
if (!matchResult.isAllMatched()) {
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage)
.found("bean", "beans")
.items(Style.QUOTE,matchResult.getNamesOfAllMatches());
}
// 判斷是否標注了@ConditionalOnSingleCandidate
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
}
else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
spec.getStrategy() == SearchStrategy.ALL)) {
return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
}
matchMessage = spec.message(matchMessage)
.found("a primary bean from beans")
.items(Style.QUOTE,matchResult.getNamesOfAllMatches());
}
// 判斷是否標注了@ConditionalOnMissingBean
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
String reason = createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
}
return ConditionOutcome.match(matchMessage);
}
分析getMatchOutcome
可以看到,getMatchOutcome
支持@ConditionalOnSingleCandidate
、@ConditionalOnMissingBean
和@ConditionalOnBean
,也就是說,對於這三個注解的判斷邏輯都在這里。其中最重要的邏輯就在getMatchingBeans
這個方法,這個方法比較復雜,也比較簡單,就是根據當前上下文容器,查找是否存在對應的類。SearchStrategy
這個枚舉定義了搜索的范圍:
- All搜索整個上下文,父子容器等等
- ANCESTORS搜索所有祖先,除開當前上下文
- CURRENT搜索當前上下文。
@ConditionalOnProperty
源碼解析
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
/**
* name的別名
* @return the names
*/
String[] value() default {};
/**
* 應該應用於每個屬性的前綴。
* 如果未指定,前綴會自動以點結尾。
* 一個有效的前綴由一個或多個用點分隔的單詞定義(例如"acme.system.feature" )
* @return 前綴名稱
*/
String prefix() default "";
/**
* 要測試的屬性的名稱。如果已定義前綴,則將其應用於計算每個屬性的完整鍵。
* 例如,如果前綴是app.config並且一個值是my-value ,則完整鍵將是app.config.my-value
* 使用虛線符號來指定每個屬性,即全部小寫,並用“-”分隔單詞(例如: my-long-property )
* @return the names
*/
String[] name() default {};
/**
* 可與name組合使用,比較獲取到的屬性值與havingValue給定的值是否相同,相同才加載配置
*/
String havingValue() default "";
/**
* 缺少該配置屬性時是否可以加載。如果為true,沒有該配置屬性時也會正常加載;反之則不會生效
*/
boolean matchIfMissing() default false;
}
注解中的屬性使用方法說明見上面代碼注釋
分析@ConditionalOnProperty
可知,和其他的一樣,也是使用了父注解@Conditional
和邏輯判斷類OnPropertyCondition
,下面看一下這個判斷邏輯類的組成
該類也是繼承了SpringBootCondition
這個類,參考@ConditionalOnBean
源碼,可以知道,SpringBootCondition
實現了Condition
接口,其中最主要的方法就是getMatchOutcome
。
OnPropertyCondition
中的getMatchOutcome
邏輯:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 獲取ConditionalOnProperty注解屬性和對應的值
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
metadata.getAllAnnotationAttributes(ConditionalOnProperty.class.getName()));
List<ConditionMessage> noMatch = new ArrayList<>();
List<ConditionMessage> match = new ArrayList<>();
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
// 判斷是否與條件匹配
// 最主要的邏輯
ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
// 如果結果匹配,結果信息存在match,反之存在noMatch中
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
}
if (!noMatch.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
}
return ConditionOutcome.match(ConditionMessage.of(match));
}
其中最主要的邏輯為determineOutcome
,這個方法根據@ConditionalOnProperty
中的需要做判斷的屬性去判斷,如果當前屬性不存在且不允許缺少屬性時加載或屬性存在,但值不對應,則認為不匹配,反之,認為匹配。
示例
@Configuration
//在application.properties配置"mf.assert",對應的值為true
@ConditionalOnProperty(prefix="mf",name = "assert", havingValue = "true", matchIfMissing = true)
public class AssertConfig {
@Autowired
private HelloServiceProperties helloServiceProperties;
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
helloService.setMsg(helloServiceProperties.getMsg());
return helloService;
}
}
- 當配置文件中存在
mf.assert
,值為false
,該配置類不會注入 - 當配置文件中不存在
mf.assert
,且matchIfMissing=true
, 該配置類不會注入 - 當配置文件中存在
mf.assert
,值為true
,則注入該配置類