【Java類加載】自定義類加載器


要自定義自己的類加載器來加載類,需要先對類加載器和類加載機制有一些基本的了解。

1、類加載器

類加載器ClassLoader的作用有兩個:

①是用於將class文件加載到JVM。
②是用於判斷JVM運行時兩個類是否相等。

2、類加載的時機

類的加載可分為隱式加載和顯示加載。

隱式加載

隱式加載包括以下幾種情況:

  • 遇到new(new 一個實例對象的時候)、getstatic(獲取一個類的靜態字段的時候)、putstatic(設置一個類的靜態字段的時候)、invokestatic(調用一個類的靜態方法的時候)這4條字節碼指令時。
  • 對類進行反射調用時。
  • 初始化一個類時,如果父類還沒有初始化,則先加載其父類並初始化(但是初始化接口時,不要求先初始化父接口)
  • 虛擬機啟動時,需要指定一個包含main函數的主類,優先加載並初始化這個主類。

顯式加載

顯示加載包含以下幾種情況:

  • 通過Class.forName()加載
  • 通過ClassLoader的loaderClass方法加載
  • 通過ClassLoader的findClass方法

3、Class.forName()加載類

Class.forName()和ClassLoader都可以對類進行加載。ClassLoader就是遵循雙親委派模型最終調用啟動類加載器的類加載器,實現的功能是“通過一個類的全限定名來獲取描述此類的二進制字節流”,獲取到二進制流后放到JVM中。Class.forName()方法實際上也是調用的CLassLoader來實現的。

先看看Class.forName()的源碼:

/**
  * 參數解釋:
  * 1、className:要加載的類名
  * 2、true:class被加載后是否要被初始化。初始化即執行static的代碼(靜態代碼)
  * 3、caller:指定類加載器
  */
public static Class<?> forName(String className) throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以看出來最后正在實現forName()方法的是forName0這一個native方法。而使用forName0需要傳遞的參數之一,就是ClassLoader類加載器。因此可以知道Class.forName()本質還是使用classloader來進行類的加載的。

4、使用ClassLoader加載類

ClassLoader 里面有三個重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加載目標類的入口,它首先會查找當前ClassLoader以及它的父類classloader里面是否已經加載了目標類,如果沒有找到就會讓父類Classloader嘗試加載,如果父類classloader都加載不了,就會調用findClass()讓自定義加載器自己來加載目標類。這實際上就是雙親委派機制的原理。

看一下loadClass()的源碼:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //查看類是否已被加載,findLoadedClass最后調用native方法findLoadedClass0
            Class<?> c = findLoadedClass(name);
            //還未加載
            if (c == null) { 
                long t0 = System.nanoTime();
                try {
                    //若有父類加載器,則調用其loadClass(),請求父類進行加載
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //若父類加載器為null,說明父類為BootstrapClassLoader(該類加載器無法被Java程序直接使用,用null代替即可),請求它來加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    //加載失敗,拋出ClassNotFoundException異常
                }
                //父類無法加載,調用findClass方法,嘗試自己加載這個類
                //注意:在這個findClass方法中,目前只是拋出一個異常,沒有任何進行類加載的動作
                //因此,想要自己進行類加載,就要重寫findClass()方法。
                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;
        }
    }

ClassLoader 的 findClass() 方法是需要子類來覆蓋重寫的,不同的加載器將使用不同的邏輯來獲取目標類的字節碼。得到字節碼之后會調用 defineClass() 方法將字節碼轉換成 Class 對象。

findClass()的源碼如下:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name); //僅拋出異常
    }

可見,ClassLoader的findClass()方法是沒有具體實現的,如果要自定義類加載器,就需要重寫findClass()方法,並且配合defineClass() 方法一起使用,defineClass()方法是用來將byte字節流解析成JVM能夠識別的Class對象。

以上就是CLassLoader進行類加載的簡單流程。

雖然Class.forName()方法本質上還是使用Classloader來進行類的加載的,但它和使用Classloader來進行類加載依然有着區別:

①Class.forName()方法除了將類的字節碼加載到jvm中之外,還會執行類中的static塊,即會導致類的初始化。Class.forName(name, initialize, loader)帶參方法也可以指定是否進行初始化,執行靜態塊。

②ClassLoader只是將類的字節碼加載到jvm中,不會執行static中的內容,即不會進行類加載,只有在newInstance才會去執行static塊。

5、自定義類加載器

我們知道,除了BootstrapClassLoader是由C/C++實現的,其他的類加載器都是ClassLoader的子類。所以如果我們想實現自定義的類加載器,首先要繼承ClassLoader。

根據我們前面的分析,ClassLoader進行類加載的核心實現就在loadClass()方法中。再根據loadClass()方法的源碼,我們可以知道有兩種方式來實現自定義的類加載,分別如下:

①如果不想打破雙親委派機制,那么只需要重寫findClass方法。
②如果想要打破雙親委派機制,那么就需要重寫整個loadClass方法。

如果沒有特殊要求,Java官方推薦重寫findClass方法,而不是重寫整個loadClass方法。這樣既讓我們能夠按照自己的意願加載類,也能保證自定義的類加載器符合雙親委派機制。

明確了如何實現,我們只需要兩步就可以實現自定義的類加載器:第一步是繼承classloader,第二步是重寫findClass方法。

不過由於在findClass()內需要調用defineClass()方法將字節數組轉換成Class類對象,因此要先對輸入的class文件做一些處理,使其變為字節數組。

實現自定義的類加載器:

public class MyClassLoader extends ClassLoader{
    //默認ApplicationClassLoader為父類加載器
    public MyClassLoader(){
        super();
    }

    //加載類的路徑
    private String path = "";

    //重寫findClass,調用defineClass,將代表類的字節碼數組轉換為Class對象
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] dataByte = new byte[0];
        try {
            dataByte = ClassDataByByte(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return this.defineClass(name, dataByte, 0, dataByte.length);
    }

    //讀取Class文件作為二進制流放入byte數組, findClass內部需要加載字節碼文件的byte數組
    private byte[] ClassDataByByte(String name) throws IOException {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
        name = name.replace(".", "/"); // 為了定位class文件的位置,將包名的.替換為/
        is = new FileInputStream(new File(path + name + ".class"));
        int c = 0;
        while (-1 != (c = is.read())) { //讀取class文件,並寫入byte數組輸出流
            arrayOutputStream.write(c);
        }
        data = arrayOutputStream.toByteArray(); //將輸出流中的字節碼轉換為byte數組
        is.close();
        arrayOutputStream.close();
        return data;
    }
}

使用自定義的類加載器:

public static void main(String[] args) {
    MyClassLoader myClassLoader = new MyClassLoader();
    Class<?> clazz = myClassLoader.loadClass("com.fengjian.www.MyClassLoader");
    clazz.newInstance();
}

由於能力有限,可能存在錯誤,感謝指出。以上內容為本人在學習過程中所做的筆記。參考的書籍、文章或博客如下:
[1]Mr羽墨青衫.深入分析Java ClassLoader原理.知乎.https://zhuanlan.zhihu.com/p/81759029
[2]愚公要移山.學了這么久的java反射機制,你知道class.forName和classloader的區別嗎?.知乎.https://zhuanlan.zhihu.com/p/101114197


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM