問題說明
條件加載是springboot自動配置的剛需,其中有兩個條件@ConditionalOnClass和@ConditionalOnMissingClass非常特別,它是基於class類是否存在判斷的。場景的使用場景如下:
import A;
import B;
@Configuration
class C{
@Bean
@ConditionalOnClass(A.class)
A getBeanA() {
return new A();
}
@Bean
@ConditionalOnMissingClass("A")
B getBeanB() {
return new B();
}
}
在我們日常開發過程中,如果class A不存在,編譯器會提示編譯錯誤。在java運行過程中,當我們遇到class類不存在的時候會報ClassNotFoundException。上面的代碼看起來無法成功初始化我們想要的類。但實際情況是spring通過這個機制正確的選擇了具體的實現類,這是爲什麼呢?
JVM ClassLoader理論回顧
java代碼的生命週期
類生命週期
何時開始類的初始化
Java虛擬機規範中並沒有進行強制約束什麼情況下需要開始類加載過程。但是對於初始化階段,虛擬機規範則是嚴格規定了僅在類被“主動引用”時纔會初始化。
主動引用
- 創建類的實例
- 訪問類的靜態變量(除常量【被final修辭的靜態變量】)。
- 訪問類的靜態方法
- 反射如(Class.forName(“my.xyz.Test”))
- 當初始化一個類時,發現其父類還未初始化,則先出發父類的初始化
- 虛擬機啓動時,定義了main()方法的那個類先初始化
被動引用
- 子類調用父類的靜態變量,子類不會被初始化。只有父類被初始化。對於靜態字段,只有直接定義這個字段的類纔會被初始化.
- 通過數組定義來引用類,不會觸發類的初始化
- 訪問類的常量,不會初始化類
關於java文件頭的import
import的存在純粹是爲了方便寫代碼,在編譯後的class文件中沒有import區。在Java源碼編譯器進行編譯時,每個名字都會經過解析(resolution)找到其全名(canonical form)。
ClassNotFoundException
當程序試圖使用Class.forname()、Classloader#findSystemClass()、Classloader#loadClass()方法通過字符串名的形式加載此類時,會拋出ClassNotFoundException。
問題分析
從以上理論分析我們可以看出,我們在加載class C時,如果觸發沒有new A()就不會初始化class A,就不會有ClassNotFoundException。那問題又來了:如果class A不存在,springboot是如何選擇執行getBeanA()方法還是getBeanB()方法呢?
springboot Bean加載機制和選擇機制
springboot Bean加載分爲Bean掃描註冊和Bean初始化。
Bean掃描
springboot 通過ConfigurationClassBeanDefinitionReader讀取@Configuration註解標識的類。在掃描候選資源時,spring並沒有通過Classloader#loadClass()來加載class文件,而是通過Classloader.getResource()獲得class二進制文件,通過ClassReader對二進制文件進行ASM語法解析,從而得到候選類和註解的元數據。也就是說在spring掃描需要加載的Bean時,所有候選類都沒有加載初始化。
Bean加載判斷
當spring獲得候選bean的元信息時,需要判斷這個bean是否真的需要被加載。這個就是spring的ConditionEvaluator體系。
比如在AnnotatedBeanDefinitionReader#doRegisterBean()中有判斷:
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
return;
}
在獲得ConditionalOnClass註解的元數據時還有個特殊處理,我們聲明@ConditionalOnClass(A.class)時,使用的是Class類。爲了後續操作時不引起class加載,AnnotatedElementUtils#getMergedAnnotationAttributes(element,annotationName,classValuesAsString,nestedAnnotationsAsMap)將class類轉換成了class的名稱字符串。
在最終判斷class是否存在時,jvm其實還是拋出了ClassNotFoundException,只是異常被吞沒了。(~ ̄▽ ̄)~
//OnClassCondition$MatchType.isPresent(String, ClassLoader) line: 219
private static boolean isPresent(String className, ClassLoader classLoader) {
//省略若干行
try {
forName(className, classLoader);
return true;
}
catch (Throwable ex) {
return false;//我就在這裏,只是你看不到 O(∩_∩)O
}
}
所以關於這個問題的正確說法應該是,class類不存在時spring條件加載能夠正常執行,而不是不報異常。
至此,當Class A不存在時,Bean B被註冊到spring容器中,等待初始化。
Bean初始化
在初始化Bean時,AbstractAutowireCapableBeanFactory#createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)直接通過MethodProxy.invokeSuper()反射調用了C#getBeanB()。C#getBeanA()將永遠不會被調用,默默地在角落裏死去。。。
JVM類生命週期概述:加載時機與加載過程: https://blog.csdn.net/justloveyou_/article/details/72466105