一、Conditional註解介紹
對SpringBoot有足夠了解的小夥伴應該都用過Conditional系列註解,該註解可用在類或者方法上用於控制Bean的初始化。
常用的Conditional註解有以下幾種:
-
@ConditionalOnBean:如果存在對應的Bean,則進行當前Bean的初始化。
-
@ConditionalOnClass:如果專案的classpath下存在對應的類檔案,則進行當前Bean的初始化。
-
@ConditionalOnExpression:如果滿足SpEL表示式,則進行當前Bean的初始化。
-
@ConditionalOnMissingBean:如果不存在對應的Bean,則進行當前Bean的初始化。
-
@ConditionalOnMissingClass:如果專案的classpath下不存在對應的類檔案,則進行當前Bean的初始化。
-
@ConditionalOnProperty:如果配置檔案上的屬性值符合預期值,則進行當前Bean的初始化。
注意如果存在多個Conditional註解,只有都滿足條件時才會生效。這裡只作簡單介紹,更多用法可以搜尋其他文章。
二、原始碼分析
我們先以@ConditionalOnBean為例,分析SpringBoot是如何實現該功能的?
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
/**
* The class types of beans that should be checked. The condition matches when beans
* of all classes specified are contained in the {@link BeanFactory}.
* @return the class types of beans to check
*/
Class<?>[] value() default {};
/**
* The class type names of beans that should be checked. The condition matches when
* beans of all classes specified are contained in the {@link BeanFactory}.
* @return the class type names of beans to check
*/
String[] type() default {};
/**
* The annotation type decorating a bean that should be checked. The condition matches
* when all of the annotations specified are defined on beans in the
* {@link BeanFactory}.
* @return the class-level annotation types to check
*/
Class<? extends Annotation>[] annotation() default {};
/**
* The names of beans to check. The condition matches when all of the bean names
* specified are contained in the {@link BeanFactory}.
* @return the names of beans to check
*/
String[] name() default {};
/**
* Strategy to decide if the application context hierarchy (parent contexts) should be
* considered.
* @return the search strategy
*/
SearchStrategy search() default SearchStrategy.ALL;
/**
* Additional classes that may contain the specified bean types within their generic
* parameters. For example, an annotation declaring {@code value=Name.class} and
* {@code parameterizedContainer=NameRegistration.class} would detect both
* {@code Name} and {@code NameRegistration<Name>}.
* @return the container types
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {};
}
首先,檢視@ConditionalOnBean註解的原始碼,發現該註解上有個後設資料註解 @Conditional,那麼這個註解是幹嘛的呢?
點進去檢視其註釋,發現有如下一行話。
/**
*
* <p>The {@code @Conditional} annotation may be used in any of the following ways:
* <ul>
* <li>as a type-level annotation on any class directly or indirectly annotated with
* {@code @Component}, including {@link Configuration @Configuration} classes</li>
* <li>as a meta-annotation, for the purpose of composing custom stereotype
* annotations</li>// 作為元註解,用於編寫自定義構造型註解
* <li>as a method-level annotation on any {@link Bean @Bean} method</li>
* </ul>
* ....
*/
@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有三種用法,當作為元註解時可以用來自定義條件註解,其核心邏輯是由其value指定的Condition介面的實現類來完成的。
@FunctionalInterface
public interface Condition {
/**
* Determine if the condition matches.
* @param context the condition context
* @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return {@code true} if the condition matches and the component can be registered,
* or {@code false} to veto the annotated component's registration
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
Condition是個函式式介面,裡面只有一個mathes方法,如果返回true則進行Bean的建立。其中matches方法有兩個引數ConditionContext和AnnotatedTypeMetadata,其作用如下:
ConditionContext: 可以拿到上下文資訊,包括beanFactory、environment和resourceLoader等。
AnnotatedTypeMetadata: 可以獲取被@Conditional標記的註解資訊。
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
@Nullable
ClassLoader getClassLoader();
}
那麼問題來了,上面的@Conditional的value屬性可以指定多個Condition類A和B,如何控制Bean的初始化呢?
其實這裡是與的邏輯,只有當所以Condition實現類的mathes方法都返回true時,才會進行Bean的初始化,否則不生效。具體原因,這裡不擴充套件了。
由此可知@ConditionalOnBean的核心邏輯就在OnBeanCondition類裡,OnBeanCondition也實現了Condition介面,但是其mathes()方法是在SpringBootCondition中實現的。
/**
* 所有Condition實現的基類
* Base of all {@link Condition} implementations used with Spring Boot. Provides sensible
* logging to help the user diagnose what classes are loaded.
*
* @author Phillip Webb
* @author Greg Turnquist
* @since 1.0.0
*/
public abstract class SpringBootCondition implements Condition {
private final Log logger = LogFactory.getLog(getClass());
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
// getMatchOutcome是個抽象方法,由各個子類去實現
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);
}
}
// 省略其他程式碼...
}
public class ConditionOutcome {
private final boolean match;
private final ConditionMessage message;
// 省略其他程式碼...
}
ConditionOutcome是個匹配結果類,裡面只有兩個屬性欄位,匹配邏輯在getMatchOutcome方法由子類實現,剛好OnBeanCondition.java中有該方法的實現,程式碼如下。
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
MergedAnnotations annotations = metadata.getAnnotations();
if (annotations.isPresent(ConditionalOnBean.class)) {
// 首先包裝成Spec類,Spec裡的屬性與ConditionalOnBean註解的基本一致
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
// 獲取所有的匹配的bean
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
// 由於@ConditionalOnBean可以指定多個bean,所以這裡要求全匹配,否則返回不匹配的原因
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
matchResult.getNamesOfAllMatches());
}
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());
}
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);
}
獲取bean的邏輯在getMatchingBeans方法中,大家可以自己去看下(我才不會說是我懶?)。
至此通過上面的分析總結下,自定義一個ConditionalOnXX註解大概分為如下幾步:
- 建立一個Condition類並實現Condition介面
- 根據自己的需求寫一個ConditionalOnXX註解,並指定Condition類
- 完善Condition類的matches方法邏輯
三、自定義ConditionalOnXX註解
需求背景: 自定義ConditionalOnXX註解,實現設定的多個配置項中,任意一個配置匹配中即可完成Bean的初始化。
1.建立Conditional註解
/**
* @Author: Ship
* @Description: 任意name-value匹配即可
* @Date: Created in 2021/10/18
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(value = OnAnyMatchCondition.class)
public @interface ConditionalOnAnyMatch {
String[] name() default {};
String[] value() default {};
}
2.實現Condition類
/**
* @Author: Ship
* @Description:
* @Date: Created in 2021/10/18
*/
public class OnAnyMatchCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
MergedAnnotations annotations = metadata.getAnnotations();
if (!annotations.isPresent(ConditionalOnAnyMatch.class)) {
return true;
}
MergedAnnotation<ConditionalOnAnyMatch> annotation = annotations.get(ConditionalOnAnyMatch.class);
// 獲取註解的屬性值
String[] names = annotation.getValue("name", String[].class).orElse(new String[]{});
String[] values = annotation.getValue("value", String[].class).orElse(new String[]{});
for (int i = 0; i < names.length; i++) {
// 通過環境變數拿到專案配置資訊
String property = environment.getProperty(names[i]);
String value = values[i];
if (value != null && value.equals(property)) {
// 任意一個匹配則返回true
return true;
}
}
return false;
}
}
四、測試
測試前需要寫一些測試程式碼,首先建立一個用於測試的Bean類TestBean
public class TestBean {
}
其次,為了分別測試@ConditionalOnAnyMatch用於類上和方法上的效果,分別建立TestClassBean和TestConfiguration。
/**
* @Author: Ship
* @Description:
* @Date: Created in 2021/10/18
*/
@ConditionalOnAnyMatch(name = {"test.aa", "test.bb"}, value = {"1", "2"})
@Component
public class TestClassBean {
@PostConstruct
public void init(){
System.out.println("Initialized bean:testClassBean...");
}
}
TestConfiguration.java
/**
* @Author: Ship
* @Description:
* @Date: Created in 2021/10/18
*/
@Configuration
public class TestConfiguration {
@Bean
@ConditionalOnAnyMatch(name = {"test.aa", "test.bb"}, value = {"1", "2"})
public TestBean testBean() {
System.out.println("Initialized bean:testBean...");
return new TestBean();
}
}
通過程式碼可以看到,test.aa=1或者test.bb=2任意一個條件成立,就會建立Bean。
-
測試不符合的場景
配置檔案application.properties新增如下配置
test.aa=3 test.bb=4
然後啟動專案,可以看到控制檯日誌沒有列印任何資訊,說明testBean和testClassBean都沒被建立。
-
測試符合一個的場景
修改配置如下
test.aa=1 test.bb=4
再重啟專案,可以看到控制檯日誌列印瞭如下資訊,說明testBean和testClassBean都被建立了。
Initialized bean:testClassBean... Initialized bean:testBean... 2021-10-20 21:37:50.319 INFO 7488 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path '' 2021-10-20 21:37:50.328 INFO 7488 --- [ main] cn.sp.SpringExtensionApplication : Started SpringExtensionApplication in 1.725 seconds (JVM running for 2.325)
說明@ConditionalOnAnyMatch成功的控制了Bean的初始化,本文程式碼已經上傳至github,如果對你有用希望能點個star,不勝感激?。