@ConditionalOnClass(A.class)爲什麼不報錯

問題說明

條件加載是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章