自定義類載入器驗證類載入機制

懦弱鼠鼠,畏懼困難發表於2021-08-23

自定義類載入器驗證類載入機制

全盤委託機制

當一個ClassLoader裝載一個類時,除非顯示地使用另一個ClassLoader,則該類所依賴及引用的類也由這個CladdLoader載入。

雙親委派機制

子類載入器如果沒有載入過該目標類,就先委託父類載入器載入該目標類,只有在父類載入器找不到位元組碼檔案的情況下才從自己的類路徑中查詢並裝載目標類。

幾個重要的函式

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;
    }
}

1. loadClass()

可以看出loadClass()方法中實現了雙親委派的機制,即找父載入器,如果找到,則呼叫父載入器的loadClass(),如果父載入器為NUll,則呼叫啟動類載入器,如果啟動類載入器與父載入器都無法載入類,則呼叫自己的findClass()方法。

2. findClass()

很明顯,如果要不改變雙親委派機制的話,只需要重寫findClass()方法,實現類載入的邏輯。

自定義類載入器

各級的類載入器關係如下

自定義載入器MyLoaderA

public class MyLoaderA extends ClassLoader{
    public MyLoaderA(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            if(!name.endsWith("A")){
                // A載入器只能載入A類
                throw new ClassNotFoundException();
            }
            System.out.println("A載入器正在進行載入");
            // 讀取類路徑下的class檔案
            String className = name.substring(name.lastIndexOf(".") + 1)+".class";
            InputStream inputStream = getClass().getResourceAsStream(className);
            if (inputStream == null){
                return super.loadClass(name, false);
            }
            byte[] bytes = new byte[inputStream.available()];

            inputStream.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException();
        }
    }
}

MyLoaderB和MyLoaderC與MyLoaderA類似,只是在if判斷處將A改成B和C

自定義需要被載入的類

public class A {
    private B b = new B();
}


public class B {
    private C c = new C();
}


public class C {
}

類A的內部引用了類B,類B的內部引用了類C。

測試類

public class Test {
    public static void main(String[] args) throws Exception {
        MyLoaderC myLoaderC = new MyLoaderC(null);
        MyLoaderB myLoaderB = new MyLoaderB(myLoaderC);
        MyLoaderA myLoaderA = new MyLoaderA(myLoaderB);

        Object o = myLoaderA.loadClass("com.available.A").newInstance();
    }
}

MyLoaderC的父載入器是null,也就是啟動類載入器,MyLoaderB的父載入器是MyLoaderC,MyLoaderA的父載入器是MyLoaderB。

猜想

首先進入myLoaderA的loadClass()方法,去尋找myLoaderA的父載入器,接著進入myLoaderB的loadClass(),尋找myLoaderB的父載入器,myLoaderC無法載入類A,丟擲異常給myLoaderB,myLoaderB也丟擲異常給myLoaderA,隨後進入myLoaderA的findClass()方法中。

其次要載入類B,還是上面一套方法,只不過在myLoaderC丟擲異常後,myLoaderB的findClass()載入了類B。

最後要載入類C,按照全盤委託機制,類B引用了類C,那麼類C將不會進入myLoaderA的loadClass()方法,而是進入myLoaderB的loadClass()方法,開啟雙親委派機制載入類。

debug驗證

  1. 尋找到了myLoaderA的父載入器

  1. 尋找到MyLoaderB的父載入器

  1. MyLoaderC無法載入拋異常給MyLoaderB

  1. MyLoaderB無法載入拋異常給MyLoaderA(這步省略,沒什麼看頭)

  2. myLoaderA載入類A

  1. 載入類B的步驟省略,關鍵看全盤委託機制,是否會進入myLoaderA的loadClass()方法中。

很明顯在載入類C時,並沒有進入myLoaderA的loadClass()中

初步結論

一個類在載入的時候,會從引用他的類的載入器自上開始執行雙親委派機制,也就是說,含有多層引用關係的類,被引用類的類載入器只能大於或等於引用類的載入器。

驗證結論

將類A,類B,類C的引用關係改變如下:

public class A {
    private C c = new C();
}

public class B {
}

public class C {
    private B b = new B();
}

類A引用類C,類C引用類B

猜想

當載入完類C之後,此時引用類為類C,被引用類為類B,按照上面得出的結論,被引用類的類載入器只能大於或等於引用類的載入器,此時大於等於MyLoaderC的載入器不能載入類B,會報錯。

驗證

猜想成功。

一種打破雙親委派機制的場景

原生的JDBC的使用,獲取資料庫連線使用的是 Connection conn = DriverManager.getConnection(xx,xx,xx),DriverManager時jdk提供的,自然使用最上層的啟動類載入器載入,而提供具體實現的是各大廠商如Mysql,按照上面的結論,啟動類載入器必然不能載入Mysql的jar包,如果不打破雙親委派,則會報錯。

執行緒上下文載入器

通過獲取到規定好的執行緒上下文載入器,就可以在任何地方使用這個載入器來載入類從而打破雙親委派機制。

// 獲得執行緒上下文載入器
Thread.currentThread().getContextClassLoader();

// 設定執行緒上下文載入器,如果沒有設定,則為系統類載入器。
Thread.currentThread().setContextClassLoader(ClassLoader cl);

最終結論

一個類在載入的時候,會從引用他的類的載入器自上開始執行雙親委派機制,也就是說,含有多層引用關係的類,被引用類的類載入器如果不指定特定的類載入器就只能大於或等於引用類的載入器。

相關文章