Classloader
JVM加載class文件到內(nèi)存有兩種方式:
- 隱式加載:不通過(guò)在代碼里調(diào)用ClassLoader來(lái)加載需要的類橡淆,而是通過(guò)JVM來(lái)自動(dòng)加載需要的類到內(nèi)存辣吃,例如:當(dāng)類中繼承或者引用某個(gè)類時(shí),JVM在解析當(dāng)前這個(gè)類不在內(nèi)存中時(shí)仰猖,就會(huì)自動(dòng)將這些類加載到內(nèi)存中捏肢。
- 顯式加載:在代碼中通過(guò)ClassLoader類來(lái)加載一個(gè)類奈籽,例如調(diào)用
this.getClass.getClassLoader().loadClass()
或者Class.forName()
注意:
程序在啟動(dòng)的時(shí)候,并不會(huì)一次性加載程序所要用的所有class文件鸵赫,而是根據(jù)程序的需要衣屏,通過(guò)Java的類加載機(jī)制來(lái)動(dòng)態(tài)加載某個(gè)class文件到內(nèi)存中。
ClassLoader工作機(jī)制
雙親委派模型
雙親委派模式是在Java 1.2后引入的辩棒,其工作原理的是:
如果一個(gè)類加載器收到了類加載請(qǐng)求狼忱,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求
委托給父類的加載器去執(zhí)行
一睁,如果父類加載器還存在其父類加載器钻弄,則進(jìn)一步向上委托,依次遞歸者吁,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類加載器窘俺,如果父類加載器可以完成類加載任務(wù),就成功返回
复凳,倘若父類加載器無(wú)法完成此加載任務(wù)瘤泪,子加載器才會(huì)嘗試自己去加載
,這就是雙親委派模式育八,即每個(gè)兒子都很懶对途,每次有活就丟給父親去干,直到父親說(shuō)這件事我也干不了時(shí)髓棋,兒子自己想辦法去完成
雙親委派模式優(yōu)勢(shì)
- 避免類的重復(fù)加載
- 安全因素实檀,java核心api中定義類型不會(huì)被隨意替換
用類加載器顯式加載類例如
java.lang.Integer
,類加載器只會(huì)返回給已加載過(guò)的按声;如果自定義java.lang.Integer
并加載之膳犹,會(huì)報(bào)錯(cuò)。
https://blog.csdn.net/Mint6/article/details/80864788?from=singlemessage
具體解析
一般來(lái)說(shuō)儒喊,例如程序hello.jar
執(zhí)行到:
Demo demo = new Demo();
會(huì)按照雙親委派模型進(jìn)行加載類Demo
镣奋。如果Demo
在hello.jar
內(nèi),AppClassLoader
就將其加載完成怀愧;但是如果例如SPI
這種侨颈,既不在應(yīng)用hello.jar
內(nèi)又不在系統(tǒng)類路徑內(nèi),那么就要拋棄雙親委派模型芯义,獲取線程上下文類加載器
加載(線程上下文類加載器
默認(rèn)是AppClassLoader
哈垢,此時(shí)的線程上下文類加載器
肯定是自定義的類加載器)。
在DriverManager類初始化時(shí)執(zhí)行了loadInitialDrivers()方法,在該方法中通過(guò)
ServiceLoader.load(Driver.class);
去加載外部實(shí)現(xiàn)的驅(qū)動(dòng)類扛拨,ServiceLoader類會(huì)去讀取mysql的jdbc.jar下META-INF文件的內(nèi)容這樣ServiceLoader會(huì)幫助我們處理一切耘分,并最終通過(guò)load()方法加載,看看load()方法實(shí)現(xiàn)就知道最終是
通過(guò)線程上下文類加載器加載
public static <S> ServiceLoader<S> load(Class<S> service) {
//通過(guò)線程上下文類加載器加載
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
自定義一個(gè)破壞雙親委派模型的類加載器
的方法:
- 父加載器
parent
設(shè)置為null- 重寫
loadClass()
方法直接調(diào)用findClass
(可以參考ClassLoader
代碼)
加載指定路徑的class或jar
這里介紹2種加載方式:
- URLClassLoader直接加載
- ServiceLoader加載
URLClassLoader直接加載
例如要加載類:
package com;
public class Demo {
public Demo() {
System.out.println("\n" + this.getClass().getClassLoader().toString());
}
}
將其編譯為class文件,存放在路徑/Users/root/Projects/idea/my/com
求泰。
注意央渣!
- 根目錄是
/Users/root/Projects/idea/my/
,com
是表示包路徑渴频。- 該類里面如果引用根目錄以外的類芽丹,必須在runtime中能夠獲取到
這時(shí)要加載它:
@Test
public void test() throws Exception {
// 使用根路徑
URL url = new URL("file:/Users/root/Projects/idea/my/");
ClassLoader newCL = new URLClassLoader(new URL[]{url}, Thread.currentThread().getContextClassLoader());
// 加載。注意要使用全限定名
Class clazz = Class.forName("com.Demo", false, newCL);
// 或者
clazz = newCL.loadClass("com.Demo");
}
ServiceLoader加載
對(duì)于SPI這種卜朗,就需要用到ServiceLoader加載拔第。可以參考地址:https://github.com/byamao1/try-plugin
需要注意:
- 放入U(xiǎn)RLClassLoader的URL场钉,目標(biāo)是jar必須是到j(luò)ar文件路徑蚊俺,目標(biāo)是class可以是class的根文件夾路徑
- 自定義類加載器或自己new的URLClassLoader,要在重寫的方法
loadClass
中先判斷要加載的類是否為非本加載器加載的類(如spi中的類)逛万,如果是則用其他類加載器(例如spi加載器)加載泳猬,否則才由自己加載- 在idea中
resources
文件夾下不要直接新建META-INFO.services
文件夾,而是要新建文件夾META-INFO
后再在其下新建文件夾services
(雖然這樣建idea的顯示就是META-INFO.services
泣港,但絕不能按照前面的做暂殖,那樣只是1個(gè)名字叫META-INFO.services
的文件夾)价匠。- 插件類例如
Demo
必須有一個(gè)無(wú)參構(gòu)造方法当纱,否則ServiceLoader
無(wú)法實(shí)例化插件類
知識(shí)點(diǎn)
- 放入U(xiǎn)RLClassLoader的URL的用途就是讓該類加載器能加載其應(yīng)該擁有的jar或class
- URLClassLoader符合雙親委派模型
- 從日志中可以看出:
Demo
中的IDemo
是由AppClassLoader加載的;Demo踩窖、OtherClass坡氯、Internal
是由插件類加載器加載。
插件化
插件化的一個(gè)重要目標(biāo)就是利用類加載器實(shí)現(xiàn)類隔離(比如不同廠商版本的依賴包)洋腮,其原理在于在類中(例如Demo
)隱式類加載器就是Demo
的類加載器(一般為插件類加載器)箫柳,對(duì)于插件中出現(xiàn)的插件外的類(例如SPI接口類)則不加載。
這里分析Presto的connector插件架構(gòu)啥供。
Presto的自定義類加載器PluginClassLoader
繼承URLClassLoader
類并重寫了loadClass
悯恍,其類加載邏輯為:
如果類已加載了,就返回它
如果是個(gè)SPI接口類伙狐,則委托給
spiClassLoader
(就是PluginManager
的類加載器)加載否則交給父方法
super.loadClass
加載涮毫。這里是真正加載插件類的地方,會(huì)到該加載器的成員URLClassPath
中找該類贷屎。要注意的是罢防,PluginManager.parent為空
,實(shí)際上就是不會(huì)委托父加載器加載唉侄,而是只由自己加載(實(shí)際上打破了雙親委派模型)咒吐。插件類的加載過(guò)程是:
注意:
更改當(dāng)前線程的
ContextClassLoader
,只是為了應(yīng)對(duì)擴(kuò)展程序中可能出現(xiàn)的如下代碼:
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
classLoader.loadClass(...);
Java 自定義 ClassLoader 實(shí)現(xiàn)隔離運(yùn)行不同版本jar包的方式
從上面我們得知,如果采取ServiceLoader的SPI方案恬叹,應(yīng)該在resources/META-INF/services
中存放實(shí)現(xiàn)類的全限定名候生。有意思的是Presto的插件基本都沒(méi)有這個(gè)聲明文件,但是編譯打包后插件模塊的target/classes
中卻能找到绽昼。如果觀察插件的pom.xml
文件陶舞,就會(huì)發(fā)現(xiàn)<packaging>presto-plugin</packaging>
。其實(shí)在根pom.xml
中使用了presto自己的打包插件presto-maven-plugin
绪励,將該maven插件打開看就能發(fā)現(xiàn)ServiceDescriptorGenerator
中會(huì)在打包時(shí)自動(dòng)生成了聲明文件肿孵。
SOFA-Ark
SOFA-Ark
是螞蟻金服開源的一款基于Java實(shí)現(xiàn)的輕量級(jí)類隔離加載容器。
具體可以參考博客:sofa-ark類隔離技術(shù)分析調(diào)研
站在插件的角度看待疏魏,我覺得:
SOFA-Ark = SPI接口聲明 + 插件間可依賴