天天看點

Java自定義類加載器全解

作者:全棧行動派

部落客強烈推薦:

Java17中文文檔:Overview - Java17中文文檔 - API參考文檔 - 全棧行動派

Linux線上查詢工具:Linux指令查詢 - 全棧工具箱 - 線上工具箱 - 免費實用工具大全

Gradle最新中文文檔:Gradle使用者手冊 - Gradle8.1.1中文文檔 - API參考文檔 - 全棧行動派Gradlegithubgradle

更多文章參見自己的部落格網站:全棧行動派-全棧技術部落格

1、為什麼要自定義類加載器呢?有什麼好處

①、隔離加載類

在某些架構内進行中間件與應用的子產品隔離,把類加載到不同的環境。比如:阿裡内某容器架構通過自定義類加載器確定應用中依賴的jar包不會影響到中間件運作時使用的jar包。再比如:Tomcat這類Web應用伺服器,内部自定義了好幾種類加載器,用于隔離同一個Web應用伺服器上的不同應用程式。

兩個jar包内都存在相同類名且包名相同,如果沒有隔離加載類,則會報錯,如:兩個版本的jar

②、修改類加載方式

類的加載模型并非強制,除Bootstrap外,其他的加載并非一定要引入,或者根據實際情況在某個時間點進行按需進行動态加載

③、擴充加載源

比如從資料庫、網絡、甚至是電視機機頂盒進行加載

④、防止源碼洩露

Java代碼容易被編譯和篡改,可以進行編譯加密。那麼類加載也需要自定義,還原加密的位元組碼。

通常Java系統想增加License(授權),就可以通過自定義類加載器實作。

具體實作我們在最後面測試。

2、自定義類加載器的使用場景

①、實作類似程序内隔離,類加載器實際上用作不同的命名空間,以提供類似容器、子產品化的效果。例如,兩個子產品依賴于某個類庫的不同版本,如果分别被不同的容器加載,就可以互不幹擾。這個方面的集大成者是JavaEE和OSGI、JPMS等架構。

②、應用需要從不同的資料源擷取類定義資訊,例如網絡資料源,而不是本地檔案系統。或者是需要自己操縱位元組碼,動态修改或者生成類型。

3、類加載器注意點

在一般情況下,使用不同的類加載器去加載不同的功能子產品,會提高應用程式的安全性。但是,如果涉及Java類型轉換,則加載器反而容易産生不美好的事情。在做Java類型轉換時,隻有兩個類型都是由同一個加載器所加載,才能進行類型轉換,否則轉換時會發生異常。

4、自定義類加載器實作方式

Java提供了抽象類java.lang.ClassLoader,所有使用者自定義的類加載器都應該繼承ClassLoader類。

在自定義ClassLoader的子類時候,我們常見的會有兩種做法:

  • 方式一:重寫loadClass()方法
  • 方式二:重寫findClass()方法

對比

這兩種方法本質上差不多,畢竟loadClass()也會調用findClass(),但是從邏輯上講我們最好不要直接修改loadClass()的内部邏輯。建議的做法是隻在findClass()裡重寫自定義類的加載方法,根據參數指定類的名字,傳回對應的Class對象的引用。

loadClass()這個方法是實作雙親委派模型邏輯的地方,擅自修改這個方法會導緻模型被破壞,容易造成問題。同時,也避免了自己重寫loadClass()方法的過程中必須寫雙親委托的重複代碼,從代碼的複用性來看,不直接修改這個方法始終是比較好的選擇。

當編寫好自定義類加載器後,便可以在程式中調用loadClass()方法來實作類加載操作。

ClassLoader類中loadClass()方法源碼,可以看出内部調用了findClass()方法

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();
                    // 通過類的全限定名稱(加包名)調用findClass方法查找類
                    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;
        }
    }           

說明

  • 自定義加載器的父類加載器是系統類加載器
  • JVM中的所有類加載都會使用java.lang.ClassLoader.loadClass(String)接口(自定義類加載器并重寫java.lang.ClassLoader.loadClass(String)接口的除外),連JDK的核心類庫也不能例外。

5、通過重寫findClass()方法實作自定義類加載器

①、自定義類com.lc.Demo

package com.lc;

/**
 * @author liuchao
 * @date 2023/3/25
 */
public class Demo {

    public void hello() {
        System.out.println("myClassLoader hello");
    }
}
           

通過javac Demo 将 類編譯為Demo.class 檔案放入/Users/liuchao/Desktop/檔案夾下

②、自定義類加載器MyClassLoader

package com.lc;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * 自定義ClassLoader
 *
 * @author liuchao
 * @date 2023/3/25
 */
public class MyClassLoader extends ClassLoader {
    /**
     * 負責加載的類所屬目錄
     */
    public String classPath;

    /**
     * 包名
     */
    public String packageName;

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

