大部分人平時(shí)不會(huì)直接接觸到ClassLoader,但ClassLoader作為Java的一個(gè)重要的核心特性卻又和平時(shí)的編碼工作息息相關(guān),了解ClassLoader的機(jī)制有助于我們更好的了解Java的工作機(jī)制,同時(shí)對(duì)于學(xué)習(xí)OSGI敌卓,Web服務(wù)器等工作原理也有幫助
ClassLoader定義
無(wú)論是寫(xiě)一個(gè)簡(jiǎn)單的單文件程序俐填,還是一個(gè)復(fù)雜的多模塊程序终佛,其大致都可分為下列幾步:
- 代碼人員將設(shè)計(jì)邏輯轉(zhuǎn)換為Java語(yǔ)言邏輯并生成.java文件
- Java編譯器將.java文件編譯為Java字節(jié)代碼(.class文件)
- ClassLoader加載.class文件并轉(zhuǎn)換成
java.lang.Class
類(lèi)的一個(gè)實(shí)例放入緩存俊嗽,每個(gè)這樣的實(shí)例用來(lái)表示一個(gè) Java 類(lèi)。后續(xù)通過(guò)此實(shí)例的newInstance()
方法就可以創(chuàng)建出該類(lèi)的對(duì)象
所以ClassLoader的主要作用就是加載.class文件以供運(yùn)行時(shí)使用
ClassLoader分類(lèi)
在Java中铃彰,ClassLoader可大致分為兩類(lèi)绍豁,第一類(lèi)為系統(tǒng)提供的,另外一類(lèi)是由開(kāi)發(fā)人員自行擴(kuò)展的牙捉,其中系統(tǒng)提供的ClassLoader大致有三種竹揍,它們分別為:
- 引導(dǎo)類(lèi)加載器(Bootstrap ClassLoader);它用來(lái)加載 Java 的核心庫(kù)邪铲,如:rt.jar芬位、resources.jar等
- 擴(kuò)展類(lèi)加載器(Extension ClassLoader);負(fù)責(zé)加載Java的擴(kuò)展類(lèi)庫(kù)带到,默認(rèn)加載JAVA_HOME/jre/lib/ext/目錄下的所有jar
- 應(yīng)用類(lèi)加載器(App ClassLoader)昧碉;負(fù)責(zé)加載應(yīng)用程序classpath目錄下的所有jar和class文件
在這三種系統(tǒng)提供的ClassLoader中,引導(dǎo)類(lèi)加載器較為特殊阴孟,這一點(diǎn)在后續(xù)會(huì)提到;而由開(kāi)發(fā)人員自行擴(kuò)展的ClassLoader則需繼承java.lang.ClassLoader
類(lèi)并根據(jù)需要重寫(xiě)特定方法税迷,一般重寫(xiě)findClass
方法即可
ClassLoader工作機(jī)制
相信即便是不了解ClassLoader工作機(jī)制的人永丝,也聽(tīng)說(shuō)過(guò)雙親委派機(jī)制,雙親委派機(jī)制就是對(duì)ClassLoader的工作機(jī)制描述箭养,除了引導(dǎo)類(lèi)加載器之外慕嚷,所有的類(lèi)加載器都有一個(gè)父類(lèi)加載器(可以通過(guò) getParent()
方法可以查看,該父類(lèi)加載器與當(dāng)前類(lèi)加載器不是繼承關(guān)系毕泌,是關(guān)聯(lián)關(guān)系)喝检,如應(yīng)用類(lèi)加載器的父類(lèi)加載器是擴(kuò)展類(lèi)加載器,而擴(kuò)展類(lèi)加載器的父類(lèi)加載器是引導(dǎo)類(lèi)加載器
ClassLoader loader = ClassLoaderStructure.class.getClassLoader();//獲得加載當(dāng)前類(lèi)的類(lèi)加載器
while(loader != null) {
System.out.println(loader);
loader = loader.getParent();//獲得父類(lèi)加載器的引用
}
System.out.println(loader);
//運(yùn)行結(jié)果
sun.misc.Launcher$AppClassLoader@232204a1 //應(yīng)用類(lèi)加載器
sun.misc.Launcher$ExtClassLoader@14ae5a5 //擴(kuò)展類(lèi)加載器
null //引導(dǎo)類(lèi)加載器撼泛,由于應(yīng)到類(lèi)加載器不繼承與 java.lang.ClassLoader挠说,由原生代碼實(shí)現(xiàn),所以這里顯示是null
對(duì)于開(kāi)發(fā)人員編寫(xiě)的類(lèi)加載器來(lái)說(shuō)愿题,其父類(lèi)加載器是加載此類(lèi)加載器 Java 類(lèi)的類(lèi)加載器损俭。因?yàn)轭?lèi)加載器 Java 類(lèi)如同其它的 Java 類(lèi)一樣,也是要由類(lèi)加載器來(lái)加載的潘酗。一般來(lái)說(shuō)杆兵,開(kāi)發(fā)人員編寫(xiě)的類(lèi)加載器的父類(lèi)加載器是應(yīng)用類(lèi)加載器。類(lèi)加載器通過(guò)這種方式組織起來(lái)仔夺,形成樹(shù)狀結(jié)構(gòu)琐脏。樹(shù)的根節(jié)點(diǎn)就是引導(dǎo)類(lèi)加載器
當(dāng)一個(gè)ClassLoader實(shí)例需要加載某個(gè)類(lèi)時(shí),它會(huì)首先檢查這個(gè)類(lèi)是否已經(jīng)加載,這個(gè)過(guò)程是由下至上依次檢查日裙,若所有加載器均未加載吹艇,則先從頂層加載器開(kāi)始試圖加載,若加載失敗阅签,則把任務(wù)轉(zhuǎn)交給擴(kuò)展類(lèi)加載器進(jìn)行加載掐暮,如果也沒(méi)加載到,則轉(zhuǎn)交給應(yīng)用類(lèi)加載器進(jìn)行加載政钟,如果它依然沒(méi)有加載到的話(huà)路克,則返回給委托的發(fā)起者,由它到指定的文件系統(tǒng)或網(wǎng)絡(luò)等URL中加載該類(lèi)养交,這個(gè)過(guò)程是由上至下的精算。如果它們都沒(méi)有加載到這個(gè)類(lèi)時(shí),則拋出ClassNotFoundException
異常碎连。否則將這個(gè)找到的類(lèi)生成一個(gè)類(lèi)的定義灰羽,并將它加載到內(nèi)存當(dāng)中,最后返回這個(gè)類(lèi)在內(nèi)存中的Class實(shí)例對(duì)象鱼辙,這就是雙親委派的工作流程了
那為什么需要使用這種流程進(jìn)行類(lèi)的加載呢廉嚼?首先來(lái)看下面實(shí)例:
//待加載類(lèi)
public class Biz {
private Biz instance;
public void setInstance(Object instance) {
this.instance = (Biz)instance; //類(lèi)型轉(zhuǎn)換
System.out.println("instance inited");
}
}
//自行實(shí)現(xiàn)的類(lèi)加載器
public class FileSystemClassLoader extends ClassLoader{
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
//調(diào)用代碼
public class Client {
public static void main(String[] args) {
String classDataRootPath = "D:\\temp"; //Biz.class放置于該目錄下
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "classloader.whydelegation.Biz";
try {
Class<?> class1 = fscl1.loadClass(className);
System.out.println("class1 ClassLoader is " + class1.getClassLoader());
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
System.out.println("class2 ClassLoader is " + class2.getClassLoader());
Object obj2 = class2.newInstance();
class1.getMethod("setInstance", java.lang.Object.class).invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//運(yùn)行結(jié)果
class1 ClassLoader is classloader.whydelegation.FileSystemClassLoader@7f31245a
java.lang.reflect.InvocationTargetException
class2 ClassLoader is classloader.whydelegation.FileSystemClassLoader@135fbaa4
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at classloader.whydelegation.Client.main(Client.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.ClassCastException: classloader.whydelegation.Biz cannot be cast to classloader.whydelegation.Biz
at classloader.whydelegation.Biz.setInstance(Biz.java:10)
... 10 more
這段代碼示例通過(guò)兩個(gè)不同的類(lèi)加載器加載同一個(gè).class文件,最后將生成的實(shí)例進(jìn)行類(lèi)型轉(zhuǎn)換(Biz#setInstance中)倒戏,但報(bào)ClassCastException
怠噪,原因就在于即便是同一個(gè).class文件被不同的類(lèi)加載器加載,最終得到的也是兩個(gè)不同的類(lèi)的示例杜跷,因?yàn)镴VM在判定兩個(gè)class是否相同時(shí)傍念,不僅要判斷兩個(gè)類(lèi)名是否相同,而且要判斷是否由同一個(gè)類(lèi)加載器加載的葛闷。只有兩者同時(shí)滿(mǎn)足的情況下憋槐,JVM才認(rèn)為這兩個(gè)class是相同的
再回到雙親委派機(jī)制, 它能保證公用的類(lèi)特別是Java核心類(lèi)庫(kù)只會(huì)被加載一次淑趾,保證Java 應(yīng)用所使用的都是同一個(gè)版本的 Java 核心庫(kù)的類(lèi)阳仔,如在加載一個(gè)類(lèi)的時(shí)候,會(huì)首先去其父級(jí)加載器查找該類(lèi)是否已經(jīng)加載過(guò)扣泊,若加載過(guò)驳概,則不會(huì)再次加載,同時(shí)保證該由父級(jí)加載器加載的類(lèi)由父級(jí)加載旷赖,而不會(huì)出現(xiàn)自行實(shí)現(xiàn)的類(lèi)加載器去加載核心類(lèi)庫(kù)的情況顺又,試想如果沒(méi)有雙親委派機(jī)制,那么對(duì)于java.lang.Object
這種通用類(lèi)等孵,就會(huì)存在多個(gè)版本稚照,且互不兼容
定義自己的ClassLoader
因?yàn)镴ava中提供的默認(rèn)ClassLoader,只加載指定目錄下的jar和class,如果我們想加載其它位置的類(lèi)或jar時(shí)果录,比如:我要加載網(wǎng)絡(luò)上的一個(gè)class文件上枕,通過(guò)動(dòng)態(tài)加載到內(nèi)存之后,要調(diào)用這個(gè)類(lèi)中的方法實(shí)現(xiàn)我的業(yè)務(wù)邏輯弱恒。在這樣的情況下辨萍,默認(rèn)的ClassLoader就不能滿(mǎn)足我們的需求了,所以需要定義自己的ClassLoader返弹。定義自已的類(lèi)加載器分為兩步:
- 繼承java.lang.ClassLoader
- 重寫(xiě)父類(lèi)的findClass方法
有人可能有疑問(wèn)锈玉,ClassLoader類(lèi)有那么多方法,為什么偏偏只重寫(xiě)findClass
方法义起?因?yàn)镴DK已經(jīng)在loadClass
方法中幫我們實(shí)現(xiàn)了ClassLoader搜索類(lèi)的算法拉背,當(dāng)在loadClass方法中搜索不到類(lèi)時(shí),loadClass方法就會(huì)調(diào)用findClass方法來(lái)搜索類(lèi)默终,所以我們只需重寫(xiě)該方法即可椅棺。如沒(méi)有特殊的要求,一般不建議重寫(xiě)loadClass搜索類(lèi)的算法齐蔽;具體代碼示例見(jiàn)本文ClassLoader工作機(jī)制章節(jié)FileSystemClassLoader
類(lèi)的實(shí)現(xiàn)
其他
對(duì)于運(yùn)行在 Java EE?容器中的 Web 應(yīng)用來(lái)說(shuō)两疚,類(lèi)加載器的實(shí)現(xiàn)方式與一般的 Java 應(yīng)用有所不同。不同的 Web 容器的實(shí)現(xiàn)方式也會(huì)有所不同含滴。以 Apache Tomcat 來(lái)說(shuō)诱渤,每個(gè) Web 應(yīng)用都有一個(gè)對(duì)應(yīng)的類(lèi)加載器實(shí)例。該類(lèi)加載器也使用代理模式蛙吏,所不同的是它是自己首先嘗試去加載某個(gè)類(lèi)源哩,如果找不到再代理給父類(lèi)加載器鞋吉。這與一般類(lèi)加載器的順序是相反的鸦做。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得 Web 應(yīng)用自己的類(lèi)的優(yōu)先級(jí)高于 Web 容器提供的類(lèi)谓着。這種代理模式的一個(gè)例外是:Java 核心庫(kù)的類(lèi)是不在查找范圍之內(nèi)的泼诱。這也是為了保證 Java 核心庫(kù)的類(lèi)型安全
OSGi?是 Java 上的動(dòng)態(tài)模塊系統(tǒng)。它為開(kāi)發(fā)人員提供了面向服務(wù)和基于組件的運(yùn)行環(huán)境赊锚,并提供標(biāo)準(zhǔn)的方式用來(lái)管理軟件的生命周期治筒。OSGi 已經(jīng)被實(shí)現(xiàn)和部署在很多產(chǎn)品上,在開(kāi)源社區(qū)也得到了廣泛的支持舷蒲。Eclipse 就是基于 OSGi 技術(shù)來(lái)構(gòu)建的耸袜。OSGi 中的每個(gè)模塊(bundle)都包含 Java 包和類(lèi)。模塊可以聲明它所依賴(lài)的需要導(dǎo)入(import)的其它模塊的 Java 包和類(lèi)(通過(guò) Import-Package)牲平,也可以聲明導(dǎo)出(export)自己的包和類(lèi)堤框,供其它模塊使用(通過(guò) Export-Package)。也就是說(shuō)需要能夠隱藏和共享一個(gè)模塊中的某些 Java 包和類(lèi)。這是通過(guò) OSGi 特有的類(lèi)加載器機(jī)制來(lái)實(shí)現(xiàn)的蜈抓。OSGi 中的每個(gè)模塊都有對(duì)應(yīng)的一個(gè)類(lèi)加載器启绰。它負(fù)責(zé)加載模塊自己包含的 Java 包和類(lèi)。當(dāng)它需要加載 Java 核心庫(kù)的類(lèi)時(shí)(以 java開(kāi)頭的包和類(lèi))沟使,它會(huì)代理給父類(lèi)加載器(通常是啟動(dòng)類(lèi)加載器)來(lái)完成委可。當(dāng)它需要加載所導(dǎo)入的 Java 類(lèi)時(shí),它會(huì)代理給導(dǎo)出此 Java 類(lèi)的模塊來(lái)完成加載腊嗡。模塊也可以顯式的聲明某些 Java 包和類(lèi)着倾,必須由父類(lèi)加載器來(lái)加載
線(xiàn)程上下文類(lèi)加載器(context ClassLoader)是從 JDK 1.2 開(kāi)始引入的。類(lèi)java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用來(lái)獲取和設(shè)置線(xiàn)程的上下文類(lèi)加載器叽唱。如果沒(méi)有通過(guò) setContextClassLoader(ClassLoader cl)
方法進(jìn)行設(shè)置的話(huà)屈呕,線(xiàn)程將繼承其父線(xiàn)程的上下文類(lèi)加載器。Java 應(yīng)用運(yùn)行的初始線(xiàn)程的上下文類(lèi)加載器是系統(tǒng)類(lèi)加載器棺亭。在線(xiàn)程中運(yùn)行的代碼可以通過(guò)此類(lèi)加載器來(lái)加載類(lèi)和資源虎眨。前面提到的類(lèi)加載器的代理模式并不能解決 Java 應(yīng)用開(kāi)發(fā)中會(huì)遇到的類(lèi)加載器的全部問(wèn)題。Java 提供了很多服務(wù)提供者接口(Service Provider Interface镶摘,SPI)嗽桩,允許第三方為這些接口提供實(shí)現(xiàn)。常見(jiàn)的 SPI 有 JDBC凄敢、JCE碌冶、JNDI、JAXP 和 JBI 等涝缝。這些 SPI 的接口由 Java 核心庫(kù)來(lái)提供扑庞,如 JAXP 的 SPI 接口定義包含在javax.xml.parsers
包中。這些 SPI 的實(shí)現(xiàn)代碼很可能是作為 Java 應(yīng)用所依賴(lài)的 jar 包被包含進(jìn)來(lái)拒逮,可以通過(guò)類(lèi)路徑(CLASSPATH)來(lái)找到罐氨,如實(shí)現(xiàn)了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類(lèi)滩援。如 JAXP 中的javax.xml.parsers.DocumentBuilderFactory
類(lèi)中的 newInstance()
方法用來(lái)生成一個(gè)新的 DocumentBuilderFactory
的實(shí)例栅隐。這里的實(shí)例的真正的類(lèi)是繼承 自 javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的實(shí)現(xiàn)所提供的玩徊。如在 Apache Xerces 中租悄,實(shí)現(xiàn)的類(lèi)是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而問(wèn)題在于恩袱,SPI 的接口是 Java 核心庫(kù)的一部分泣棋,是由引導(dǎo)類(lèi)加載器來(lái)加載的;SPI 實(shí)現(xiàn)的 Java 類(lèi)一般是由系統(tǒng)類(lèi)加載器來(lái)加載的畔塔。引導(dǎo)類(lèi)加載器是無(wú)法找到 SPI 的實(shí)現(xiàn)類(lèi)的潭辈,因?yàn)樗患虞d Java 的核心庫(kù)纪吮。它也不能代理給系統(tǒng)類(lèi)加載器,因?yàn)樗窍到y(tǒng)類(lèi)加載器的祖先類(lèi)加載器萎胰。也就是說(shuō)碾盟,類(lèi)加載器的代理模式無(wú)法解決這個(gè)問(wèn)題。線(xiàn)程上下文類(lèi)加載器正好解決了這個(gè)問(wèn)題技竟。如果不做任何的設(shè)置冰肴,Java 應(yīng)用的線(xiàn)程的上下文類(lèi)加載器默認(rèn)就是系統(tǒng)上下文類(lèi)加載器。在 SPI 接口的代碼中使用線(xiàn)程上下文類(lèi)加載器榔组,就可以成功的加載到 SPI 實(shí)現(xiàn)的類(lèi)熙尉。線(xiàn)程上下文類(lèi)加載器在很多 SPI 的實(shí)現(xiàn)中都會(huì)用到。