大家好,我是“良工說技術”。 今天給大家帶來的是springboot中的@ConditionalOnClass註解的用法。上次的@ConditionalOnBean註解還記得嗎? 一、@ConditionalOnClass註解初始 看下@CodidtionalOnClass註解的定義, 需要註意的有 ...
大家好,我是“良工說技術”。
今天給大家帶來的是springboot中的@ConditionalOnClass註解的用法。上次的@ConditionalOnBean註解還記得嗎?
一、@ConditionalOnClass註解初始
看下@CodidtionalOnClass註解的定義,
需要註意的有兩點,
- 該註解可以用在類及方法上;類指的是標有@Configuration的類,方法是標有@Bean的方法;
- 該註解使用了@Conditional註解標記;這是重點
看到這裡,有小伙伴會疑惑,講了那麼多@Conditional註解的作用是什麼,不急,作用馬上來。
@ConditionalOnClass註解的作用是當項目中存在某個類時才會使標有該註解的類或方法生效;
這句話有點拗口,通俗的講,@ConditionalOnClass標識在@Configuration類上,只有存在@ConditionalOnClass中value/name配置的類該Configuration類才會生效;@ConditionalOnClass標識在@Bean方法上,只有隻有存在@ConditionalOnClass中value/name配置的類方法才會生效。看具體的實例更容易理解些
二、@ConditionalOnClass註解用法
從上面@ConditionalOnClass註解的定義中我們知道該註解可以配置兩個屬性,分別是value和name,其中value和name都是數組,只不過內容不一樣,
value是Class的數組,name是全限類名的字元串。
1、使用value屬性
開始,我一直使用value屬性進行配置,但是總是報錯,比如我配置
@Configuration
@ConditionalOnClass(value = {Client.class})
public class MyAutoConfig {
public MyAutoConfig(){
System.out.println("constructor MyAutoConfig");
}
}
該Client是下麵的類,
org.springframework.boot.autoconfigure.data.elasticsearch.Client
它是ES中的一個類,我本身配置的含義是只有在Client存在的時候MyAutoConfig才會生效,但是總是不成功。你知道為什麼不成功嗎?
這是因為我沒有引ES的依賴,導致在我的classpath中沒有這個類,按照@ConditionalOnClass的理解,應該是不存在則不會生效,但是由於沒有這個類,導致的問題是:無法編譯,提示下麵的錯誤
java: 找不到符號
符號: 類 Client
這是可以理解的,因為沒有這個類,而我要引用這個類肯定是引用不到的,所以編譯是失敗的,也就程式跑不起來。那麼存在一個問題,@ConditionalOnClass註解的value屬性要在什麼情況下使用?
這裡有一個mybatisplus的配置類,
其配置類上標識了@ConditionalOnClass註解,該註解中配置了value屬性,且配置了SqlSessionFactory和SqlSessionFactoryBean兩個類,
MyBatisPlusAutoConfiguration是在mybatis-plus-boot-starter的jar包下
SqlSessionFactory是在mybatis的jar包下
SqlSessionFactoryBean是在mybaits-spring的jar包下
這三個類分屬於不同的jar包,如果我在一個項目中引入了mybatis-plus-boot-starter的jar包,沒有引入mybatis的jar包那麼MybatisPlusAutoConfiguration不會生效,也就是只有mybatis和mybatis-spring的jar包都引入了,MybatisPlusAutoConfiguration才會生效,才會被納入spring容器的管理。
需要註意一點:為了防止少引包,在mybatis-plus-boot-starter中會依賴mybatis和mybatis-spring,這也是starter的好處,不會少引包,需要哪些依賴它都引好了。
那麼再回到問題的開始,為什麼,我配置了一個不存在的類就沒成功,那是因為java的源文件需要編譯,在編譯時會檢查類是否存在,不存在肯定是編譯不通過的;而如果引用的是jar包中的文件引用另外一個jar的,則是因為jar包經過了編譯,已經打包成功了,故不存在問題。
通過value屬性需要結合jar包的方式,這裡就不演示了,感興趣的小伙伴可以自己嘗試。通過name屬性來指定。
2、使用name屬性
@ConditionalOnClass註解還有name屬性,name屬性指定的是全限類名,也就是包含包名+類名。看下我的配置,
@Configuration
@ConditionalOnClass(name = {"com.my.template.config.ClassA"})
public class MyAutoConfig {
public MyAutoConfig(){
System.out.println("constructor MyAutoConfig");
}
}
這裡配置了“com.my.template.config.ClassA”,ClassA是我的一個存在的類,
下麵啟動,看下在啟動日誌中是否有“constructor MyConfig”列印,
constructor MyAutoConfig
constructor MyAutoConfig2
constructor classA
2022-07-30 17:18:54.113
看到了,日誌說明name配置是生效的,也就是存在ClassA則MyAutoConfig會註冊到spring的容器中。作為對比,下麵配置一個不存在的類ClassD,
@Configuration
@ConditionalOnClass(name = {"com.my.template.config.ClassD"})
public class MyAutoConfig {
public MyAutoConfig(){
System.out.println("constructor MyAutoConfig");
}
}
看下啟動日誌
constructor MyAutoConfig2
constructor classA
2022-07-30 21:43:30.550 INFO 13116 --- [
從上面的日誌可以看到,沒有列印出來想要的日誌,說明MyAutoConfig沒有註冊到spring的容器中。
我們知道name屬性是一個數組,上面僅僅配置了一個類,如果配置多個會是什麼樣子,感興趣的可以自己嘗試,這裡這直接給出答案,只有name屬性中配置的全部滿足相應的配置類才會生效。
不知道,你是否對@ConditionalOnClass是怎麼實現的感興趣嗎,繼續往下看,大揭秘了。
三、@ConditionalOnClass是怎麼實現的
要理解@ConditionalOnClass是怎麼實現的還是要回到該註解的定義上,前邊提到該註解被
@Conditional(OnClassCondition.class)
註解標識,@Conditional註解的含有是要滿足條件才會生效,該註解後邊再看。今天的主角是OnClassCondition類,看下其繼承關係
重點關註XXCondition即可,可以看到最終實現了Condition介面,@Conditional註解的本質就是考查是否滿足Codition介面的matches()方法,所以這看SpringBootCondition中matches方法的實現,
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//獲得該註解標準的類或方法
String classOrMethodName = getClassOrMethodName(metadata);
try {
//模板方法,該實現在OnClassCodition類中
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);
}
}
getMatchOutCome()方法使用了模板方法,實現在OnClassCondition類中,這是最要的方法,
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
//獲得@ConditionalOnClass註解中配置的value和name屬性的值
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
//判斷@ConditionOnClass註解配置的類是否都可以載入到,如有載入不到的則放到missing中
List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
if (!missing.isEmpty()) {
//有載入不到的,則返回ConditionOutcome對下,其中屬性match為false
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes").items(Style.QUOTE, missing));
}
matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes")
.items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
}
//@ConditionalOnMissingClass的處理邏輯
List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);
if (onMissingClasses != null) {
List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
if (!present.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class)
.found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
}
matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
.didNotFind("unwanted class", "unwanted classes")
.items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
}
//返回ConditionOutCome對下,其match屬性為true
return ConditionOutcome.match(matchMessage);
}
上面的代碼已經給出了註釋,對應@ConditionalOnClass註解的處理就是解析器配置的value和name屬性,判斷配置的類是否載入到,如有未載入到的則直接返回屬性match=false的ConditionOutcome對象,那麼是如何判斷是否載入到的,是通過FilteringSpringBootCondition中的filter方法,
protected final List<String> filter(Collection<String> classNames, ClassNameFilter classNameFilter,
ClassLoader classLoader) {
if (CollectionUtils.isEmpty(classNames)) {
return Collections.emptyList();
}
List<String> matches = new ArrayList<>(classNames.size());
for (String candidate : classNames) {
//迴圈調用matches方法
if (classNameFilter.matches(candidate, classLoader)) {
matches.add(candidate);
}
}
return matches;
}
對於@CoditionalOnClass的處理該方法傳入的參數為
List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
那麼也就是調用ClassNameFilter.MISSING的matches方法,其方法如下
可以看到調用的是!isPresent方法,看下該方法的實現,
static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
//具體實現邏輯
FilteringSpringBootCondition.resolve(className, classLoader);
return true;
} catch (Throwable var3) {
return false;
}
}
具體的實現在resolve方法中,且該方法被try catch包住了,如果載入不到,直接返回false。
protected static Class<?> resolve(String className, ClassLoader classLoader) throws ClassNotFoundException {
return classLoader != null ? Class.forName(className, false, classLoader) : Class.forName(className);
}
看到這裡,大家明白了,@ConditionalOnClass註解中判斷配置的類是否存在使用的方法是Class.forName,類載入。
四、總結
本文主要認識了@ConditionalOnClass註解,分析了其註解的原理,如何判斷配置的類是否存在。
- @ConditionalOnClass註解有兩個屬性,分別是value和name,註意其配置方式;
- @ConditionalOnClass註解判斷配置的類是否存在的方式是通過Class.forName的方式;
推薦閱讀
springboot的@ConditionalOnBean註解
一個愛寫文章的程式員,歡迎關註我的公眾號“良工說技術”。我有故事,你有酒嗎