Spring Boot 注解——@Conditional


簡介

@Conditional是Spring4新提供的注解,它的作用是按照一定的條件進行判斷,滿足條件給容器注冊Bean

作用

條件裝配,滿足Conditional指定的條件,則進行組件注入

根注解與派生注解

image

@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);
}

運行結果如下:
image

@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中。
image
首先看這個類頭,OnBeanCondition中並沒有實現Condition,但是他繼承了一個叫FilteringSpringBootCondition的類,在進入這個類看看
image

FilteringSpringBootCondition這個類也沒有實現Condition這個類,不過他也繼承了一個叫SpringBootsCondition的類

image

進入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行最重要的邏輯
image

@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搜索當前上下文。

image

@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,下面看一下這個判斷邏輯類的組成

image

該類也是繼承了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,則注入該配置類


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM