要自定義自己的類加載器來加載類,需要先對類加載器和類加載機制有一些基本的了解。
1、類加載器
類加載器ClassLoader的作用有兩個:
①是用於將class文件加載到JVM。
②是用於判斷JVM運行時兩個類是否相等。
2、類加載的時機
類的加載可分為隱式加載和顯示加載。
隱式加載
隱式加載包括以下幾種情況:
- 遇到new(new 一個實例對象的時候)、getstatic(獲取一個類的靜態字段的時候)、putstatic(設置一個類的靜態字段的時候)、invokestatic(調用一個類的靜態方法的時候)這4條字節碼指令時。
- 對類進行反射調用時。
- 初始化一個類時,如果父類還沒有初始化,則先加載其父類並初始化(但是初始化接口時,不要求先初始化父接口)
- 虛擬機啟動時,需要指定一個包含main函數的主類,優先加載並初始化這個主類。
顯式加載
顯示加載包含以下幾種情況:
- 通過Class.forName()加載
- 通過ClassLoader的loaderClass方法加載
- 通過ClassLoader的findClass方法
3、Class.forName()加載類
Class.forName()和ClassLoader都可以對類進行加載。ClassLoader就是遵循雙親委派模型最終調用啟動類加載器的類加載器,實現的功能是“通過一個類的全限定名來獲取描述此類的二進制字節流”,獲取到二進制流后放到JVM中。Class.forName()方法實際上也是調用的CLassLoader來實現的。
先看看Class.forName()的源碼:
/**
* 參數解釋:
* 1、className:要加載的類名
* 2、true:class被加載后是否要被初始化。初始化即執行static的代碼(靜態代碼)
* 3、caller:指定類加載器
*/
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
可以看出來最后正在實現forName()方法的是forName0這一個native方法。而使用forName0需要傳遞的參數之一,就是ClassLoader類加載器。因此可以知道Class.forName()本質還是使用classloader來進行類的加載的。
4、使用ClassLoader加載類
ClassLoader 里面有三個重要的方法 loadClass()、findClass() 和 defineClass()。
loadClass() 方法是加載目標類的入口,它首先會查找當前ClassLoader以及它的父類classloader里面是否已經加載了目標類,如果沒有找到就會讓父類Classloader嘗試加載,如果父類classloader都加載不了,就會調用findClass()讓自定義加載器自己來加載目標類。這實際上就是雙親委派機制的原理。
看一下loadClass()的源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//查看類是否已被加載,findLoadedClass最后調用native方法findLoadedClass0
Class<?> c = findLoadedClass(name);
//還未加載
if (c == null) {
long t0 = System.nanoTime();
try {
//若有父類加載器,則調用其loadClass(),請求父類進行加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//若父類加載器為null,說明父類為BootstrapClassLoader(該類加載器無法被Java程序直接使用,用null代替即可),請求它來加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//加載失敗,拋出ClassNotFoundException異常
}
//父類無法加載,調用findClass方法,嘗試自己加載這個類
//注意:在這個findClass方法中,目前只是拋出一個異常,沒有任何進行類加載的動作
//因此,想要自己進行類加載,就要重寫findClass()方法。
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 的 findClass() 方法是需要子類來覆蓋重寫的,不同的加載器將使用不同的邏輯來獲取目標類的字節碼。得到字節碼之后會調用 defineClass() 方法將字節碼轉換成 Class 對象。
findClass()的源碼如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name); //僅拋出異常
}
可見,ClassLoader的findClass()方法是沒有具體實現的,如果要自定義類加載器,就需要重寫findClass()方法,並且配合defineClass() 方法一起使用,defineClass()方法是用來將byte字節流解析成JVM能夠識別的Class對象。
以上就是CLassLoader進行類加載的簡單流程。
雖然Class.forName()方法本質上還是使用Classloader來進行類的加載的,但它和使用Classloader來進行類加載依然有着區別:
①Class.forName()方法除了將類的字節碼加載到jvm中之外,還會執行類中的static塊,即會導致類的初始化。Class.forName(name, initialize, loader)帶參方法也可以指定是否進行初始化,執行靜態塊。
②ClassLoader只是將類的字節碼加載到jvm中,不會執行static中的內容,即不會進行類加載,只有在newInstance才會去執行static塊。
5、自定義類加載器
我們知道,除了BootstrapClassLoader是由C/C++實現的,其他的類加載器都是ClassLoader的子類。所以如果我們想實現自定義的類加載器,首先要繼承ClassLoader。
根據我們前面的分析,ClassLoader進行類加載的核心實現就在loadClass()方法中。再根據loadClass()方法的源碼,我們可以知道有兩種方式來實現自定義的類加載,分別如下:
①如果不想打破雙親委派機制,那么只需要重寫findClass方法。
②如果想要打破雙親委派機制,那么就需要重寫整個loadClass方法。
如果沒有特殊要求,Java官方推薦重寫findClass方法,而不是重寫整個loadClass方法。這樣既讓我們能夠按照自己的意願加載類,也能保證自定義的類加載器符合雙親委派機制。
明確了如何實現,我們只需要兩步就可以實現自定義的類加載器:第一步是繼承classloader,第二步是重寫findClass方法。
不過由於在findClass()內需要調用defineClass()方法將字節數組轉換成Class類對象,因此要先對輸入的class文件做一些處理,使其變為字節數組。
實現自定義的類加載器:
public class MyClassLoader extends ClassLoader{
//默認ApplicationClassLoader為父類加載器
public MyClassLoader(){
super();
}
//加載類的路徑
private String path = "";
//重寫findClass,調用defineClass,將代表類的字節碼數組轉換為Class對象
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] dataByte = new byte[0];
try {
dataByte = ClassDataByByte(name);
} catch (IOException e) {
e.printStackTrace();
}
return this.defineClass(name, dataByte, 0, dataByte.length);
}
//讀取Class文件作為二進制流放入byte數組, findClass內部需要加載字節碼文件的byte數組
private byte[] ClassDataByByte(String name) throws IOException {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
name = name.replace(".", "/"); // 為了定位class文件的位置,將包名的.替換為/
is = new FileInputStream(new File(path + name + ".class"));
int c = 0;
while (-1 != (c = is.read())) { //讀取class文件,並寫入byte數組輸出流
arrayOutputStream.write(c);
}
data = arrayOutputStream.toByteArray(); //將輸出流中的字節碼轉換為byte數組
is.close();
arrayOutputStream.close();
return data;
}
}
使用自定義的類加載器:
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("com.fengjian.www.MyClassLoader");
clazz.newInstance();
}
由於能力有限,可能存在錯誤,感謝指出。以上內容為本人在學習過程中所做的筆記。參考的書籍、文章或博客如下:
[1]Mr羽墨青衫.深入分析Java ClassLoader原理.知乎.https://zhuanlan.zhihu.com/p/81759029
[2]愚公要移山.學了這么久的java反射機制,你知道class.forName和classloader的區別嗎?.知乎.https://zhuanlan.zhihu.com/p/101114197