1. 程式人生 > 其它 >ClassLoader和類載入機制

ClassLoader和類載入機制

01 背景
最近在做專案的過程中,由於系統需要提供一個對外介面,使系統使用者可以以指令碼的形式提交自己的程式碼,每個使用者可以在系統規範的約束下編寫指令碼,由系統去執行使用者的程式碼,實現了熱部署。
什麼叫熱部署呢?簡單來說就是把程式碼當成U盤或者外設一樣即插即用,每個使用者可以維護自己的解決方案(也就是一段指令碼,一個單獨的類),在更新修改解決方案的過程中而不需要重新編譯啟動整個系統。我們採用的方案就是GroovyClassLoader,我主要講一講自己對ClassLoader的理解和使用。

02

類載入與類載入器

類載入:

類載入的過程就是將Class檔案中描述的各種資訊載入到虛擬機器中,供程式後期執行和使用的。
類載入的生命週期主要分為五個步驟:

1、載入:

通過一個類的全限定名來獲取描述此類的二進位制位元組流
將這個位元組流所代表的靜態儲存結構轉化為方法去的執行時資料結構
在記憶體中生成一個代表這個類的java.lang.Class 物件,作為方法區的各種資料型別的入口

2、驗證

為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害到自身的安全。包括檔案格式驗證,元資料驗證,位元組碼驗證,符號引用驗證。

3、準備

為變數分配記憶體,設定類變數的初始值。

4、解析

將常量池中的符號應用替代為直接引用。

5、初始化

是類載入生命週期的最後一個過程,執行類中定義的java程式程式碼

類載入器:

在前面的類載入過程中,大部分動作都是完全由虛擬機器主導和控制的。而類載入器使得使用者可以在載入的過程中參與進來,結合前面的內容,類載入器就是將“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到java虛擬機器外部來實現。將主動權交給程式猿。
類載入器和這個類本身確定了其在java虛擬機器中的唯一性,每一個類載入器都有一個獨立的類名稱空間,也就意味著,如果比較兩個類是否相等,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個虛擬機器載入,只要載入他們的類載入器不同,那麼這兩個類就註定不相同。

1、Bootstrap Class Loader:負責載入JAVA_HOME/lib目錄下或-Xbootclasspath指定目錄的jar包;

2、Extention Class Loader:載入JAVA_HOME/lib/ext目錄下的或-Djava.ext.dirs指定目錄下的jar包。

3、System Class Loader:載入classpath或者-Djava.class.path指定目錄下的類或jar包。

ClassLoader各司其職,載入在不同路徑下的class檔案,值得注意的是,類載入採用的是雙親委託的設計模式,即傳入一個類限定名,逐層向上到Bootstrap Class Loader中查詢,如果找到即返回,若沒有找到,則在Extention Class Loader中找,若還沒有找到則在System Class Loader下找,即classpath中,如果還沒有找到,則呼叫findClass(name)方法,執行使用者自己的類載入邏輯(可能在其他的地方)

ClassLoader中的幾個重要的方法:

1、loadClass(String name, boolean resolve):載入類的方法,在jdk1.2以前需要重寫該方法實現使用者自己的邏輯,1.2以後為了向下相容,仍然可以重寫該方法,但是建議使用者將自己的載入邏輯實現在findName(name)中。這樣系統先向上尋找能否載入到該類,如果加在不到,將呼叫使用者自定義的findName函式載入物件.

   /**     
 * @param name    類名字      
* @param resolve  是否解析,如果只是想知道該class是否存在可以設定該引數為false     
 * @return 返回一個class泛型     
 * @throws ClassNotFoundException      
*/     
protected Class<?> loadClass(String name, boolean resolve)             
throws ClassNotFoundException {         
/**          
* getClassLoadingLock(name)          
* 為類的載入操作返回一個鎖物件。為了向後相容,這個方法這樣實現:如果當前的classloader物件註冊了並行能力,         
 * 方法返回一個與指定的名字className相關聯的特定物件,否則,直接返回當前的ClassLoader物件。         
 */         
synchronized (getClassLoadingLock(name)) {             
// 首先檢視class是否已經被載入過            
 Class<?> c = findLoadedClass(name);             
if (c == null) {                
long t0 = System.nanoTime();                
 try {                    
 //如果父載入器不為空,則委託給父載入器去載入                     
if (parent != null) {                         
c = parent.loadClass(name, false);                     
} else {                        
 /**                         
 *  如果父載入器為空,說明父載入器已經是Bootstrap ClassLoader了,則直接使用根載入器載入,也就是使用虛擬機器加                          
*  載器載入                          
*/                        
 c = findBootstrapClassOrNull(name);                    
 }                 
} catch (ClassNotFoundException e) {                    
 // ClassNotFoundException thrown if class not found                     
// from the non-null parent class loader                
 }                
 //如果以上的載入器在自己的路徑上面都沒有載入到,則呼叫findClass(name)呼叫使用者自定義的載入器                
 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();                
 }            
 }            
 //根據resolve引數決定是否解析該類             
if (resolve) {                
 resolveClass(c);            
 }            
 return c;        
 }     
}

