類載入機制與反射

Bacer發表於2021-09-09

一. 類的載入,連線,初始化

  1.1. JVM和類

當呼叫Java命令執行某個Java程式時,該命令將會啟動一個Java虛擬機器程式。不管Java程式多麼複雜,啟動多少個執行緒,它們都處於該Java虛擬機器程式裡,都是使用同一個Java程式記憶體區。

JVM程式終止的方式:

  • 程式執行到最後正常結束

  • 程式執行到使用System.exit()或Runtime.getRuntime().exit()程式碼處結束程式

  • 程式執行過程中遇到未捕獲的異常或錯誤而結束

  • 程式所在平臺強制結束了JVM程式

JVM程式結束,該程式所在記憶體中的狀態將會丟失

  1.2 類的載入

當程式主動使用某個類時,如果該類還未被載入到記憶體中,則系統會透過載入、連線、初始化三個步驟來對該類進行初始化。

類的載入時將該類的class檔案讀入記憶體,併為之建立一個java.lang.Class物件,也就是說,當程式使用任何類時,系統都會為之建立一個java.lang.Class物件。

系統中所有的類實際上也是例項,它們都是java.lang.Class的例項

類的載入透過JVM提供的類載入器完成,類載入器時程式執行的基礎,JVM提供的類載入器被稱為系統類載入器。除此之外,開發者可以透過繼承ClassLoader基類來建立自己的類載入器。

透過使用不同的類載入器,可以從不同來源載入類的二進位制資料,通常有如下幾種來源。

  1. 從本地檔案系統載入class檔案,這是前面絕大部分例項程式的類載入方式

  2. 從jar包載入class檔案,這種方式也是很常見的,jdbc程式設計所用的驅動類就放在jar檔案中,JVM可以直接從jar檔案中載入該class檔案。

  3. 透過網路載入class檔案

  4. 把一個Java原始檔動態編譯,並執行載入

類載入器通常無需等到首次使用該類時才載入該類,Java虛擬機器規範允許系統預先載入某些類。

  1.3 類的連線

當類被載入後,系統會為之生成一個對應的Class物件,接著會進入連線階段,連線階段負責把類的二進位制資料合併到JRE中。類的連結可分為如下三個階段。

  1. 驗證:驗證階段用於檢驗被載入的類是否有正確的內部結構,並和其他類協調一致

  2. 準備:類準備階段則負責為類的類變數分配記憶體,並設定預設初始值

  3. 解釋:將類的二進位制資料中的變數進行符號引用替換成直接引用

  1.4 類的初始化

再累舒適化階段,虛擬機器負責對類進行初始化,主要就是對類變數進行初始化。在Java類中對類變數指定初始值有兩種方式:①宣告類變數時指定初始值;②使用靜態初始化塊為類變數指定初始值。

JVM初始化一個類包含如下步驟

  1. 載入並連線該類

  2. 先初始化其直接父類

  3. 依次執行初始化語句

當執行第2步時,系統對直接父類的初始化也遵循1~3,以此類推

  1.5 類初始化時機

當Java程式首次透過下面6種方式使用某個類或介面時,系統會初始化該類或介面

  • 建立類的例項。建立類的例項包括new運算子來建立例項,透過反射來建立例項,透過反射例項化建立例項

  • 呼叫某個類的類方法(靜態方法)

  • 訪問某個類或介面的類變數或為該類變數賦值

  • 使用反射方式來強制來建立某個類或介面的java.lang.Class物件。例如程式碼“Class.forname("Person")”,如果系統還未初始化Person類,則這行程式碼會導致Person類被初始化,並返回person類的java.lang.Class物件

  • 初始化某個類的子類

  • 使用java.exe命令來執行某個主類。當執行某個主類時,程式會初始化該主類

二. 類載入器

  2.1類載入器介紹

  類載入器負責將.class檔案載入到記憶體中,併為之生成對應的java.lang.Class物件。

一個載入JVM的類有一個唯一的標識。在Java中,一個類使用全限定類名(包括包名和類名)作為標識;但在JVM中,一個類使用全限定類名和其類載入器作為唯一標識。

