自定義JAVA類載入器
技術標籤:java/J2SE
轉自:http://blog.csdn.net/seu_calvin/article/details/52315125
(問:自定義類載入器怎麼實現,其中哪個方法走雙親委派模型,(實現findclass方法,一般用defineclass載入外部類),如何才能不走雙親委派。(重寫loadclass方法))
三個重要函式:loadClass,findClass,defineClass
loadClass:呼叫父類載入器的loadClass,載入失敗則呼叫自己的findClass方法
findClass:根據名稱讀取檔案存入位元組陣列
defineClass:把一個位元組陣列轉為Class物件
0. 為什麼需要自定義類載入器
網上的大部分自定義類載入器文章,幾乎都是貼一段實現程式碼,然後分析一兩句自定義ClassLoader的原理。但是我覺得首先得把為什麼需要自定義載入器這個問題搞清楚,因為如果不明白它的作用的情況下,還要去學習它顯然是很讓人困惑的。
首先介紹自定義類的應用場景:
(1)加密:Java程式碼可以輕易的被反編譯,如果你需要把自己的程式碼進行加密以防止反編譯,可以先將編譯後的程式碼用某種加密演算法加密,類加密後就不能再用Java的ClassLoader去載入類了,這時就需要自定義ClassLoader在載入類的時候先解密類,然後再載入。
(2)從非標準的來源載入程式碼:如果你的位元組碼是放在資料庫、甚至是在雲端,就可以自定義類載入器,從指定的來源載入類。
(3)以上兩種情況在實際中的綜合運用:比如你的應用需要通過網路來傳輸 Java 類的位元組碼,為了安全性,這些位元組碼經過了加密處理。這個時候你就需要自定義類載入器來從某個網路地址上讀取加密後的位元組程式碼,接著進行解密和驗證,最後定義出在Java虛擬機器中執行的類。
1. 雙親委派模型
在實現自己的ClassLoader之前,我們先了解一下系統是如何載入類的,那麼就不得不介紹雙親委派模型的實現過程。
//雙親委派模型的工作過程原始碼 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { 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 //子載入器進行類載入 c = findClass(name); } } if (resolve) {//判斷是否需要連結過程,引數傳入 resolveClass(c); } return c; }
雙親委派模型的工作過程如下:
(1)當前類載入器從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。
(2)如果沒有找到,就去委託父類載入器去載入(如程式碼c = parent.loadClass(name,false)所示)。父類載入器也會採用同樣的策略,檢視自己已經載入過的類中是否包含這個類,有就返回,沒有就委託父類的父類去載入,一直到啟動類載入器。因為如果父載入器為空了,就代表使用啟動類載入器作為父載入器去載入。
(3)如果啟動類載入器載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用拓展類載入器來嘗試載入,繼續失敗則會使用AppClassLoader來載入,繼續失敗則會丟擲一個異常ClassNotFoundException,然後再呼叫當前載入器的findClass()方法進行載入。
比如要載入自己寫的String類,自定義一個String類放在某路徑下,自定義一個類載入器繼承ClassLoader類,並實現findClass方法(在自己的路徑下去取String類)。重寫loadClass方法讓它不走雙親委派,這樣他就會直接呼叫findClass載入自己的String類了。
雙親委派模型的好處:
(1)主要是為了安全性,避免使用者自己編寫的類動態替換Java的一些核心類,比如String。
(2)同時也避免了類的重複載入,因為JVM中區分不同類,不僅僅是根據類名,相同的class檔案被不同的ClassLoader載入就是不同的兩個類。
2. 自定義類載入器
(1)從上面原始碼看出,呼叫loadClass時會先根據委派模型在父載入器中載入,如果載入失敗,則會呼叫當前載入器的findClass來完成載入。
(2)因此我們自定義的類載入器只需要繼承ClassLoader,並覆蓋findClass方法,下面是一個實際例子,在該例中我們用自定義的類載入器去載入我們事先準備好的class檔案。
2.1自定義一個People.java類做例子,.java編譯後生成.class,即二進位制位元組流檔案
public class People {
//該類寫在記事本里,在用javac命令列編譯成class檔案,放在d盤根目錄下
private String name;
public People() {}
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "I am a people, my name is " + name;
}
}
2.2自定義類載入器
自定義一個類載入器,需要繼承ClassLoader類,並實現findClass方法。其中defineClass方法可以把二進位制流位元組組成的檔案轉換為一個java.lang.Class(只要二進位制位元組流的內容符合Class檔案規範)。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader
{
public MyClassLoader()
{
}
public MyClassLoader(ClassLoader parent)
{
super(parent);
}
protected Class<?> findClass(String name) throws ClassNotFoundException
{
File file = new File("D:/People.class");
try{
byte[] bytes = getClassBytes(file);
//defineClass方法可以把二進位制流位元組組成的檔案轉換為一個java.lang.Class
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
}
catch (Exception e)
{
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(File file) throws Exception
{
// 這裡要讀入.class的位元組,因此要使用位元組流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true){
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
2.3在主函式裡使用
MyClassLoader mcl = new MyClassLoader();
Class<?> clazz = Class.forName("People", true, mcl);
Object obj = clazz.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());//打印出我們的自定義類載入器
2.4執行結果
ClassLoader中的defineClass方法:
//Converts an array of bytes into an instance of class <tt>Class</tt>.
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
另一個自定義類載入器示例
首先,我們定義一個待載入的普通Java
類:Test.java
。放在com.huachao.cl
包下:
package com.huachao.cl;
public class Test {
public void hello() {
System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass()
+ " 載入進來的");
}
}
注意:
如果你是直接在當前專案裡面建立,待
Test.java
編譯後,請把Test.class
檔案拷貝走,再將Test.java
刪除。因為如果Test.class
存放在當前專案中,根據雙親委派模型可知,會通過sun.misc.Launcher$AppClassLoader
類載入器載入。為了讓我們自定義的類載入器載入,我們把Test.class檔案放入到其他目錄。
在本例中,我們Test.class檔案存放的目錄如下:
class檔案目錄
接下來就是自定義我們的類載入器:
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class Main {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
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();
return data;
}
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();
}
}
};
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class clazz = classLoader.loadClass("com.huachao.cl.Test");
Object obj = clazz.newInstance();
Method helloMethod = clazz.getDeclaredMethod("hello", null);
helloMethod.invoke(obj, null);
}
}