1.概述

classloader 的作用是加載字節碼到jvm,有些情況下,我們比如使用插件模式,可能需要自定義從外部加載插件到jvm。這裏還有一個需要注意的是java的雙親委派機制。

2.實現過程

2.1.定義自定義classloader

package com.example.demo.loader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader {
    private String classPath;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        // 將包名轉換為路徑
        String fileName = classPath + File.separator +
                className.replace('.', File.separatorChar) + ".class";
        try (InputStream is = new FileInputStream(fileName);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while ((ch = is.read()) != -1) {
                baos.write(ch);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

這個代碼繼承 classloader,並重寫了findClass.

2.2.定義外部類

package com.example.demo.loader;

public class MyDynamicClass {
    public void sayHello() {
        System.out.println("Hello from dynamically loaded class!");
    }
}

2.3. 使用自定義classloader

package com.example.demo.loader;

public class ClassLoaderDemo {

    public static void main(String[] args) {
        try {
            // 指向 .class 文件的根目錄
            MyClassLoader loader = new MyClassLoader("D:\\work\\research\\demo\\target\\classes");

            // 加載類(注意使用全限定名)
            Class<?> clazz = loader.loadClass("com.example.demo.loader.MyDynamicClass");

            // 創建實例
            Object instance = clazz.getDeclaredConstructor().newInstance();

            // 調用方法
            clazz.getMethod("sayHello").invoke(instance);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這樣我們可以看到 MyDynamicClass 的輸出。
Hello from dynamically loaded class
這個時候,如果我將這個 MyDynamicClass 拷貝到D:\temp\class 目錄下。

我們更改代碼:
MyClassLoader loader = new MyClassLoader("D:\temp\class");
這個時候,我在修改一下MyDynamicClass 的輸出

package com.example.demo.loader;

public class MyDynamicClass {
    public void sayHello() {
        System.out.println("測試一下輸出!");
    }
}

這個時候在ClassLoaderDemo執行一下,我們發現輸出是"測試一下輸出!",並沒有使用D:\temp\class 下的class

這裏涉及到一個 java的雙親委派機制。

2.3 什麼是雙親委派機制

雙親委派機制是Java類加載器的一種工作模式,當一個類加載器收到類加載請求時,它首先不會自己去嘗試加載這個類,而是把請求委託給父類加載器去完成,依次向上遞歸,只有當父類加載器無法完成加載時,子加載器才會嘗試自己加載。

2.4 如果不想使用雙親機制

我們的類可以這麼修改一下

Class<?> clazz = loader.findClass("com.example.demo.loader.MyDynamicClass");

這個就是沒有走 loadClass 方法,直接調用 findClass方法,獲取到外部類。

2.5 雙親委派機制的實現原理代碼

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先,檢查請求的類是否已經被加載過
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 如果父加載器不為空,則委託給父加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    // 如果父加載器為空,則委託給啓動類加載器加載
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加載器無法加載,拋出ClassNotFoundException
            }

            if (c == null) {
                // 如果父加載器無法加載,則調用自己的findClass方法進行加載
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

從這個代碼我們就可以看出,因為 在classpath下已經找到了 實現類,所以就不會在使用自定義classload加載的類。
如果直接從class path 下刪除哪個 MyDynamicClass class 文件,我們可以看到自定義 classloader是生效的。