當JVM啟動時,會形成由三個類載入器組成的初始類載入器層次結構

  • Bootstrap ClassLoader:跟類載入器

  • Extension ClassLoader:擴充套件類載入器

  • System ClassLoader:系統類載入器

Bootrap ClassLoader被稱為引導(也稱為原始或跟)類載入器,它負責載入Java的核心類。跟類載入器不是java.lang.ClassLoader的子類,而是JVM自身實現的。

Extension ClassLoader負責載入JRE擴充目錄中的JAR包的類,它的父類載入器是跟類載入器

System ClassLoader,它負責在JVM啟動時載入來自Java命令的-classpath選項、java.class,path系統屬性,或CLASSPATH指定的jar包和類歷經。系統可透過ClassLoader的靜態方法或區該系統類載入器。如果沒有特別指定,則使用者自定義的類載入器都已類載入器作為父載入器

  2.2 類載入機制

JVM類載入機制主要有三種

  • 全盤負責。就是當類載入器負責載入某個Class時,該Class所依賴的和所引用的其他Class也將由該類載入器負責載入,除非顯式使用另外一個類載入器來載入

  • 父類委託。所謂父類委託,就是先讓父類載入器試圖載入該Class。只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類

  • 快取機制。快取機制將會保證所有載入過的Class都會被快取,當程式需要使用時,先從快取中搜尋該Class,當快取中不存在該Class,系統菜才讀取該類對應的二進位制資料,並將其轉為Class物件,存入快取區中。這就是為什麼修改了Class後,必須重新啟動JVM,程式所做的修改才會生效的原因。

類載入器載入Class大致經過8個步驟

  1. 檢測此Class是否載入過(即快取區中是否有此Class),如果有則直接進入第8步,否者接著第2步

  2. 如果父類載入器(父類      gt+ 載入器,要麼Parent一定是跟類載入器,要麼本身就是跟類載入器)不存在,則調到第4步執行

  3. 請求使用父類載入器載入目標類,如果成功載入調到第8步

  4. 請求使用跟類載入器來載入目標類

  5. 當前類載入器嘗試尋找Class檔案(從與此ClassLoader相關的類路徑中尋找),如果找到則執行第6步,如果找不到執行第7步

  6. 從檔案中載入Class,成功載入調到第8步

  7. 丟擲ClassNotFoundException異常

  8. 返回對應的java.lang.Class物件

其中,第5、6步允許重寫ClassLoader的findClass()方法來實現自己的載入策略,甚至重寫loadClass()方法來實現自己的載入過程。

  2.3 建立並使用自定義的類載入器

JVM除跟類載入器之外的所有類載入器都是ClassLoader子類的例項,開發者可以透過擴充ClassLoader的子類,並重寫該ClassLoader所包含的方法實現自定義的類載入器。ClassLoader有如下兩個關鍵方法。

  • loadClass(String name,boolean resolve):該方法為ClassLoader的入口點,根據指定名稱來載入類,系統就是呼叫ClassLoader的該方法來獲取指定類的class物件

  • findClass(String name):根據指定名稱來查詢類

如果需要是實現自定義的ClassLoader,則可以透過重寫以上兩個方法來實現,通常推薦重寫findClass()方法而不是loadClass()方法。

classLoader()方法的執行步驟:

  1. findLoadedClass():來檢查是否載入類,如果載入直接返回。

  2. 父類載入器上呼叫loadClass()方法。如果父類載入器為null,則使用跟類載入器載入。

  3. 呼叫findClass(String)方法查詢類

從上面看出,重寫findClass()方法可以避免覆蓋預設類載入器的父類委託,緩衝機制兩種策略;如果重寫loadClass()方法,則實現邏輯更為複雜。

ClassLoader的一些方法:

  • Class defineClass(String name,byte[] b,int off,int len):負責將位元組碼分析成執行時資料結構,並檢驗有效性

  • findSystemClass(String name):從本地檔案系統裝入檔案。

  • static getSystemClassLoader():返回系統類載入器

  • getParent():獲取該類載入器的父類載入器

  • resolveClass(Class> c):連結指定的類

  • findClassLoader(String name):如果載入器載入了名為name的類,則返回該類對用的Class例項,否則返回null。該方法是類載入快取機制的體現。

