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
當然,這樣做還是會被反編譯破解,要加大難度,還需要其他處理的。