1. 程式人生 > >Java自定義類載入器與雙親委派模型[轉]

Java自定義類載入器與雙親委派模型[轉]

其實,雙親委派模型並不複雜。自定義類載入器也不難!隨便從網上搜一下就能搜出一大把結果,然後copy一下就能用。但是,如果每次想自定義類載入器就必須搜一遍別人的文章,然後複製,這樣顯然不行。可是自定義類載入器又不經常用,時間久了容易忘記。相信你經常會記不太清loadClass、findClass、defineClass這些函式我到底應該重寫哪一個?它們主要是做什麼的?本文大致分析了各個函式的流程,目的就是讓你看完之後,難以忘記!或者說,延長你對自定義類載入器的記憶時間!隨時隨地想自定義就自定義!

  1. 雙親委派模型
    關於雙親委派模型,網上的資料有很多。我這裡只簡單的描述一下,就當是複習。

1.1 什麼是雙親委派模型?
首先,先要知道什麼是類載入器。簡單說,類載入器就是根據指定全限定名稱將class檔案載入到JVM記憶體,轉為Class物件。如果站在JVM的角度來看,只存在兩種類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath引數指定的路徑中的類庫載入到記憶體中。

其他類載入器:由Java語言實現,繼承自抽象類ClassLoader。如:

  • 擴充套件類載入器(Extension ClassLoader):負責載入<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變數指定的路徑中的所有類庫。
  • 應用程式類載入器(Application ClassLoader)。負責載入使用者類路徑(classpath)上的指定類庫,我們可以直接使用這個類載入器。一般情況,如果我們沒有自定義類載入器預設就是用這個載入器。

雙親委派模型工作過程是:如果一個類載入器收到類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器完成。每個類載入器都是如此,只有當父載入器在自己的搜尋範圍內找不到指定的類時(即ClassNotFoundException),子載入器才會嘗試自己去載入。
在這裡插入圖片描述
類載入器的雙親委派模型
1.2 為什麼需要雙親委派模型?
為什麼需要雙親委派模型呢?假設沒有雙親委派模型,試想一個場景:

黑客自定義一個java.lang.String類,該String類具有系統的String類一樣的功能,只是在某個函式稍作修改。比如equals函式,這個函式經常使用,如果在這這個函式中,黑客加入一些“病毒程式碼”。並且通過自定義類載入器加入到JVM中。此時,如果沒有雙親委派模型,那麼JVM就可能誤以為黑客自定義的java.lang.String類是系統的String類,導致“病毒程式碼”被執行。

而有了雙親委派模型,黑客自定義的java.lang.String類永遠都不會被載入進記憶體。因為首先是最頂端的類載入器載入系統的java.lang.String類,最終自定義的類載入器無法載入java.lang.String類。

或許你會想,我在自定義的類載入器裡面強制載入自定義的java.lang.String類,不去通過呼叫父載入器不就好了嗎?確實,這樣是可行。但是,在JVM中,判斷一個物件是否是某個型別時,如果該物件的實際型別與待比較的型別的類載入器不同,那麼會返回false。

舉個簡單例子:

ClassLoader1、ClassLoader2都載入java.lang.String類,對應Class1、Class2物件。那麼Class1物件不屬於ClassLoad2物件載入的java.lang.String型別。

1.3 如何實現雙親委派模型?
雙親委派模型的原理很簡單,實現也簡單。每次通過先委託父類載入器載入,當父類載入器無法載入時,再自己載入。其實ClassLoader類預設的loadClass方法已經幫我們寫好了,我們無需去寫。

  1. 自定義類載入器
  2. 1幾個重要函式
    2.1.1 loadClass
    loadClass預設實現如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
再看看loadClass(String name, boolean resolve)函式:

複製程式碼
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            long t1 = System.nanoTime();
            c = findClass(name);

            // this is the defining class loader; record the stats
            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            sun.misc.PerfCounter.getFindClasses().increment();
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

}
複製程式碼
從上面程式碼可以明顯看出,loadClass(String, boolean)函式即實現了雙親委派模型!整個大致過程如下:

首先,檢查一下指定名稱的類是否已經載入過,如果載入過了,就不需要再載入,直接返回。
如果此類沒有載入過,那麼,再判斷一下是否有父載入器;如果有父載入器,則由父載入器載入(即呼叫parent.loadClass(name, false);).或者是呼叫bootstrap類載入器來載入。
如果父載入器及bootstrap類載入器都沒有找到指定的類,那麼呼叫當前類載入器的findClass方法來完成類載入。
話句話說,如果自定義類載入器,就必須重寫findClass方法!

2.1.1 find Class
findClass的預設實現如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看出,抽象類ClassLoader的findClass函式預設是丟擲異常的。而前面我們知道,loadClass在父載入器無法載入類的時候,就會呼叫我們自定義的類載入器中的findeClass函式,因此我們必須要在loadClass這個函式裡面實現將一個指定類名稱轉換為Class物件.

如果是是讀取一個指定的名稱的類為位元組陣列的話,這很好辦。但是如何將位元組陣列轉為Class物件呢?很簡單,Java提供了defineClass方法,通過這個方法,就可以把一個位元組陣列轉為Class物件啦~

2.1.1 defineClass
defineClass主要的功能是:

將一個位元組陣列轉為Class物件,這個位元組陣列是class檔案讀取後最終的位元組陣列。如,假設class檔案是加密過的,則需要解密後作為形參傳入defineClass函式。

defineClass預設實現如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
2.2 函式呼叫過程
上一節所提的函式呼叫過程如下:
在這裡插入圖片描述
自定義函式呼叫過程
2.3 簡單示例
首先,我們定義一個待載入的普通Java類:Test.java。放在com.huachao.cl包下:

public class Test {
public void hello() {
System.out.println(“恩,是的,我是由 " + getClass().getClassLoader().getClass()
+ " 載入進來的”);
}
}
複製程式碼
注意:

如果你是直接在當前專案裡面建立,待Test.java編譯後,請把Test.class檔案拷貝走,再將Test.java刪除。因為如果Test.class存放在當前專案中,根據雙親委派模型可知,會通過sun.misc.Launcher$AppClassLoader 類載入器載入。為了讓我們自定義的類載入器載入,我們把Test.class檔案放入到其他目錄。

在本例中,我們Test.class檔案存放的目錄如下:
在這裡插入圖片描述
class檔案目錄
接下來就是自定義我們的類載入器:

複製程式碼
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class Main {
static class MyClassLoader extends ClassLoader {
private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;

    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

};

public static void main(String args[]) throws Exception {
    MyClassLoader classLoader = new MyClassLoader("D:/test");
    Class clazz = classLoader.loadClass("com.huachao.cl.Test");
    Object obj = clazz.newInstance();
    Method helloMethod = clazz.getDeclaredMethod("hello", null);
    helloMethod.invoke(obj, null);
}

}
複製程式碼
最後執行結果如下:
恩,是的,我是由 class Main$MyClassLoader 載入進來的