下面程式開發了一個自定義的ClassLoader。該classLoader透過重寫findClass()方法來實現自定義的類載入機制。這個ClassLoader可以在載入類之前先編譯該類的原始檔,從而實現執行Java之前先編譯該程式的目標,這樣即可透過該classLoader執行Java原始檔。

 

圖片描述

package com.gdut.basic;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.lang.reflect.Method;public class CompileClassLoader extends ClassLoader {private byte[] getBytes(String fileName) {
    File file = new File(fileName);
    Long len = file.length();    byte[] raw = new byte[(int)len];
    
        FileInputStream fin = new FileInputStream(file);        //一次讀取class檔案的二進位制資料
        int r = fin.read(raw);        if(r != len) {            throw new IOException("無法讀取檔案"+r+"!="+raw);        
    
    return null;
        }
}    private boolean compile(String javaFile) throws IOException {
        System.out.println("正在編譯"+javaFile+"...");
        Process p = Runtime.getRuntime().exec("javac"+javaFile);        try {            //其他執行緒都等待這執行緒完成            p.waitFor();
        }catch(InterruptedException ie) {
            System.out.println(ie);
        }        int ret = p.exitValue();        return ret == 0;
    }
    @Override    protected Class> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        String findStub = name.replace(".", "/");
        String javaFileName = findStub+".java";
        String classFileName = findStub+".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 {            if(!compile(javaFileName)|| !classFile.exists()) {                throw new ClassNotFoundException("ClassNotFoundExcetion"+javaFileName);
            }
            }catch(IOException ie) {
                ie.printStackTrace();
            }
        }        if(classFile.exists()) {            
                byte[] raw = getBytes(classFileName);
                
                clazz = defineClass(name,raw,0,raw.length);
        }        //如果clazz為null,表明載入失敗,則丟擲異常
        if(clazz == null) {            throw new ClassNotFoundException(name);
        }        return clazz;
    }    
    public static void main(String[] args) throws Exception {        //如果執行該程式時沒有引數,即沒有目標類
        if (args.length clazz = ccl.loadClass(progClass);        //獲取執行時的類的主方法
        Method main = clazz.getMethod("main", (new String[0]).getClass());
        Object argsArray[] = {progArgs};
        main.invoke(null, argsArray);
        
    }
}

圖片描述

接下來可以提供任意一個簡單的主類,該主類無需編譯就可以使用上面的CompileClassLoader來執行他

圖片描述

package com.gdut.basic;public class Hello {    public static void main(String[] args) {        for(String arg:args) {
            System.out.println("執行Hello的引數:"+arg);
        }

    }

}

圖片描述

無需編譯該Hello.java,可以直接執行下面命令來執行該Hello.java程式

java CompileClassLoader hello 瘋狂Java講義

執行結果如下:

CompileClassLoader:正常編譯 Hello.java...
執行hello的引數:瘋狂Java講義

 

使用自定義的類載入器,可以實現如下功能

  1. 執行程式碼前自動驗證數字簽名

  2. 根據使用者提供的密碼解密程式碼,從而可以實現程式碼混淆器來避免反編譯*.class檔案

  3. 根據應用需求把其他資料以位元組碼的形式載入到應用中。

    2.4 URLClassLoader類

該類時系統類載入器和擴充類載入器的父類(此處的父類,是指類與類之間的的繼承關係)。URLClassLoader功能比較強大,它可以從本地檔案系統獲取二進位制檔案來載入類,也可以從遠端主機獲取二進位制檔案載入類。

該類提供兩個構造器

  • URLClassLoader(URL[] urls):使用預設的父類載入器建立一個ClassLoader物件,該物件將從urls所指定的路徑來查詢並載入類

  • URLClassLoader(URL[] urls,ClassLoader prarent):使用指定的父類載入器建立一個ClassLoader物件,該物件將從urls所指定的路徑來查詢並載入類。

下面程式示範瞭如何從檔案系統中載入MySQL驅動,並使用該驅動獲取資料庫連線。透過這種方式來獲取資料庫連線,無需將MySQL驅動新增到CLASSPATH中。

圖片描述

