---------- android培訓、java培訓、期待與您交流!----------
一、瞭解類加載器
類加載器負責將.class文件(該文件可能在磁盤上,也有可能在網絡上)加載到內存中,併爲之生成對應的java.lang.Class對象。
系統默認提供了三個類加載器:
BootStrap ClassLoader:根類加載器。----->BootStreap
Extension ClassLoader:擴展類加載器。----->ExtClassLoader
System ClassLoader:系統類加載器。------>AppClassLoader
類加載器也是Java類,因爲其他是java類的類加載器本身也要被類加載器加載,顯然必須有第一個類加載器不是不是java類,這正是BootStrap。根類加載器不是java.lang.ClassLoader的子類,而是由java虛擬機自身實現的。
下面舉兩個小例子來初識類加載器:
package itheimareview; public class ClassLoaderTest { public static void main(String[] args) { System.out.println(ClassLoaderTest.class. getClassLoader().getClass().getName());//AppClassLoader //雖然返回null,但並不代表沒有類加載器,而是它的類加載器爲根類加載器,BootStrap System.out.println(System.class.getClassLoader());//null ClassLoader loader = ClassLoaderTest.class.getClassLoader(); while (loader != null) { System.out.println(loader.getClass().getName()); loader = loader.getParent(); } System.out.println(loader); /*sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader null*/ } }
二、類加載器之間的父子關係和管轄範圍圖
Java虛擬機中的所有類裝載器採用具有父子關係的樹形結構進行組織,在實例化每個類裝載器對象時,需要爲其指定一個父級類裝載器對象或者默認採用系統類裝載器爲其父級類加載。
三、類加載機制
JVM的類加載機制主有如下3種:
1、全盤負責。
就是當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯式使用另外一個類加載器來載入。
要注意的是:
首先當前線程的類加載器去加載線程中的第一個類。
如果類A中引用了類B,Java虛擬機將使用加載類A的類裝載器來加載類B。
還可以直接調用ClassLoader.loadClass()方法來指定某個類加載器去加載某個類。
2、父類委託。
就是先讓parent類加載器試圖加載該Class,只有在父類加載器無法加載 該類時才嘗試從自己的類路徑中加載該類。
3、緩存機制。
緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器首先從緩存區中檢索該Class,只有當緩存區中不存在該Class對象時,系統纔會讀取該類對應的二進制數據,也就是字節碼文件,並將其轉換成Class對象,存入緩存區中。這就是爲什麼修改了Class後,必須重新啓動JVM,程序所做的修改纔會生效的原因。
四、類加載器加載Class大致要經過如下8個步驟:
1、檢測此Class是否載入過(即在緩存區中是否有此Class),如果有則直接進入第8步,否則接着執行第2步
2、如果父類加載器不存在(如果沒有父類加載器,則要麼parent一定是根類加載器,要麼本身就是根類加載器),則跳到第4步執行;如果父類加載器存在,則接着執行第3步。
3、請求使用父類加載器去載入目標類,如果成功載入則跳到第8步,否則接着執行第5步。
4、請求使用根類加載器去載入目標類,如果成功載入則跳到第8步,否則跳到第7步。
5、當前類加載器嘗試尋找Class文件(從與此ClassLoader相關的類路徑中尋找),如果找到則執行第6步,如果找不到則跳到第7步。
6、從文件中載入Class,成功載入後跳到第8步。
7、拋出ClassNotFoundException異常。
8、返回對應的java.lang.Class對象。
其中,第5、6步允許重寫ClassLoader的findClass()方法來實現自己的載入策略,甚至重寫
loadClass()方法來實現自己的載入過程。
五、自定義類加載器
1、ClassLoader中包含了大量的protected方法,這些方法都可以被子類重寫。
2、ClassLoader類有如下兩個關鍵方法:
loadClass(String name, boolean resovle):該方法爲ClassLoader的入口點,根據指定的二進制名稱來加載類,系統就是調用ClassLoader的該方法來獲取指定類對應的Class對象。(如果該參數爲 true,則分析這個類)
findClass(String name):根據二進制名稱來查找類。
3、如果需要實現自定義的ClassLoader,則可以通過上面兩種方法來實現。但建議在自定義
加載器時,重寫findClass()方法,而不是重寫loadClass()方法。因爲loadClass()方法有父類委託和緩衝兩種機制,重寫findClass()方法可以避免覆蓋默認類加載器的父類委託、緩衝機制兩種策略如果重寫loadClass()方法,則實現邏輯更爲複雜。
4、loadClass()方法的執行步驟如下:
①用findLoadedClass(String)來檢查是否已經加載類,如果已經加載則直接返回。
②在父類加載器上調用loadClass()方法。如果父類加載器爲null,則使用根類加載器來加載。
③調用findClass(String)方法查找類。
5、defineClass(String name,byte[],int off,int len),該方法負責將指定類的字節文件(即Class文件,如Hello.class)讀入字節數組byte[] b內,並把它轉換爲Class對象,該字節碼文件可以來源於文件、網絡等。
六、自定義類加載器的應用:
自定義類加載器應用還是比較多的,我們通過使用自定義的類加載器,可以實現如下
常見功能:
執行代碼前自動驗證數字簽名
根據用戶提供的密碼解密代碼,從而可以避免通過代碼混淆器來實現反編譯Class文件
根據用戶需求來動態地加載類
根據應用需求把其他數據以字節碼的形式加載到應用中。
七、例子
1、編寫一個對文件內容進行簡單加密的程序。
package itheimareview; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class MyClassLoader extends ClassLoader { public static void main(String[] args) throws Exception { //通過主函數傳參數的形式 String srcPath = args[0];//源文件路徑名 String destDir = args[1];//目的文件夾名,將編譯後的.class文件存放的位置 FileInputStream fis = new FileInputStream(srcPath);//對文件進行流操作 String destFileName = srcPath.substring(srcPath.lastIndexOf("\\")+1);//這個就是源文件路徑中的文件名稱 String destPath = destDir + "\\" + destFileName;//指定存入文件夾中的.class文件的文件名稱 FileOutputStream fos = new FileOutputStream(destPath);//輸出到指定目的文件 cypher(fis, fos);//加密過程 fis.close(); fos.close(); } private static void cypher(InputStream ips, OutputStream ops)throws Exception { int b = -1; while((b = ips.read()) != -1) { //進行異或運算,並數據寫入到輸出流中 ops.write(b ^ 0xff); } } }
再定義一個需要被加密的測試用的類:
package itheimareview; import java.util.Date; public class ClassLoaderAttachment extends Date { public String toString() { return "hello,itcast"; } }
以此可以把需要被加密的文件(ClassLoaderAttachment.java)和把.class文件(ClassLoaderAttachment.class)需要存放的目錄(itcastlib)以主函數的形式傳入即可完成加密作。
爲了可以看到效果,可以用加密後的ClassLoaderAttachment.class文件覆蓋加密之前的
ClassLoaderAttachment.class。在進行這些操作的時候,要注意絕對路徑和相對路徑的應用,否則實驗不一定能成功。
2、再編寫了一個自己的類裝載器,可實現對加密過的類進行裝載和解密。
爲了便於理解,對加密的類進行裝載解密的動作在原MyClassLoader類中進行修改:
package itheimareview; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class MyClassLoader extends ClassLoader { public static void main(String[] args) throws Exception { String srcPath = args[0]; String destDir = args[1]; FileInputStream fis = new FileInputStream(srcPath); String destFileName = srcPath.substring(srcPath.lastIndexOf("\\")+1); String destPath = destDir + "\\" + destFileName; FileOutputStream fos = new FileOutputStream(destPath); cypher(fis, fos); fis.close(); fos.close(); } private static void cypher(InputStream ips, OutputStream ops)throws Exception { int b = -1; while((b = ips.read()) != -1) { ops.write(b ^ 0xff); } } private String classDir; @Override //覆蓋findClass()方法 protected Class<?> findClass(String name) throws ClassNotFoundException { String classFileName = classDir + "\\" + name + ".class"; try { FileInputStream fis = new FileInputStream(classFileName); ByteArrayOutputStream bos = new ByteArrayOutputStream(); //再進行異或運算,就可以解密,當然實際當中不可能這麼簡單,這裏只作測試。 cypher(fis, bos); fis.close(); byte[] bytes = bos.toByteArray(); //return defineClass(bytes, 0, bytes.length);//這個已過時。 return defineClass(null, bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } public MyClassLoader() { } public MyClassLoader(String classDir) { this.classDir = classDir; } }
再定義一個測試類:
package itheimareview; import java.util.Date; public class ClassLoaderTest { public static void main(String[] args) throws Exception { Class clazz = new MyClassLoader("itcastlib").loadClass("ClassLoaderAttachment"); /*這就是爲什麼爲繼承Date類,如果不繼承,原ClassLoaderAttachment類將不能被友好加載 程序運行時會報錯*/ Date d1 = (Date)clazz.newInstance(); System.out.println(d1); } }