
?類加載器負責把.class文件加載到內存中并生成對應的Class類對象,本小節將講解類加載器的種類、工作原理以及如何自定義類加載器。
類加載器負責加載所有的類,系統為所有被載入內存中的類生成一個Class類的對象實例。一旦一個類被載入內存中,同個類就不會被再次載入了。那么,如何樣才算“同一個類”呢?正如一個對象有一個唯一的標識一樣,一個載入內存中的類也有一個唯一的標識。 在Java語言中,一個類用包名以及自身的類名作為唯一標識,但在類加載機制中,一個類用包名、類名和其類加載器作為唯一標識。例如,如果在pg的包中有一個名為Person的類,它被類加載器ClassLoader對象k1加載,則該Person類對應的Class對象在表示為(Person、pg、k1)。 如果用ClassLoader對象k2加載這個類,則這個類的Class類對象被表示為(Person、pg、k2),在虛擬機看來,(Person、pg、k1) 和(Person、pg、k2) 是不同的,它們是互不兼容的。?
(資料圖片)
Java虛擬機剛啟動時,會有三個類加載器組成一個類加載器組,這個組中的成員包括:?
Bootstrap ClassLoader:根類加載器?Extension ClassLoader:擴展類加載器?Application ClassLoader:應用程序類加載器?下面分別介紹這些類加載器的作用。Bootstrap ClassLoader被稱為根類加載器、引導類加載器或原始類加載器,它負責加載Java的核心類。Extension ClassLoader被稱為擴展類加載器,它負責加載JAVA_HOME\lib\ext目錄中的、或者通過java.ext.dirs系統變量指定路徑中的類。Application ClassLoade被稱為應用程序類加載器,它負責加載程序員所編寫的那些類。程序員可以通過?
除了Java虛擬機所提供的三個類加載器之外,用戶也可以自定義類加載器。用戶自定義的類加載器位于以上所有類加載器的下級,因此由虛擬機提供的類加載器以及用戶自定義的類加載器可以形成一個自上而下的體系,如圖19-2所示。?
圖19-2 4種類加載器的層次結構?
在這個體系中,一個類加載器的上級類加載器也被稱為“父類加載器”。類加載的工作方式分為以下三種:?
全盤負責:所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯式使用另外一個類加載器來載入。?父類委托:所謂父類委托,也稱雙親委派,是指先讓上一級類加載器試圖加載該Class,只有在上級類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。?緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區中搜尋該Class,只有當緩存區中不存在該Class對象時,類加載器才會讀取該類對應的二進制數據并將其轉換成Class對象存入緩存區中。?類加載器加載Class大致要經過如下8個步驟:?
檢測此Class是否載入過(即在緩存區中是否有此Class),如果有則直接進入第8步,否則接著執行第2步?如果父類加載器不存在(如果沒有父類加載器,則要么parent一定是根類加載器,要么本身就是根類加載器),則跳到第4步執行;如果父類加載器存在,則接著執行第3步?請求使用父類加載器去載入目標類,如果成功載入則跳到第8步,否則接著執行第5步?請求使用根類加載器來載入目標類,如果成功載入則跳到第8步,否則跳到第7步?當前類加載器嘗試尋找Class文件(從與此ClassLoader相關的類路徑中尋找),如果找到則執行第6步,如果找不到則跳到第7步。?從文件中載入Class,成功載入后跳到第8步。?拋出ClassNotFoundException異常。?返回對應的java.lang.Class對象。?其中,第5、6步允許重寫ClassLoader的findClass()方法來實現自己的載入策略,甚至重寫loadClass()方法來實現自己的載入過程。?
實際開發過程,程序員可以通過ClassLoader類的getSystemClassLoader()靜態方法獲得應用程序類加載器的引用。下面的【例19_03】展示了如何獲得應用程序類加載器的引用并通過這個引用獲得其上級類加載器。?
【例19_03 應用程序類加載器】
Exam19_03.java?
import java.net.URL;import java.util.Enumeration;public class Exam19_03 { public static void main(String[] args) { try{ //獲得應用程序類加載器 ClassLoader appLoader = ClassLoader.getSystemClassLoader(); System.out.println("應用程序類加載器:"+appLoader); Enumerationem1 = appLoader.getResources("") ; System.out.print("應用程序類加載器加載路徑:"); while(em1.hasMoreElements()){ System.out.println (em1.nextElement()) ; } //獲得應用程序類加載器的上級類加載器(即擴展類加載器) ClassLoader extLoader = appLoader.getParent(); System.out.println("擴展類加載器:"+extLoader); System.out.print("擴展類加載器的加載路徑:"); System.out.println(System.getProperty("java.ext.dirs")); System.out.println("擴展類加載器的上級加載器:"+extLoader.getParent()); }catch (Exception e){ e.printStackTrace(); } }}
【例19_03】的運行結果如圖19-3所示。?
圖19-3【例19_03】運行結果?
從圖19-3可以看出:程序中無法獲得擴展類加載器的加載路徑,這是從JDK1.8之后Java語言做出的改變,實際上JDK1.8或更早版本的JDK允許在程序中獲得擴展類加載器的加載路徑。此外還可以看出:擴展類加載器的上級類加載器,也就是根類加載器也無法獲得,是因為根類加載器并沒有繼承ClassLoader抽象類,并且根類加載器并不是用Java語言,而是用C++語言實現的,所以擴展類加載器的getParent()方法返回null。?
在Java語言中,除根類加載器以外,其他所有的類加載器都是ClassLoader類的子類,因此程序員可以通過繼承ClassLoader類并重寫其部分方法自定義類加載器。ClassLoader類定義了兩個關鍵的方法,它們分別是:?
loadClas(String name, boolean resolve):該方法為ClassLoader的入口點,它根據指定名稱來加載類,系統就是調用ClassLoader的該方法來獲取指定類對應的Class對象。?findClass(String name):根據指定名稱來查找類。?如果需要實現自定義的類加載器,就可以通過重寫以上兩個方法來實現。通常推薦重寫findClass()方法而不是重寫loadClass()方法。loadClass()方法的執行步驟如下:?
用findLoadedClass(String name) 來檢查是否已經加載類,如果已經加載則直接返回?在父類加載器上調用loadClass()方法,如果父類加載器為null, 則使用根類加載器來加載?調用findClass(String name)方法查找類?從上面步驟中可以看出,重寫findClass()方法可以避免覆蓋默認類加載器的父類委托、緩沖機制兩種策略,而如果重寫loadClass()方法,則實現邏輯更為復雜。在ClassLoader類中還有一個核心方法是defineClass(),該方法負責將指定類的字節碼文件(即Class文件,如Hello.class) 讀入字節數組中并把它轉換為Class對象,該字節碼文件可以來源于文件、網絡等。defineClass()方法管理JVM的許多復雜的實現,它負責將字節碼分析成運行時數據結構,并校驗有效性等。但程序員無須重寫該方法,因為該方法是被final關鍵字修飾的最終方法。?
除此之外,ClassLoader中還包含一些普通方法,如表19-1所示。?
表19-1 ClassLoader類的普通方法?
方法? | 功能? |
Class> findSystemClass(String name)? | 從本地文件系統裝入文件。它在本地文件系統中尋找類文件,如果存在,就使用defineClass()方法將原始字節轉換成Class對象? |
static ClassLoader getSystemClassLoader()? | 用于返回應用程序類加載器? |
ClassLoader getParent()? | 獲取該類加載器的父類加載器? |
void resolveClass(Class> c)? | 鏈接指定的類,類加載器可以使用此方法來鏈接類c? |
Class> findLoadedClass(String name)? | 如果此Java虛擬機已加載了名為name的類,則直接返回該類對應的Class實例,否則返回null。 該方法是Java類加載緩存機制的體現? |
下面的【例19_04】展示了一個自定義的類加載器MyClassLoader的實現過程。首先創建了一個MyClassLoader類的對象mcl,mcl調用loadClass()方法對Hello.class文件進行加載,加載過程中會調用findClass()方法找到Hello.java這個類,然后對其進行編譯并進行加載。加載完畢后,調用了Hello類的main()方法打印其參數。?
【例19_04自定義類加載器】
Hello.java?
public class Hello{ public static void main (String[] args) { for (String arg : args) System.out .println(arg) ;//打印參數 }}
Exam19_04.java?
import java.io.*;import java.lang.reflect.Method;class MyClassLoader extends ClassLoader { //定義讀取文件內容的方法 private byte[] getBytes(String fileName) throws IOException { File file = new File(fileName); long len = file.length(); byte[] raw = new byte[(int) len]; FileInputStream fis = new FileInputStream(file); // 一次讀取Class文件的全部二進制數據 int r = fis.read(raw); if (r != len) { throw new IOException("無法讀取全部文件:" + r + "!=" + len); } return raw; } //定義編譯指定Java文件的方法 private boolean compile(String fileName) throws IOException { System.out.println("MyClassLoader正在編譯" + fileName); //調用系統的javac命令 Process p = Runtime.getRuntime().exec("javac " + fileName); try { //其他線程都等待這個線程完成 p.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } int ret = p.exitValue(); //返回編譯是否成功 return ret == 0; } //重寫ClassLoader的findClass()方法 protected Class> findClass(String name) throws ClassNotFoundException { Class clazz = null; //將路徑中的點(.)替換成斜杠(/) String fileStub = name.replace(".", "/"); String javaFilename = "./src/"+fileStub + ".java"; String classFilename = "./src/"+fileStub + ".class";//① File javaFile = new File(javaFilename); File classFile = new File(classFilename); //當指定Java源文件存在,且Class文件不存在,或者Java源文件 //的修改時間比Class文件的修改時間更晚時,重新編譯 if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) { try { //如果編譯失敗,或者該Class文件不存在 if (!compile(javaFilename) || !classFile.exists()) { throw new ClassNotFoundException("ClassNotFoundExcetpion:" + javaFilename); } } catch (IOException e) { e.printStackTrace(); } } //如果Class文件存在,系統負責將該文件轉換成Class對象 if(classFile.exists()){ try{ //將class文件的二進制數據讀入數組 byte[] raw = getBytes(classFilename); //調用ClassLoader的def ineClass()方法將二進制數據轉換成Class對象 clazz = defineClass(name,raw,0,raw.length); }catch (IOException e){ e.printStackTrace(); } } //如果clazz為null,表明加載失敗,則拋出異常 if (clazz ==null){ throw new ClassNotFoundException (name) ; } return clazz ; }}public class Exam19_04 { public static void main(String[] args) throws Exception{ MyClassLoader mcl = new MyClassLoader(); String progClass = "Hello"; String[] progArgs = {"我喜歡Java","我正在努力學習這門語言"}; //加載需要運行的類 Class> clazz = mcl.loadClass (progClass); // 獲取需要運行的類的主方法 Method main = clazz. getMethod ("main", (new String[0]) .getClass()) ; Object argsArray[] = {progArgs}; main. invoke (null, argsArray) ; }}
【例19_04】中,由自定義的類加載器對Java源文件進行編譯并加載,由于在默認情況下是父類委托方式加載類,所以都是由應用程序類加載器來加載程序員編寫的類。為了不讓應用程序類加載器加載Hello類,就要在工程文件夾中按照“out”->“production”->“lesson19”的順序找到Hello.class文件并把它刪掉。需要注意:mcl對象在對Java源文件進行編譯時把字節碼文件直接生成到了與Java源文件的相同的路徑下,因此在程序中的語句①中定義的字節碼文件的路徑與源文件的路徑是相同的,都是src。刪掉Hello.class文件后運行【例19_04】的結果如圖19-4所示。?
圖19-4【例19_04】運行結果?
運行完【例19_04】后可以看到存放源文件的src文件夾下出現了一個Hello.class,這是因為自定義類加載器mcl對Hello.java進行編譯的結果。在Hello.class存在的情況下再次運行【例19_04】,可以看到控制臺上不會輸出“MyClassLoader正在編譯./src/Hello.java”這句話,這是因為字節碼文件已經存在的情況下不會被再次編譯。需要說明:【例19_04】中使用了反射技術調用Hello類的main()方法,關于反射技術的細節將在19.3小節講解。?
本書的第15章中曾介紹過URL類,每一個URL類對象就代表一個資源,這個資源可以在網絡上,也可以在本地硬盤上。實際上,一個字節碼文件或一個jar包也是一個資源,因此一個字節碼文件或一個jar包都可以用URL類對象來表示。URLClassLoader類能以URL數組作為構造方法的參數,并且能夠把URL數組中的代表字節碼文件或jar包的資源加載到內存中。URLClassLoader用于加載類的方法是loadClass(),這個方法在加載一個類之后會生成代表該類的Class類對象。獲得了Class類對象后,利用反射技術就能創建出這個類的對象。例如獲得了代表A類的Class對象后,利用反射技術就能獲得一個A類的對象。下面的【例19_05】利用URLClassLoader加載了用于驅動數據庫的Driver接口,緊接著生成了Driver接口的實現類對象,并以此對象創建一個Connection對象。?
【例19_05 URLClassLoader類的使用】
Exam19_05.java?
import java.net.*;import java.sql.*;import java.util.Properties;public class Exam19_05 { private static Connection con; //定義一個獲得數據庫連接的方法 public static Connection getConnection(String url,String user,String password) throws Exception{ if(con==null){ //以jar包作為資源 URL[] urls = {new URL("file:D://mysql-connector-java-8.0.27.jar")}; //創建URLClassLoader類對象 URLClassLoader ucl = new URLClassLoader(urls); //加載jar包中的Driver類并用反射技術創建Driver類對象 Driver driver = (Driver) ucl.loadClass("com.mysql.cj.jdbc.Driver"). getConstructor().newInstance(); Properties props = new Properties(); props.setProperty("user",user) ; props.setProperty ("password",password) ; //調用Driver對象的connect()方法來取得數據庫連接 con = driver.connect (url,props) ; } return con; } public static void main(String[] args) throws Exception{ Connection con = getConnection("jdbc:mysql://127.0.0.1:3306/mydb","root","123456"); System.out.println(con); }}
【例19_05】中URLClassLoader類加載了D盤下mysql-connector-java-8.0.27.jar文件中的Driver接口,因此只有把mysql-connector-java-8.0.27.jar文件提前拷貝到D盤根目錄下才能成功的運行程序。此外,讀者要把程序中的用戶名和密碼修改成自己真實的用戶名和密碼?!纠?9_05】的運行結果如圖19-5所示。?
圖19-5【例19_05】運行結果?
從圖19-5可以看出:Connection接口的實現類對象已經被創建,由此可見,只要掌握了類加載技術,即使不把jar包加入IDE的CLASSPATH中也能加載jar包中的類。正如【例19_05】所示,創建URLClassLoader類對象時傳入了一個URL數組參數,該ClassLoader就可以從這系列URL指定的資源中加載指定類,這里的URL可以以file:為前綴,表明從本地文件系統加載,也可以以http:為前綴,表明從互聯網通過HTTP訪問來加載,還可以以ftp:為前綴,表明從互聯網通過FTP訪問來加載,總之它的功能非常強大。?
本文字版教程還配有更詳細的視頻講解,小伙伴們可以??點擊這里??觀看。