2、ClassLoader getParent() :可以返回委託的父類載入器。在你自定義載入器找不到相應類的時候,可以呼叫此方法,不過在ClassLoader的預設實現中,ClassLoader先判斷父類載入器是否可以載入,然後再呼叫使用者自定義的findClass方法。

3、 resolveClass():若resolve引數為true的時候,我們需要呼叫該函式,resolve我們的classLoader。

4、ClassLoader getSystemClassLoader():提供了一個直接訪問系統classloader的方法。

03

廢話少說上程式碼!

下面我將以一個例子來闡述如何使用ClassLoader,自定義的ClassLoader將載入被加密的類,而且這個類儲存的路徑不在ClassPath中,也不可以被Bootstrap Class Loader和Extention Class Loader載入,在實際應用中,可以是網路中傳遞過來的加密位元組流,抑或著是實現指令碼的熱部署操作。

package com.siyu;

import java.io.*;
public class ClassLoaderTest extends ClassLoader {     
//自定義載入器載入該路徑下面的檔案     private String directory;      
public ClassLoaderTest(String directory) {         
this.directory = directory;    
 }     
/**     
 * 重寫findClass,使用者可以做以下的事情      
* 1.可以載入boot、ext、system載入器所載入不了的路徑下的檔案      
* 2.可以解密加密後的class檔案      
*/     
@Override     
protected Class<?> findClass(String name) throws ClassNotFoundException {         
//解密金鑰        
byte key = (byte) 1;         
//加密檔案的路徑         
String fileName = directory + name + ".class";         
File file = new File(fileName);         
byte[] decryptedByte = readFromFile(file);         
//解密為原始的class檔案         
for (int i = 0; i < decryptedByte.length; i++) {             
decryptedByte[i] = (byte) (decryptedByte[i] ^ key);         
}         
//defineClass實現了連結階段的驗證等         
return defineClass(null, decryptedByte, 0, decryptedByte.length);     
}       
private byte[] readFromFile(File fileName) {         
try {             
byte[] bytes = null;             
FileInputStream fin = new FileInputStream(fileName);              
int i;             
if ((i = fin.read()) != -1) {                 
//初始化陣列大小和檔案大小一樣                 
bytes = new byte[fin.available()];                 
fin.read(bytes);             
}            
return bytes;         
} catch (FileNotFoundException e) {             
e.printStackTrace();             
return null;        
 } catch (IOException e) {             
e.printStackTrace();            
 return null;        
 }    
 }      
private byte[] encrypt(byte[] bytes) {         
byte key = (byte) 1;         
//依次加密的程式碼        
 for (int i = 0; i < bytes.length; i++) {             
bytes[i] = (byte) (bytes[i] ^ key); 
//利用異或加密         
}         
return bytes;    
 }      
public void encryptFile(String fileName, String directory) {        
 try {            
 String name = fileName.substring(fileName.lastIndexOf("\") + 1, fileName.length() - 6);             
//加密檔案的路徑             
String destFileName = directory + "encryted" + name + ".class";             
//如果加密檔案不存在則建立加密檔案             
File f = new File(destFileName);            
 if (f == null) {                 
f.createNewFile();             
}             
//加密             
byte[] encryptedByte = encrypt(readFromFile(new File(fileName)));             
FileOutputStream fos = new FileOutputStream(destFileName);             
//把加密後的位元組寫入到加密檔案中             
fos.write(encryptedByte);        
 } catch (FileNotFoundException e) {             
e.printStackTrace();        
 } catch (IOException e) {             
e.printStackTrace();        
 }     
}      
 public static void main(String[] args) {         
//設定加密路徑         
ClassLoaderTest classLoaderTest=new ClassLoaderTest("C:\EncryptedClass\");        
 //將test.class加密後儲存到EncryptedClass目錄下         classLoaderTest.encryptFile("C:\Users\jasonchu.zsy\IdeaProjects\BoKeTest\out\production\BoKeTest\com\siyu\test.class"              
,"C:\EncryptedClass\");         
try {             
Class<?> t=classLoaderTest.loadClass("encrytedtest");        
 } catch (ClassNotFoundException e) {             
e.printStackTrace();        
 }    
 }
}

在main函式中先將一個編譯好的class檔案加密後儲存在非classpath路徑下,然後用自定義classLoader進行載入,加密為了簡單起見,使用的是異或加密,利用的原理是二進位制的數經過兩次異或操作後得到的值是相同的。路徑也使用的絕對路徑,大家可以根據需要自行進行修改,有什麼問題可以繼續交流,謝謝。