    @Override
    protected Class<?> findClass(String name) {
        //擷取位元組碼完整路徑
        String fileName = classPath + name + ".class";
        ByteArrayOutputStream baos = null;
        BufferedInputStream bis = null;
        try {
            //擷取輸入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //擷取輸出流
            baos = new ByteArrayOutputStream();

            //讀取資料寫入輸出流
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //擷取記憶體中完整的位元組素組資料
            byte[] byteCodes = baos.toByteArray();
            //通過調用defineClass 方法将位元組數組轉換為class的執行個體
            return defineClass(packageName + "." + name, byteCodes, 0, byteCodes.length);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (null != baos) {
                    baos.close();
                }
                if (null != bis) {
                    bis.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
           

③、寫測試代碼MyClassLoaderTest

package com.lc;

import java.lang.reflect.Method;

/**
 * 測試
 *
 * @author liuchao
 * @date 2023/3/25
 */
public class MyClassLoaderTest {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("com.lc", "/Users/liuchao/Desktop/");

        Class clazz = classLoader.loadClass("Demo");

        System.out.println("目前Demo類的加載器為:" + clazz.getClassLoader().getClass().getName());
        System.out.println("目前Demo類的加載器的父類加載器為:" + clazz.getClassLoader().getClass().getClassLoader().getClass().getName());

        Method method = clazz.getMethod("hello");

        Object obj = clazz.newInstance();

        method.setAccessible(Boolean.TRUE);
        method.invoke(obj);
    }
}
           

執行效果:

Java自定義類加載器全解

至此我們的自定義類加載器就算完成了

6、自定義類加載器實作Java license

我們改造第5節的代碼

①、增加位元組碼加密方法

@Test
    public void test01() {
        //秘鑰
        String secretKey = "12asdfwe23123212";
        String className = "Demo";
        //原位元組碼
        byte[] byteCodes = FileUtil.readBytes("/Users/liuchao/Desktop/" + className + ".class");
        //加密後的位元組碼
        byte[] encryptByteCodes = SecureUtil.aes(secretKey.getBytes()).encrypt(byteCodes);
        //加密後的位元組碼重新生成新的class檔案放入 temp目錄下
        FileUtil.writeBytes(encryptByteCodes, "/Users/liuchao/Desktop/temp/" + className + ".class");


    }           

②、改造MyClassLoader類

package com.lc;

import cn.hutool.crypto.SecureUtil;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * 自定義ClassLoader
 *
 * @author liuchao
 * @date 2023/3/25
 */
public class MyClassLoader extends ClassLoader {
    /**
     * 負責加載的類所屬目錄
     */
    public String classPath;

    /**
     * 包名
     */
    public String packageName;

    /**
     * 秘鑰
     */
    public String secretKey;

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

    @Override
    protected Class<?> findClass(String name) {
        //擷取位元組碼完整路徑
        String fileName = classPath + name + ".class";
        ByteArrayOutputStream baos = null;
        BufferedInputStream bis = null;
        try {
            //擷取輸入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //擷取輸出流
            baos = new ByteArrayOutputStream();

            //讀取資料寫入輸出流
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //擷取記憶體中完整的位元組素組資料 (加密的)
            byte[] encryptByteCodes = baos.toByteArray();

            //解密傳回
            byte[] byteCodes = SecureUtil.aes(secretKey.getBytes()).decrypt(encryptByteCodes);
            //通過調用defineClass 方法将位元組數組轉換為class的執行個體
            return defineClass(packageName + "." + name, byteCodes, 0, byteCodes.length);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (null != baos) {
                    baos.close();
                }
                if (null != bis) {
                    bis.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
           

③、調用測試

package com.lc;

import java.lang.reflect.Method;

/**
 * 測試
 *
 * @author liuchao
 * @date 2023/3/25
 */
public class MyClassLoaderTest {
    public static void main(String[] args) throws Exception {
        String secretKey = "12asdfwe23123212";
        MyClassLoader classLoader = new MyClassLoader(secretKey, "com.lc", "/Users/liuchao/Desktop/temp/");

        Class clazz = classLoader.loadClass("Demo");

        System.out.println("目前Demo類的加載器為:" + clazz.getClassLoader().getClass().getName());
        System.out.println("目前Demo類的加載器的父類加載器為:" + clazz.getClassLoader().getClass().getClassLoader().getClass().getName());

        Method method = clazz.getMethod("hello");

        Object obj = clazz.newInstance();

        method.setAccessible(Boolean.TRUE);
        method.invoke(obj);
    }
}
           

效果:

Java自定義類加載器全解

在未解密之前,想打開加密後的類,效果:

Java自定義類加載器全解

當然現實中,需要有秘鑰過期時間的限制,大家可以在此基礎上繼續擴充。

繼續閱讀