Java中的上下文加載器ContextClassLoader詳解
ContextClassLoader
ContextClassLoader是通過 Thread.currentThread().getContextClassLoader() 返回該線程上下文的ClassLoader
1、前置知識(shí)
在講解ContextClassLoader之前,需要先提兩個(gè)知識(shí)點(diǎn):
1)雙親委派模型
- 啟動(dòng)類加載器(Bootstrap ClassLoader):負(fù)責(zé)將放在<JAVA HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的類庫加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時(shí),如果需要把加載請求委派給引導(dǎo)類加載器,那直接使用null代替即可
- 擴(kuò)展類加載器(ExtClassLoader):由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載<JAVA HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴(kuò)展類加載器
- 應(yīng)用程序類加載器(AppClassLoader):由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所有指定的類庫,開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器
類加載之間的這種層次關(guān)系,稱為類加載器的雙親委派模型。雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里類加載器之間的父子關(guān)系一般不會(huì)以繼承的關(guān)系來實(shí)現(xiàn),而是都使用組合關(guān)系來復(fù)用父加載器的代碼
雙親委派模型的工作過程:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載
使用雙親委派模型來組織類加載器之間的關(guān)系,有一個(gè)顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如類 java.lang.Object ,它存放在 rt.jar 之中,無論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給處于模型最頂端的啟動(dòng)類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類
2)如果一個(gè)類由類加載器A加載,那么這個(gè)類的依賴類也是由相同的類加載器加載
比如Spring作為一個(gè)Bean工廠,它需要?jiǎng)?chuàng)建業(yè)務(wù)類的實(shí)例,并且在創(chuàng)建業(yè)務(wù)類實(shí)例之前需要加載這些類。Spring是通過調(diào)用 Class.forName 來加載業(yè)務(wù)類的。調(diào)用 Class.forName() 的時(shí)候,會(huì)獲取調(diào)用該方法的類的類加載器,使用該類加載器來加載 Class.forName() 中傳入的類,代碼如下:
public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement { @CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { // 獲取調(diào)用該方法的類 Class<?> caller = Reflection.getCallerClass(); // ClassLoader.getClassLoader獲取調(diào)用該方法的類的類加載器 return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
2、為什么需要ContextClassLoader?
當(dāng)我們需要加載一個(gè)類,從自定義ClassLoader,到AppClassLoader,再到ExtClassLoader,最后到Bootstrap ClassLoader。沒問題, 很順利。這是從下到上加載。但是反過來,當(dāng)從上到下加載的時(shí)候,這個(gè)變得是一個(gè)不可能完成的任務(wù)。為了彌補(bǔ)這個(gè)缺陷, 特定設(shè)計(jì)的ContextClassLoader
這里你可能會(huì)有個(gè)疑問:為什么會(huì)出現(xiàn)從上到下加載的情況。比如一個(gè)類是由Bootstrap ClassLoader加載,該類引用了一個(gè)我們自己開發(fā)的類(該類能被AppClassLoader加載但不能被Bootstrap ClassLoader加載),由如果一個(gè)類由類加載器A加載,那么這個(gè)類的依賴類也是由相同的類加載器加載可知:默認(rèn)情況下我們自己開發(fā)的類會(huì)被Bootstrap ClassLoader嘗試加載,最終會(huì)由于無法加載到類而拋出異常
以SPI為例,SPI接口屬于Java核心庫,由BootstrapClassLoader加載,當(dāng)SPI接口想要引用第三方實(shí)現(xiàn)類的具體方法時(shí),BootstrapClassLoader無法加載Classpath下的第三方實(shí)現(xiàn)類,這時(shí)就需要使用線程上下文類加載器ContextClassLoader來解決。借助這種機(jī)制可以打破雙親委托機(jī)制限制
SPI核心類ServiceLoader源碼如下:
public final class ServiceLoader<S> implements Iterable<S> { public static <S> ServiceLoader<S> load(Class<S> service) { // 線程上下文類加載器,在Launcher類的構(gòu)造器中被賦值為AppClassLoader,它可以讀到ClassPath下的自定義類 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
3、ContextClassLoader默認(rèn)為AppClassLoader
JVM啟動(dòng)時(shí),會(huì)去調(diào)用Launcher類的構(gòu)造方法:
public class Launcher { public Launcher() { ClassLoader extcl; try { // 首先創(chuàng)建擴(kuò)展類加載器 extcl = ExtClassLoader.getExtClassLoader(); } catch (IOException e) { throw new InternalError( "Could not create extension class loader"); } // Now create the class loader to use to launch the application try { // 再創(chuàng)建AppClassLoader并把extcl作為父加載器傳遞給AppClassLoader loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader"); } // 設(shè)置線程上下文類加載器,稍后分析 Thread.currentThread().setContextClassLoader(loader); // 省略其他代碼... }
Launcher初始化時(shí)首先會(huì)創(chuàng)建ExtClassLoader類加載器,然后再創(chuàng)建AppClassLoader并把ExtClassLoader傳遞給它作為父類加載器,還把AppClassLoader默認(rèn)設(shè)置為線程上下文類加載器
4、子線程ContextClassLoader默認(rèn)為父線程的ContextClassLoader
Thread在 init()
方法中會(huì)把子線程ContextClassLoader設(shè)置為父線程的ContextClassLoader
public class Thread implements Runnable { private ClassLoader contextClassLoader; private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { // 省略其他代碼... // 當(dāng)前線程為父線程 Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); // 子線程ContextClassLoader設(shè)置為父線程的ContextClassLoader if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; // 省略其他代碼... }
到此這篇關(guān)于Java中的上下文加載器ContextClassLoader詳解的文章就介紹到這了,更多相關(guān)ContextClassLoader詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
@FeignClient?path屬性路徑前綴帶路徑變量時(shí)報(bào)錯(cuò)的解決
這篇文章主要介紹了@FeignClient?path屬性路徑前綴帶路徑變量時(shí)報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07springboot整合netty框架實(shí)現(xiàn)站內(nèi)信
Netty 是一個(gè)基于NIO的客戶、服務(wù)器端編程框架,使用Netty 可以確保你快速和簡單的開發(fā)出一個(gè)網(wǎng)絡(luò)應(yīng)用,這篇文章主要介紹了springboot整合netty框架的方式小結(jié),需要的朋友可以參考下2022-12-12SpringBoot啟動(dòng)嵌入式Tomcat的實(shí)現(xiàn)步驟
本文主要介紹了淺談SpringBoot如何啟動(dòng)嵌入式Tomcat,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08Springboot 全局時(shí)間格式化三種方式示例詳解
時(shí)間格式化在項(xiàng)目中使用頻率是非常高的,當(dāng)我們的 API? 接口返回結(jié)果,需要對其中某一個(gè) date? 字段屬性進(jìn)行特殊的格式化處理,通常會(huì)用到 SimpleDateFormat? 工具處理,這篇文章主要介紹了3 種 Springboot 全局時(shí)間格式化方式,需要的朋友可以參考下2024-01-01深扒Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和區(qū)別以及如何應(yīng)用
po vo bo dto dao 和 pojo 是軟件開發(fā)中經(jīng)常使用的一些概念,用于設(shè)計(jì)和實(shí)現(xiàn)對象模型,下面將分別解釋這些概念的含義及其在開發(fā)中的應(yīng)用,這篇文章主要給大家介紹了關(guān)于Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和區(qū)別以及如何應(yīng)用的相關(guān)資料,需要的朋友可以參考下2024-08-08Maven打包JavaWeb項(xiàng)目的兩種實(shí)現(xiàn)方式
介紹了兩種Maven打包Web項(xiàng)目的方式:通過Eclipse和通過命令行,Eclipse方式包括清理、打包、跳過測試、輸入?Goals?等步驟,命令行方式包括進(jìn)入項(xiàng)目目錄、執(zhí)行?clean?和?package?命令、跳過測試等步驟,注意事項(xiàng)包括確保有JDK環(huán)境、正確配置pom.xml文件和修改版本號(hào)2025-02-02