一、類加載器
1、類加載器概述
在開發中會遇到 java.lang.ClassNotFoundException 和 java.lang.NoClassDefError,想要更好解決這類問題,或者在一些特殊的應用場景,比如需要支持類的動態加載或需要對編譯后的字節碼文件進行加密解密操作,那么需要你自定義類加載器,因此了解類加載器及其加載機制成為了Java開發必備技能之一。
2、類加載器
類加載的作用: 完成類的加載。將class文件字節碼內容加載到內存中, 並將這些靜態數據轉換成方法區的運行時數據結構, 然后在堆中生成一個代表這個類的java.lang.Class對象, 作為方法區中類數據的訪問入口。
類緩存: 標准的JavaSE類加載器可以按要求查找類, 但一旦某個類被加載到類加載器中, 它將維持加載(緩存) 一段時間。 不過JVM垃圾回收機制可以回收這些Class對象。
3、類加載器作用
(1)本質工作: 類加載器的本質工作就是用於加載類;
(2)類緩存:加載到 JVM 中的類會緩存一段時間;
(3)加載文件:類加載器還可以用來加載“類路徑下”的資源文件。
二、類加載器的分類
1、分類
類加載器作用是用來把類(class)裝載進內存的。 JVM 規范定義了如下類型的類的加載器。
2、引導類加載器(Bootstrap Classloader),又稱為根類加載器
它負責加載 Java 的核心庫(JAVA_HOME/jre/lib/rt.jar 等或 sun.boot.class.path 路徑下的內容),是用原生代碼(C/C++)來實現的,並不繼承自 java.lang.ClassLoader,所以通過 Java 代碼獲取引導類加載器對象將會得到 null。(只有核心類庫如 String 才使用 引導類加載器)
3、擴展類加載器(Extension Classloader)
它由 sun.misc.Launcher$ExtClassLoader 實現,是 java.lang.ClassLoader 的子類,負責加載 Java 的擴展庫(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路徑下的內容)
4、應用程序類加載器(Application Classloader)
它由 sun.misc.Lanuncher$AppClassLoader 實現,是 java.lang.ClassLoader 的子類,負責加載 Java 應用程序類路徑(classpath、java.class.path)下的內容。(通俗的講:項目的路徑bin文件夾下的字節碼,以及如果你配置了環境變量classpath)
5、自定義類加載器
開發人員可以通過繼承java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求,例如對字節碼進行加密來避免class文件被反編譯,或者加載特殊目錄下的字節碼數據。
6、
三、經典委托模式
1、Java 中類加載器的雙親委托模式
類加載器負責加載所有的類,系統為所有被載入內存中的類生成一個java.lang.Class實例。一旦一個類被載入JVM中,同一個類就不會被再次載入了。
那么,怎么樣算是“同一個類”呢?
在JVM中,一個類用其全限定類名和其類加載器作為其唯一標識。換句話說,同一個類如果用兩個類加載器分別加載,JVM將視為“不同的類”,它們互不兼容。
那么,我們的類加載器在執行類加載任務的時候,如何確保一個類的全局唯一性呢?
Java虛擬機的設計者們通過一種稱之為“雙親委派模型(Parent Delegation Model)”的委派機制來約定類加載器的加載機制。
按照雙親委派模型的規則,除了引導類加載器之外,程序中的每一個類加載器都應該擁有一個超類加載器,比如:ExtClassLoader的超類加載器是引導類加載器,而AppClassLoader的超類加載器是ExtClassLoader,而自定義類加載器的超類就是AppClassLoader。
那么當一個類加載器接收到一個類加載任務的時候,它並不會立即展開加載,先檢測此類是否加載過,即在方法區尋找該類對應的Class對象是否存在,如果存在就是已經加載過了,直接返回該Class對象,否則會將加載任務委派給它的超類加載器去執行,每一層的類加載器都采用相同的方式,直至委派給最頂層的啟動類加載器為止,如果超類加載器無法加載委派給它的類時,便會將類的加載任務退回給它的下一級類加載器去執行加載,如果所有的類加載器都加載失敗,就會報java.lang.ClassNotFoundException或java.lang.NoClassDefFoundError。
在此大家需要注意,由於Java虛擬機規范並沒有要求類加載器的加載機制一定要使用雙親委托模式,只是建議采用這種方式而已。比如在Tomcat中,類加載器所采用的加載機制就和傳統的雙親委派模型有一定區別,當缺省的類加載器就接收到一個類的加載任務時,首先會由它自行加載,當它加載失敗時,才會將類的加載任務委派給它的超類加載器去執行,這同時也是Servlet規范推薦的一種做法。
說明:
數組類型本身並不是由類加載器負責創建,而是由JVM在運行時根據需要而直接創建的,但數組的元素類型仍然需要依靠類加載器去創建。因此,JVM會把數組元素類型的類加載器記錄為數組類型的類加載器。
2、雙親委托模式目的是什么?
目的:為了安全,而且各司其職,保證核心類庫的安全性
當我們自己聲明一個 java.lang.String 類,類加載器會為我們加載自定義的String類還是系統的String類呢?
當應用程序類加載器接到加載某個類的任務時,例如:java.lang.String。
(1)會現在內存中,搜索這個類是否加載過了,如果是,就返回這個類的Class對象,不去加載。
(2)如果沒有找到,即沒有加載過。會把這個任務先提交給“父加載器”
當擴展類加載器接到加載某個類的任務時,例如:java.lang.String。
(1)會現在內存中,搜索這個類是否加載過了,如果是,就返回這個類的Class對象,不去加載。
(2)如果沒有找到,即沒有加載過。會把這個任務先提交給“父加載器”
當引導類加載器接到加載某個類的任務時,例如:java.lang.String。
(1)會現在內存中,搜索這個類是否加載過了,如果是,就返回這個類的Class對象,不去加載。
(2)如果沒有找到,即沒有加載過。會在它的負責的范圍內嘗試加載。
如果可以找到,那么就返回這個類的Class對象。就結束了。
如果沒有找到,那么會把這個任務往回傳,讓“子加載器”擴展類加載器去加載。
“子加載器”擴展類加載器接到“父加載器”返回的任務后,去它負責的范圍內加載。
如果可以找到,那么就返回這個類的Class對象。就結束了。
如果沒有找到,那么會把這個任務往回傳,讓“子加載器”應用程序類加載器去加載。
“子加載器”應用程序類加載器接到“父加載器”返回的任務后,去它負責的范圍內加載。
如果可以找到,那么就返回這個類的Class對象。就結束了。
如果沒有找到,那么就報錯ClassNotFoundException或java.lang.NoClassDefError
四、獲取 ClassLoader 對象
獲取某個類的類加載對象需要兩步:
① 獲取某個類的 Class 對象;
② 通過 Class 對象調用 getClassLoader() 方法獲取類加載器對象
五、java.lang.ClassLoader 對象
ClassLoader 類是一個抽象類,學習一下 ClassLoader 的相關方法:
public final ClassLoader getParent():返回委托的父類加載器。一些實現可能使用 null 來表示引導類加載器;
public static ClassLoader getSystemClassLoader():返回委托的系統類加載器;
public Class<?> loadClass(String name):使用指定的二進制名稱(類的全限定名)來加載類。例如:java.lang.String,注意內部類的名稱:匿名內部類(外部類的全限定名$編號)、局部內部類(外部類的全限定名$編號+類名)、成員/靜態內部類(外部類的全限定名$+類名);
protected Class<?> findClass(String name):使用指定的二進制名稱(類的全限定名)來查找類。此方法應該被類加載器的實現重寫,該實現按照委托模型來加載類。在通過父類加載器檢查所請求的類后,此方法將被 loadClass 方法調用;
protected final Class<?> findLoadedClass(String name):返回Class 對象,如果類沒有被加載,則返回 null;
protected final Class<?> defineClass(String name,byte[] b,int off,int len):將一個 byte 數組轉換為 Class 類的實例;
六、自定義類加載器
自定義加載器案例
1 import java.io.ByteArrayOutputStream; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.io.FileNotFoundException; 5 import java.io.IOException; 6 import java.io.InputStream; 7
8 public class FileClassLoader extends ClassLoader{ 9 private String rootDir;//指定加載路徑
10
11 public FileClassLoader(String rootDir){ 12 this.rootDir = rootDir; 13 } 14
15 @Override 16 protected Class<?> findClass(String name) throws ClassNotFoundException { 17 //首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經被裝載,直接返回;
18 Class<?> c = findLoadedClass(name); 19
20 if(c ==null){ 21 //委派類加載器請求給父類加載器,如果父類加載器能夠完成,則返回父類加載器加載的Class實例;
22 ClassLoader parent = this.getParent(); 23 try { 24 c = parent.loadClass(name); 25 //加異常處理,父類加載不到,然后自己加載
26 } catch (Exception e) { 27 } 28
29 //調用本類加載器的findClass()方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass()導入類型到方法區;
30 //如果獲取不到對應的字節碼或其他原因失敗,則異常,終止加載過程
31 if(c == null){ 32 byte[] classData = getClassData(name); 33 if(classData == null){ 34 throw new ClassNotFoundException(); 35 }else{ 36 c = defineClass(name, classData, 0, classData.length); 37 } 38 } 39 } 40 return c; 41 } 42
43 //把.class文件的內容讀取到一個字節數組中
44 //為什么要讀取的字節數組中,因為protected final Class<?> defineClass(String name,byte[] b,int off,int len)
45 private byte[] getClassData(String name) { 46 String path = rootDir + File.separator + name.replace(".", File.separator)+".class"; 47 InputStream is = null; 48 ByteArrayOutputStream baos = null; 49 try { 50 is = new FileInputStream(path); 51 baos =new ByteArrayOutputStream(); 52 byte[] buffer = new byte[1024]; 53 int len; 54 while((len = is.read(buffer))!=-1){ 55 baos.write(buffer, 0, len); 56 } 57 return baos.toByteArray(); 58 } catch (FileNotFoundException e) { 59 e.printStackTrace(); 60 } catch (IOException e) { 61 e.printStackTrace(); 62 }finally{ 63 try { 64 if(is!=null){ 65 is.close(); 66 } 67 } catch (IOException e) { 68 e.printStackTrace(); 69 } 70 } 71 return null; 72 } 73 } 74
75 public class TestFileClassLoader { 76
77 public static void main(String[] args) throws ClassNotFoundException { 78 FileClassLoader fsc = new FileClassLoader("D:/java/code"); 79 Class<?> uc = fsc.loadClass("com.ks.UserManager"); 80 System.out.println(uc); 81
82 Class<?> sc = fsc.loadClass("java.lang.String"); 83 System.out.println(sc); 84 System.out.println(sc.getClassLoader());//null,因為委托給父類加載器...一直到引導類加載器
85 } 86
87 }
七、獲取不同的類加載器
1、案例一:獲取類加載器
1 @Test 2 public void test1(){ 3 //對於自定義類,使用系統類加載器進行加載
4 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); 5 System.out.println(classLoader); 6
7 //調用系統類加載器的getParent():獲取擴展類加載器
8 ClassLoader classLoader1 = classLoader.getParent(); 9 System.out.println(classLoader1); 10
11 //調用擴展類加載器的getParent():無法獲取引導類加載器 12 //引導類加載器主要負責加載java的核心類庫,無法加載自定義的類。
13 ClassLoader classLoader2 = classLoader1.getParent(); 14 System.out.println(classLoader2); 15
16 ClassLoader classLoader3 = String.class.getClassLoader(); 17 System.out.println(classLoader3); 18
19 }
運行結果:
引導類加載器是無法獲取的,返回為 null。
2、案例二
1 @Test 2 public void test2() throws ClassNotFoundException { 3 //1.獲取一個系統類加載器
4 ClassLoader classloader = ClassLoader.getSystemClassLoader(); 5 System.out.println(classloader); 6
7 //2.獲取系統類加載器的父類加載器,即擴展類加載器
8 classloader = classloader.getParent(); 9 System.out.println(classloader); 10
11 //3.獲取擴展類加載器的父類加載器,即引導類加載器
12 classloader = classloader.getParent(); 13 System.out.println(classloader); 14
15 //4.測試當前類由哪個類加載器進行加載
16 classloader = Class.forName("com.njf.java.ClassLoaderTest").getClassLoader(); 17 System.out.println(classloader); 18
19 //5.測試JDK提供的Object類由哪個類加載器加載
20 classloader = Class.forName("java.lang.Object").getClassLoader(); 21 System.out.println(classloader); 22 }
運行結果:
七、類加載器加載資源文件
ClassLoader 類的職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然后從這些字節代碼中定義出一個 Java類,即 java.lang.Class 類的一個實例。除此之外,ClassLoader 還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。
1、案例一:加載類路徑(如:src 下)jdbc.properties 資源文件
擴展:SourceFolder:源代碼文件夾,一般會單獨用一個config這種SourceFolder來裝配置文件、等價於src,不同於普通的 Folder
代碼:
在 src 下面創建 jdbc1.properties 文件,內容如下:
username=root
password=123456
url=jdbc:mysql://localhost:3306/test
讀取配置文件值:
1 @Test 2 public void test2() throws Exception { 3
4 Properties pros = new Properties(); 5 //此時的文件默認在當前的module下。 6 //讀取配置文件的方式一: 7 // FileInputStream fis = new FileInputStream("jdbc.properties"); 8 // FileInputStream fis = new FileInputStream("src\\jdbc1.properties"); 9 // pros.load(fis); 10
11 //讀取配置文件的方式二:使用ClassLoader 12 //配置文件默認識別為:當前module的src下
13 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); 14 InputStream is = classLoader.getResourceAsStream("jdbc1.properties"); 15 pros.load(is); 16
17
18 String user = pros.getProperty("user"); 19 String password = pros.getProperty("password"); 20 System.out.println("user = " + user + ",password = " + password); 21
22
23
24 }
2、案例二:讀取指定包下面文件
獲取存放在“com.ks.reflect” 包下面的 demo.properties 文件
代碼示例:
1 @Test 2 public void test() throws IOException{ 3 Properties pro = new Properties();//集合,map,key=value
4
5 Class clazz = Test.class; //獲取本類的 Class 對象
6 ClassLoader loader = clazz.getClassLoader(); //獲取類加載器
7 InputStream in = loader.getResourceAsStream("com/ks/reflect/demo.properties"); 8
9 pro.load(in); 10
11 System.out.println(pro); 12 System.out.println(pro.getProperty("name")); 13 }
3、單例三:讀取其他目錄下的文件
獲取當前項目下的 out.properties 文件,不在 src 下面(這個文件沒有在編譯目錄下,不需要使用類加載器)
代碼實現:
1 @Test 2 public void test() throws IOException{ 3 Properties pro = new Properties();
5 //在項目的根路徑下,不在src里面
6 pro.load(new FileInputStream("out.properties")); 7
8 System.out.println(pro); 9 System.out.println(pro.getProperty("out")); 10 }