JAVA
類動態(tài)載入的實現(xiàn)
1
??????
前言
前段時間因為項目的需要,我搞了一套類似
Servlet
重新載入的一個框架,實現(xiàn)了類的動態(tài)載入過程。本文寫了一些我的學(xué)習(xí)成果以及心得供大家分享一下。
2
??????
類載入的原理
(下面引用網(wǎng)上的一篇文章):
當(dāng)
JVM
(
Java
虛擬機)啟動時,會形成由三個類加載器組成的初始類加載器層次結(jié)構(gòu):
??????? bootstrap classloader
???????????????? |
??????? extension classloader
???????????????? |
??????? system classloader
bootstrap classloader
-
引導(dǎo)(也稱為原始)類加載器,它負(fù)責(zé)加載
Java
的核心類。在
Sun
的
JVM
中,在執(zhí)行
java
的命令中使用
-Xbootclasspath
選項或使用
-D
選項指定
sun.boot.class.path
系統(tǒng)屬性值可以指定附加的類。這個加載器的是非常特殊的,它實際上不是
java.lang.ClassLoader
的子類,而是由
JVM
自身實現(xiàn)的。大家可以通過執(zhí)行以下代碼來獲得
bootstrap classloader
加載了那些核心類庫:
??? URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
??? for (int i = 0; i < urls.length; i++) {
????? System.out.println(urls
.toExternalForm());
??? }
在我的計算機上的結(jié)果為:
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
file:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
file:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
file:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
file:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
file:/C:/j2sdk1.4.1_01/jre/classes
這時大家知道了為什么我們不需要在系統(tǒng)屬性
CLASSPATH
中指定這些類庫了吧,因為
JVM
在啟動的時候就自動加載它們了。
extension classloader
-
擴展類加載器,它負(fù)責(zé)加載
JRE
的擴展目錄(
JAVA_HOME/jre/lib/ext
或者由
java.ext.dirs
系統(tǒng)屬性指定的)中
JAR
的類包。這為引入除
Java
核心類以外的新功能提供了一個標(biāo)準(zhǔn)機制。因為默認(rèn)的擴展目錄對所有從同一個
JRE
中啟動的
JVM
都是通用的,所以放入這個目錄的
JAR
類包對所有的
JVM
和
system classloader
都是可見的。在這個實例上調(diào)用方法
getParent()
總是返回空值
null
,因為引導(dǎo)加載器
bootstrap classloader
不是一個真正的
ClassLoader
實例。所以當(dāng)大家執(zhí)行以下代碼時:
??? System.out.println(System.getProperty("java.ext.dirs"));
??? ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
??? System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
結(jié)果為:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader
是
system classloader
的
parent
,而
bootstrap classloader
是
extension classloader
的
parent
,但它不是一個實際的
classloader
,所以為
null
。
system classloader
-
系統(tǒng)(也稱為應(yīng)用)類加載器,它負(fù)責(zé)在
JVM
被啟動時,加載來自在命令
java
中的
-classpath
或者
java.class.path
系統(tǒng)屬性或者
CLASSPATH
操作系統(tǒng)屬性所指定的
JAR
類包和類路徑。總能通過靜態(tài)方法
ClassLoader.getSystemClassLoader()
找到該類加載器。如果沒有特別指定,則用戶自定義的任何類加載器都將該類加載器作為它的父加載器。執(zhí)行以下代碼即可獲得:
??? System.out.println(System.getProperty("java.class.path"));
輸出結(jié)果則為用戶在系統(tǒng)屬性里面設(shè)置的
CLASSPATH
。
classloader
加載類用的是全盤負(fù)責(zé)委托機制。所謂全盤負(fù)責(zé),即是當(dāng)一個
classloader
加載一個
Class
的時候,這個
Class
所依賴的和引用的所有
Class
也由這個
classloader
負(fù)責(zé)載入,除非是顯式的使用另外一個
classloader
載入;委托機制則是先讓
parent
(父)類加載器
(
而不是
super
,它與
parent classloader
類不是繼承關(guān)系
)
尋找,只有在
parent
找不到的時候才從自己的類路徑中去尋找。此外類加載還采用了
cache
機制,也就是如果
cache
中保存了這個
Class
就直接返回它,如果沒有才從文件中讀取和轉(zhuǎn)換成
Class
,并存入
cache
,這就是為什么我們修改了
Class
但是必須重新啟動
JVM
才能生效的原因。
每個
ClassLoader
加載
Class
的過程是:
1.
檢測此
Class
是否載入過(即在
cache
中是否有此
Class
),如果有到
8,
如果沒有到
2
2.
如果
parent classloader
不存在(沒有
parent
,那
parent
一定是
bootstrap classloader
了),到
4
3.
請求
parent classloader
載入,如果成功到
8
,不成功到
5
4.
請求
jvm
從
bootstrap classloader
中載入,如果成功到
8
5.
尋找
Class
文件(從與此
classloader
相關(guān)的類路徑中尋找)。如果找不到則到
7.
6.
從文件中載入
Class
,到
8.
7.
拋出
ClassNotFoundException.
8.
返回
Class.
其中
5.6
步我們可以通過覆蓋
ClassLoader
的
findClass
方法來實現(xiàn)自己的載入策略。甚至覆蓋
loadClass
方法來實現(xiàn)自己的載入過程。
類加載器的順序是:
先是
bootstrap classloader
,然后是
extension classloader
,最后才是
system classloader
。大家會發(fā)現(xiàn)加載的
Class
越是重要的越在靠前面。這樣做的原因是出于安全性的考慮,試想如果
system classloader“
親自
”
加載了一個具有破壞性的
“java.lang.System”
類的后果吧。這種委托機制保證了用戶即使具有一個這樣的類,也把它加入到了類路徑中,但是它永遠(yuǎn)不會被載入,因為這個類總是由
bootstrap classloader
來加載的。大家可以執(zhí)行一下以下的代碼:
??? System.out.println(System.class.getClassLoader());
將會看到結(jié)果是
null
,這就表明
java.lang.System
是由
bootstrap classloader
加載的,因為
bootstrap classloader
不是一個真正的
ClassLoader
實例,而是由
JVM
實現(xiàn)的,正如前面已經(jīng)說過的。
下面就讓我們來看看
JVM
是如何來為我們來建立類加載器的結(jié)構(gòu)的:
sun.misc.Launcher
,顧名思義,當(dāng)你執(zhí)行
java
命令的時候,
JVM
會先使用
bootstrap classloader
載入并初始化一個
Launcher
,執(zhí)行下來代碼:
?? System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
結(jié)果為:
?? the Launcher's classloader is null (
因為是用
bootstrap classloader
加載
,
所以
class loader
為
null)
Launcher
會根據(jù)系統(tǒng)和命令設(shè)定初始化好
class loader
結(jié)構(gòu),
JVM
就用它來獲得
extension classloader
和
system classloader,
并載入所有的需要載入的
Class
,最后執(zhí)行
java
命令指定的帶有靜態(tài)的
main
方法的
Class
。
extension classloader
實際上是
sun.misc.Launcher$ExtClassLoader
類的一個實例,
system classloader
實際上是
sun.misc.Launcher$AppClassLoader
類的一個實例。并且都是
java.net.URLClassLoader
的子類。
讓我們來看看
Launcher
初試化的過程的部分代碼。
Launcher
的部分代碼:
public class Launcher? {
??? public Launcher() {
??????? ExtClassLoader extclassloader;
??????? try {
??????????? //
初始化
extension classloader
??????????? extclassloader = ExtClassLoader.getExtClassLoader();
??????? } catch(IOException ioexception) {
??????????? throw new InternalError("Could not create extension class loader");
??????? }
??????? try {
??????????? //
初始化
system classloader
,
parent
是
extension classloader
??????????? loader = AppClassLoader.getAppClassLoader(extclassloader);
??????? } catch(IOException ioexception1) {
??????????? throw new InternalError("Could not create application class loader");
??????? }
??????? //
將
system classloader
設(shè)置成當(dāng)前線程的
context classloader
(將在后面加以介紹)
??????? Thread.currentThread().setContextClassLoader(loader);
??????? ......
??? }
??? public ClassLoader getClassLoader() {
??????? //
返回
system classloader
??????? return loader;
??? }
}
extension classloader
的部分代碼:
static class Launcher$ExtClassLoader extends URLClassLoader {
??? public static Launcher$ExtClassLoader getExtClassLoader()
??????? throws IOException
??? {
??????? File afile[] = getExtDirs();
??????? return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
??? }
?? private static File[] getExtDirs() {
??????? //
獲得系統(tǒng)屬性
“java.ext.dirs”
??????? String s = System.getProperty("java.ext.dirs");
??????? File afile[];
??????? if(s != null) {
??????????? StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
??????????? int i = stringtokenizer.countTokens();
??????????? afile = new File;
??????????? for(int j = 0; j < i; j++)
??????????????? afile[j] = new File(stringtokenizer.nextToken());
??????? } else {
??????????? afile = new File[0];
??????? }
??????? return afile;
??? }
}
system classloader
的部分代碼:
static class Launcher$AppClassLoader extends URLClassLoader
{
??? public static ClassLoader getAppClassLoader(ClassLoader classloader)
??????? throws IOException
??? {
??????? //
獲得系統(tǒng)屬性
“java.class.path”
??????? String s = System.getProperty("java.class.path");
??????? File afile[] = s != null ? Launcher.access$200(s) : new File[0];
??????? return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
??? }
}
???
看了源代碼大家就清楚了吧,
extension classloader
是使用系統(tǒng)屬性
“java.ext.dirs”
設(shè)置類搜索路徑的,并且沒有
parent
。
system classloader
是使用系統(tǒng)屬性
“java.class.path”
設(shè)置類搜索路徑的,并且有一個
parent classloader
。
Launcher
初始化
extension classloader
,
system classloader
,并將
system classloader
設(shè)置成為
context classloader
,但是僅僅返回
system classloader
給
JVM
。
這里怎么又出來一個
context classloader
呢?它有什么用呢?我們在建立一個線程
Thread
的時候,可以為這個線程通過
setContextClassLoader
方法來指定一個合適的
classloader
作為這個線程的
context classloader
,當(dāng)此線程運行的時候,我們可以通過
getContextClassLoader
方法來獲得此
context classloader
,就可以用它來載入我們所需要的
Class
。默認(rèn)的是
system classloader
。利用這個特性,我們可以
“
打破
”classloader
委托機制了,父
classloader
可以獲得當(dāng)前線程的
context classloader
,而這個
context classloader
可以是它的子
classloader
或者其他的
classloader
,那么父
classloader
就可以從其獲得所需的
Class
,這就打破了只能向父
classloader
請求的限制了。這個機制可以滿足當(dāng)我們的
classpath
是在運行時才確定
,
并由定制的
classloader
加載的時候
,
由
system classloader(
即在
jvm classpath
中
)
加載的
class
可以通過
context classloader
獲得定制的
classloader
并加載入特定的
class(
通常是抽象類和接口
,
定制的
classloader
中是其實現(xiàn)
),
例如
web
應(yīng)用中的
servlet
就是用這種機制加載的
.
???
好了,現(xiàn)在我們了解了
classloader
的結(jié)構(gòu)和工作原理,那么我們?nèi)绾螌崿F(xiàn)在運行時的動態(tài)載入和更新呢?只要我們能夠動態(tài)改變類搜索路徑和清除
classloader
的
cache
中已經(jīng)載入的
Class
就行了,有兩個方案,一是我們繼承一個
classloader
,覆蓋
loadclass
方法,動態(tài)的尋找
Class
文件并使用
defineClass
方法來;另一個則非常簡單實用,只要重新使用一個新的類搜索路徑來
new
一個
classloader
就行了,這樣即更新了類搜索路徑以便來載入新的
Class
,也重新生成了一個空白的
cache(
當(dāng)然
,
類搜索路徑不一定必須更改
)
。
(結(jié)束)
?
以上所述,想必大家對
Jvm
類載入的原理有了一定的了解,大致也猜到實現(xiàn)的方法了吧。
?
3
??????
實現(xiàn)類的動態(tài)載入
結(jié)合上面說到,要
JVM
重新載入一個類,一共有
3
種辦法:(我加了一種)
1.
???????
重新寫一個新的路徑,更換包名或類名都是可行的方法。
2.
???????
自己寫一個類的尋找機制取代
SystemClassLoader
中的,再調(diào)用
defineClass
方法。
3.
???????
覆蓋
loadClass
方法,自己實現(xiàn)一個類載入過程。
?
顯而易見,第一種方法是我自己添加上去的,而且應(yīng)該是用得最多得方法,因為在對
JVM
的類搜索、載入機制不是很熟悉的前提下,只要做一些簡單的修改就能達(dá)到所要的效果。(至少在我的項目中,這是首選,因為不會出錯)
但如果第一種方法能實現(xiàn),本文就沒有寫的意義了。在一個
7 x 24
的大型系統(tǒng)中(如電信,銀行等),用第一種方法,就凸現(xiàn)出它最大問題,就是代碼變得無比龐大。因為每一個類的名字不同導(dǎo)致相同功能的類具有兩個或以上的類文件存在,對代碼的開發(fā)和維護(hù)來說,這是非常嚴(yán)重的問題。
好,現(xiàn)在又退回到開始,排除掉第一種方法,如果使用第三種方法,應(yīng)該是可控性最高的方法,但本人才疏學(xué)淺,沒有把握把整個類載入過程摸透,因此還是排除的第三種方法。
剩下只有第二種方法了。在
ClassLoader
中,
defineClass
方法是比較好理解的,只要傳入類的字節(jié)流即可,因此,我只要重寫一次類文件的載入。下面是我的實現(xiàn)代碼:
public class testClassLoader extends ClassLoader {
??? private static testClassLoader cl = null;
??? private static boolean flag = true;
??? private InputStream classFile = null;
??? private String name = null;
??
??? /**
???? * @param name String?
類全名
???? * @param url URL?
類路徑
???? * @throws FileNotFoundException
???? * @throws IOException
???? */
??? public testClassLoader(String name, URL url) throws
??????????? FileNotFoundException, IOException {
??????? super(getSystemClassLoader());
??????? this.name = name + ".class";
?
??????? //
打開
URL
指定的資源
??????? URLConnection con = url.openConnection();
??????? InputStream classIs = con.getInputStream();
??????? this.classFile = classIs;
??????? ByteArrayOutputStream baos = new ByteArrayOutputStream();
??????? byte buf[] = new byte[1024];
??????? //
讀取文件流
??????? for (int i = 0; (i = classIs.read(buf)) != -1; ) {
??????????? baos.write(buf, 0, i);
????
???}
??????? classIs.close();
??????? baos.close();
?
??????? //
創(chuàng)建新的類對象
??????? byte[] data = baos.toByteArray();
??????? defineClass(name, data, 0, data.length);
??? }
?
??? /**
???? *
重載
getResourceAsStream()
是為了返回該類的文件流。
???? * @return an InputStream of the class bytes, or null
???? */
??? public InputStream getResourceAsStream(String resourceName) {
??????? try {
??????????? if (resourceName.equals(name)) {
??????????????? return this.classFile;
??????????? }
??????? } catch (Exception e) {
??????????? return null;
??????? }
??????? return null;
??? }
}
??????
相信大家已經(jīng)明白了吧。我在
ClassLoader
構(gòu)造時便載入了指定的類文件,因此,就跳過了在
new
新的對象時去查找
Cache
中該類是否有載入的過程。(注明一下:我的類路徑用了
URL
的方式傳入,目的是為了適應(yīng)多平臺的機制,因為
Window
和
Linux
的路徑命名不一樣)
相信聰明的你一定猜到后面應(yīng)該怎么做了吧,沒錯,就是用
Class.forName
構(gòu)建類:
String name = "test.testBlank";
?????? URL url = new URL("file:/c:/test/classes/test/testBlank.class");
ClassLoader cl = new testClassLoader(name, url);
Class c = Class.forName(name, false, cl);
//
實例化
Object obj = c.newInstance();
testInterface i = (testInterface) obj;
i.run();
?
??????
你一定注意到了,為什么我用了一個
testInterface
,其實自定義
ClassLoader
中,不能直接以當(dāng)前的類去直接強制轉(zhuǎn)換,因為在
JVM
中,不同的
ClassLoader
載入同一個類,在
JVM
中是兩個完全不同的類。因此,默認(rèn)的
class
文件都是
SystemClassLoader
,所以會拋出
ClassCastException
。解決方法很簡單,只要該對象實現(xiàn)一個接口,然后用父類去強制轉(zhuǎn)換就沒有問題了。
?
??????
以上只是我在學(xué)習(xí)過程中的一點積累,如果有什么錯誤,歡迎大家評論。
?