Java類加載器及自定義

Java類加載器的作用是尋找類文件,然後加載Class字節碼到JVM內存中,鏈接(驗證、準備、解析)並初始化,最終形成可以被虛擬機直接使用的Java類型。
圖片描述

類加載器種類

有兩種類加載器:
1 啓動類加載器(Bootstrap ClassLoader)
由C++語言實現(針對HotSpot VM),負責將存放在<JAVA_HOME>lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到JVM內存中,像java.lang.、java.util.、java.io.*等等。可以通過vm參數“-XX:+TraceClassLoading”來獲取類加載信息。我們無法直接使用該類加載器。

2 其他類加載器(Java語言實現)
1)擴展類加載器(Extension ClassLoader)
負責加載<JAVA_HOME>libext目錄或java.ext.dirs系統變量指定的路徑中的所有類庫。我們可以直接使用這個類加載器。
2)應用程序類加載器(Application ClassLoader),或者叫系統類加載器
負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。
3)自定義類加載器
通過繼承ClassLoader類實現,主要重寫findClass方法。

類加載器使用順序

在JVM虛擬機中,如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每個類加載器都是如此,只有當父加載器在自己的搜索範圍內找不到指定的類時(即ClassNotFoundException),子加載器纔會嘗試自己去加載。

也就是說,對於每個類加載器,只有父類(依次遞歸)找不到時,才自己加載 。這就是雙親委派模型

圖片描述

爲什麼需要雙親委派模型呢?這可以提高Java的安全性,以及防止程序混亂。
提高安全性方面:
假設我們使用一個第三方Jar包,該Jar包中自定義了一個String類,它的功能和系統String類的功能相同,但是加入了惡意代碼。那麼,JVM會加載這個自定義的String類,從而在我們所有用到String類的地方都會執行該惡意代碼。
如果有雙親委派模型,自定義的String類是不會被加載的,因爲最頂層的類加載器會首先加載系統的java.lang.String類,而不會加載自定義的String類,防止了惡意代碼的注入。

防止程序混亂
假設用戶編寫了一個java.lang.String的同名類,如果每個類加載器都自己加載的話,那麼會出現多個String類,導致混亂。如果本加載器加載了,父加載器則不加載,那麼以哪個加載的爲準又不能確定了,也增加了複雜度。

自定義類加載器

我們可以自定義類加載器,只需繼承ClassLoader抽象類,並重寫findClass方法(如果要打破雙親委派模型,需要重寫loadClass方法)。原因可以查看ClassLoader的源碼:

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

這個是ClassLoader中的loadClass方法,大致流程如下:
1)檢查類是否已加載,如果是則不用再重新加載了;
2)如果未加載,則通過父類加載(依次遞歸)或者啓動類加載器(bootstrap)加載;
3)如果還未找到,則調用本加載器的findClass方法;
以上可知,類加載器先通過父類加載,父類未找到時,纔有本加載器加載。

因爲自定義類加載器是繼承ClassLoader,而我們再看findClass方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {    
    throw new ClassNotFoundException(name);
}

可以看出,它直接返回ClassNotFoundException。
因此,自定義類加載器必須重寫findClass方法。

自定義類加載器示例代碼:
類加載器HClassLoader:

class HClassLoader extends ClassLoader {

private String classPath;

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

@Override
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();
    }

}

/**
 * 獲取.class的字節流
 *
 * @param name
 * @return
 * @throws Exception
 */
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();

    // 字節流解密
    data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);

    return data;
  }
}

被加載的類Car:

public class Car {

    public Car() {
        System.out.println("Car:" + getClass().getClassLoader());
        System.out.println("Car Parent:" + getClass().getClassLoader().getParent());
    }

    public String print() {
        System.out.println("Car:print()");
        return "carPrint";
    }
 }

測試代碼:

@Test
public void testClassLoader() throws Exception {
    HClassLoader myClassLoader = new HClassLoader("e:/temp/a");
    Class clazz = myClassLoader.loadClass("com.ha.Car");
    Object o = clazz.newInstance();
    Method print = clazz.getDeclaredMethod("print", null);
    print.invoke(o, null);
}

以上代碼,展示了自定義類加載器加載類的方法。

需要注意的是:
執行測試代碼前,必須將Car.class文件移動到e:/temp/a下,並且按包名建立層級目錄(這裏爲com/ha/)。因爲如果不移動Car.class文件,那麼Car類會被AppClassLoader加載(自定義類加載器的parent是AppClassLoader)。

自定義類加載器的應用

上面介紹了Java類加載器的相關知識。對於自定義類加載器,哪裏可以用到呢?
主流的Java Web服務器,比如Tomcat,都實現了自定義的類加載器。因爲它要解決幾個問題:
1)Tomcat上可以部署多個不同的應用,但是它們可以使用同一份類庫的不同版本。這就需要自定義類加載器,以便對加載的類庫進行隔離,否則會出現問題;
2)對於非.class的文件,需要轉爲Java類,就需要自定義類加載器。比如JSP文件。

這裏舉一個其它的例子:Java核心代碼的加密。
假設我們項目當中,有一些核心代碼不想讓別人反編譯看到。當前知道有兩種方法,一種是通過代碼混淆(推薦Allatori,商用收費);一種是自己編寫加密算法,對字節碼加密,加大反編譯難度。

代碼混淆如果用Allatori,比較簡便。注意控制自己編寫類的訪問權限即可。接口用public,內部方法用private,其他的用默認的(即不加訪問修飾符)或者protected。代碼混淆這裏不過多說明,這裏主要介紹一下字節碼加密。
大概的流程可以如下:
圖片描述

.class加密代碼:

@Test
public void testEncode() {
    String classFile = "e:/temp/a/com/ha/Car.class";
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(classFile);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();

        data = DESInstance.enCode("1234567890qwertyuiopasdf".getBytes(), data);

        String outFile = "e:/temp/a/com/ha/EnCar.class";
        FileOutputStream fos = new FileOutputStream(outFile);
        fos.write(data);
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }

}

類加載器中解密,查看上文中的:

// 字節流解密
data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);

加解密工具類:

public class DESInstance {

    private static String ALGORITHM = "DESede";

    /**
     * 加密
     *
     * @param key
     * @param src
     * @return
     */
    public static byte[] enCode(byte[] key, byte[] src) {

        byte[] value = null;
        SecretKey deskey = new SecretKeySpec(key, ALGORITHM);
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, deskey);
            value = cipher.doFinal(src);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return value;
    }

    /**
     * 解密
     *
     * @param key
     * @param src
     * @return
     */
    public static byte[] deCode(byte[] key, byte[] src) {
        byte[] value = null;
        SecretKey deskey = new SecretKeySpec(key, ALGORITHM);

        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, deskey);
            value = cipher.doFinal(src);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return value;
    }
}

注意祕鑰是24位,否則會報錯:
java.security.InvalidKeyException: Invalid key length
如果解密密碼錯誤,則是如下錯誤:
javax.crypto.BadPaddingException: Given final block not properly padded

當然,這樣做還是會被反編譯破解,要加大難度,還需要其他處理的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章