package java.gdut;import java.net.URL;import java.net.URLClassLoader;import java.sql.Connection;import java.sql.Driver;import java.util.Properties;public class URLClassLoaderTest {    private static Connection conn;    public static Connection getConn(String url,String user,String pass)throws Exception{        if(conn == null){
            URL[] urls = {new URL("file:mysql-connection-java-5.1.46-bin.jar")};
            URLClassLoader myClassLoader = new URLClassLoader(urls);            //載入MySQL,並建立例項
            Driver driver = (Driver)myClassLoader.loadClass("com.mysql.jdbc.Driveer").newInstance();

            Properties properties = new Properties();
            properties.setProperty("user",user);
            properties.setProperty("pass",pass);            //呼叫driver的connect方法來取得資料庫連線
            conn = driver.connect(url,properties);
        }        return conn;
    }    public static void main(String[] args) throws Exception {
        System.out.println(getConn("jdbc:mysql://localhost:3306/tb_test","sherman","a123"));
    }
}

圖片描述

本程式類載入器的載入路徑是當前路徑下的mysql-connection-java-5.1.46-bin.jar檔案,將MySQL驅動複製到該路徑下,這樣保證ClassLoader可以正常載入到驅動類

三. 透過反射檢視類資訊

Java程式中的許多物件在執行時都會出現收到外部傳入的一個物件,該物件編譯時型別是Object,但程式又需要呼叫該物件執行時的方法。

  • 第一種做法是假設編譯時和執行時都知道該物件的的型別的具體資訊,這種情況下,可以先用instanceof()運算子進行判斷,再利用強制型別轉換將其轉換成執行時型別的變數即可

  • 第二種做法是編譯時根本無法知道該物件和類可能屬於那些類,程式只依靠執行時資訊來發現該物件和類的真實資訊,這就必須使用反射

  3.1 獲得class物件

每個類被載入後,系統會為該類生成一個對應的Class物件,透過該Class物件可以訪問到JVM中的這個類。獲得Class物件通常三種方式

  1. 使用Class類的forName(String clazz)靜態方法。字串引數傳入全限定類名(必須新增包名),可能會丟擲ClassNotFoundexception異常。

  2. 呼叫某個類的class屬性來獲取該類的的Class物件。

  3. 呼叫某個物件的getClass()方法,該方法是Object類的一個方法。

對於第一種方式,第二種的優勢:

  • 程式碼更安全。程式在編譯階段就可以檢查需要訪問的Class物件是否存在。

  • 程式效能更好。這的種方式無需呼叫方法,所以效能更好。

  3.2 從Class中獲取資訊

Class類提供了大量的例項方法獲取該Class物件所對應類的詳細資訊

下面4個方法用於獲取Class物件對應類的構造器

  • ConStructor getConStructor(Class> parameterTypes):返回Class物件對應類的,帶指定引數列表的public構造器

  • ConStructor>[] getConStructor():返回此Class物件對應類的所有public構造器

  • ConStructor getDeclaredConStructor(Class>... parameterTypes):返回此Class物件對應類的、帶指定引數列表的構造器,與構造器的訪問許可權無關

  • ConStructor>[] getDeclaredConStructor():返回此Class物件對應類的所有構造器,與構造器的訪問許可權無關

下面四個方法獲取Class物件對應類所包含方法。

  • Method getMethod(String name,Class> parameterTypes):返回Class物件對應類的,帶指定形參列表的public方法

  • Method[] getMethods():返回Class物件對應類的所有public方法

  • Method getDeclaredMethod(String name,Class> parameterTypes):返回Class物件對應類的,帶指定形參列表的方法,與訪問許可權無關

  • Method[] getDeclaredMethods():返回Class物件對應類的所有全部方法,與方法的訪問許可權無關

下面四個方法獲取Class物件對應類所包含的成員變數。

  • Field getField(String name):返回Class物件對應類的,指定名稱的public成員變數

  • Field[] getFIelds():返回Class物件對應類的所有public成員變數

  • Field getDeclaredField(String name):返回Class物件對應類的,指定名稱的成員變數,與成員的訪問許可權無關

  • Field[] getFIelds():返回Class物件對應類的所有成員變數,與成員的訪問許可權無關

如下幾個方法用於訪問Class對應類的上所包含的Annotation.


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2768/viewspace-2801223/,如需轉載,請註明出處,否則將追究法律責任。

相關文章