java類載入機制 機製、反射、動態代理詳解

2020-08-12 18:51:09

類的載入、連線和初始化(系統可能在第一次使用某個類時載入該類, 也可能採用預載入機制 機製來載入某個類)動態代理實現

1、JVM和類

當呼叫 java 命令執行某個 Java 程式時, 該命令將會啓動一個 Java 虛擬機器進程, 不管該 Java 程式有多麼複雜, 該程式啓動了多少個執行緒, 它們都處於該 Java 虛擬機器進程裡。 同一個 JVM的所有執行緒、 所有變數都處於同一個進程裡, 它們都使用該 JVM 進程的記憶體區。

JVM執行時數據區

在这里插入图片描述

當系統出現以下幾種情況時, JVM 進程將被終止:

  • 程式執行到最後正常結束。
  • 程式執行到使用 System.exit()或 Runtime.getRuntime().exit()程式碼處結束程式。
  • 程式執行過程中遇到未捕獲的異常或錯誤而結束。
  • 程式所在平臺強制結束了 JVM 進程。

小結:當 Java 程式執行結束時, JVM 進程結束, 該進程在記憶體中的狀態將會丟失。

範例:

1.1、定義一個包含類變數的類:A

public class A
{
    // 定義該類的類變數
    public static int a = 6;
}

1.2、定義一個類建立 A 類的範例, 並存取 A 物件的類變數 a:

public class ATest1
{
    public static void main(String[] args)
    {
        // 建立A類的範例
        A a = new A();
        // 讓a範例的類變數a的值自加
        a.a ++;
        System.out.println(a.a);
    }
}

1.3、建立 A 物件, 並存取其類變數 a 的值:

public class ATest2
{
    public static void main(String[] args)
    {
        // 建立A類的範例
        A b = new A();
        // 輸出b範例的類變數a的值
        System.out.println(b.a);
    }
}

分析:在 ATestl.java 程式中建立了 A 類的範例, 並讓該範例的類變數 a 的值自加, 程式輸出該範例的類變數 a 的值將看到 7。 執行第二個程式 ATest2 時, 程式再次建立了 A 物件, 並輸出 A 物件類變數的 a 的值, 此時 a 的值是多少呢? 結果依然是 6, 並不是 7。 這是因爲執行 ATestl 和 ATest2 是兩次執行 JVM 進程, 第一次執行 JVM 結束後, 它對 A 類所做的修改將全部丟失——第二次執行 JVM 時將再次初始化 A 類。

2、類的載入

​ Java類的生命週期
在这里插入图片描述
2.1、當程式主動使用某個類時, 如果該類還未被載入到記憶體中, 則系統會通過載入、 連線、 初始化三個步驟來對該類進行初始化。 如果沒有意外, JVM 將會連續完 成這三個步驟, 所以有時也把這三個步驟統稱爲類載入或類初始化。
類載入指的是將類的 class 檔案讀入記憶體, 併爲之建立一個 java.lang.Class 物件, 也就是說, 當程式中使用任何類時, 系統都會爲之建立一個java.lang.Class 物件。

2.2、類的載入由類載入器完成, 類載入器通常由 JVM 提供, 這些類載入器也是前面所有程式執行的基礎, JVM 提供的這些類載入器通常被稱爲系統類載入器。 除此之外, 開發者可以通過繼承 ClassLoader基礎類別來建立自己的類載入器。

2.3、通過使用不同的類載入器, 可以從不同來源載入類的二進制數據, 通常有如下幾種來源。

  • 從本地檔案系統載入 class 檔案, 這是前面絕大部分範例程式的類載入方式。
  • 從 JAR 包載入 class 檔案, 這種方式也是很常見的, 前面介紹 JDBC 程式設計時用到的數據庫驅動類就放在 JAR 檔案中, JVM 可以從 JAR 檔案中直接載入該 class 檔案。
  • 通過網路載入 class 檔案。
  • 把 一 個 Java 原始檔動態編譯, 並執行載入。

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

3、類的連線

3.1、當類被載入之後, 系統爲之生成一個對應的 Class 物件, 接着將會進入連線階段, 連線階段負責把類的二進制數據合併到 JRE 中。 類連線又可分爲如下三個階段:
(1) 驗證: 驗證階段用於檢驗被載入的類是否有正確的內部結構, 並和其他類協調一致。
(2) 準備: 類準備階段則負責爲類的類變數分配記憶體, 並設定預設初始值。
(3 ) 解析: 將類的二進制數據中的符號參照替換成直接參照。

4、類的初始化

在類的初始化階段, 虛擬機器負責對類進行初始化, 主要就是對類變數進行初始化。

4.1、在 Java 類中對類變數指定初始值有兩種方式:① 宣告類變數時指定初始值;② 使用靜態初始化塊爲類變數指定初始值。

例:

public class Test{
    // 宣告變數 a 時指定初始值
    static int a = 5
    static int b;
    static int c;
    static{
        // 使用靜態初始化塊爲變數 b 指定初始值
        b=6;
    }
}

分析:對於上面程式碼, 程式爲類變數 a、 b 都顯式指定了初始值, 所以這兩個類變數的值分別爲 5、 6, 但類變數 c 則沒有指定初始值, 它將採用預設初始值 0。

4.2、宣告變數時指定初始值, 靜態初始化塊都將被當成類的初始化語句, JVM 會按這些語句在程式中的排列順序依次執行它們

public class Test
{
    static
    {
        // 使用靜態初始化塊爲變數b指定出初始值
        b = 6;
        System.out.println("----------");
    }
    // 宣告變數a時指定初始值
    static int a = 5;
    static int b = 9;         // ①
    static int c;
    public static void main(String[] args)
    {
        System.out.println(Test.b);
    }
}

分析:上面程式碼先在靜態初始化塊中爲 b 變數賦值, 此時類變數 b 的值爲 6; 接着程式向下執行, 執行到①號程式碼處, 這行程式碼也屬於該類的初始化語句, 所以程式再次爲類變數 b 賦值。 也就是說, 當 Test類初始化結束後, 該類的類變數 b 的值爲 9。

4.3、JVM 初始化一個類包含如下幾個步驟:

  • 假如這個類還沒有被載入和連線, 則程式先載入並連線該類。
  • 假如該類的直接父類別還沒有被初始化,則先初始化其直接父類別
  • 假如類中有初始語句 , 則系統依次執行這些初始化語句

4.4、類初始時機

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

  • 建立類的範例。 爲某個類建立範例的方式包括: 使用 new 操作符來建立範例, 通過反射來建立範例, 通過反序列化的方式來建立範例。

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

  • 存取某個類或介面的類變數, 或爲該類變數賦值。

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

  • 初始化某個類的子類。 當初始化某個類的子類時, 該子類的所有父類別都會被初始化。

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

5、類載入器

類載入器負責將.class 檔案( 可能在磁碟上, 也可能在網路上) 載入到記憶體中, 併爲之生成對應的java.lang.Class 物件。

6、類載入機制 機製

類載入器負責載入所有的類, 系統爲所有被載入記憶體中的類生成java.lang.Class 範例。

一個類被載入 JVM 中, 同 一個類就不會被再次載入了——正如一個物件有一個唯一的標識一樣, 一個載入 JVM 中的類也有一個唯一的標識。 在 Java 中, 一
個類用其全限定類名( 包括包名和類名) 作爲標識; 但在 JVM 中, 一個類用其全限定類名和其類載入器作爲唯一標識。 例如, 如果在 pg 的包中有一個名爲 Person 的類, 被類載入器 ClassLoader 的範例 kl負責載入, 則該 Person 類對應的 Class 物件在 JVM 中表示爲 Person、pg、kl ) 這意味着兩個類載入器載入的同名類: (Person 、pg、 kl ) 和( Person、 pg、 kl2) 是不同的, 它們所載入的類也是完全不同、互不相容的。

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

  • 根類載入器(Bootstrap ClassLoader):其負責載入Java的核心類,比如String、System這些類
  • 拓展類載入器(Extension ClassLoader):其負責載入JRE的拓展類庫
  • 系統類載入器(System ClassLoader):其負責載入CLASSPATH環境變數所指定的JAR包和類路徑

除了可以使用 Java 提供的類載入器之外, 開發者也可以實現自己的類載入器, 自定義的類載入器通過繼承 ClassLoader 來實現。

  • 使用者類載入器:使用者自定義的載入器,以類載入器爲父類別

類載入器的層次

在这里插入图片描述

**雙親委派模型:如上圖所示的類載入器之間的這種層次關係,就稱爲類載入器的雙親委派模型(Parent Delegation Model)。該模型要求除了頂層的啓動類載入器外,其餘的類載入器都應當有自己的父類別載入器。子類載入器和父類別載入器不是以繼承(Inheritance)的關係來實現,而是通過組合(Composition)關係來複用父載入器的程式碼。
雙親委派模型的工作過程爲:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類別載入器去完成,每一個層次的載入器都是如此,因此所有的類載入請求都會傳給頂層的啓動類載入器,只有當父載入器反饋自己無法完成該載入請求(該載入器的搜尋範圍中沒有找到對應的類)時,子載入器纔會嘗試自己去載入。
**

JVM 的類載入機制 機製主要有如下三種:

  • 全盤負責。 所謂全盤負責, 就是當一個類載入器負責載入某個 Class 時, 該 Class 所依賴的和參照的其他 Class 也將由該類載入器負責載入, 除非顯式使用另外一個類載入器來載入。
  • 父類別委託。 所謂父類別委託, 則是先讓 parent (父) 類載入器試圖載入該 Class, 只有在父類別載入器無法載入該類時才嘗試從自己的類路徑中載入該類。
  • 快取機制 機製。快取機制 機製將會保證所有載入過的 Class 都會被快取, 當程式中需要使用某個 Class 時,類載入器先從快取區中搜尋該 Class, 只有當快取區中不存在該 Class 物件時, 系統纔會讀取該類對應的二進制數據, 並將其轉換成Class 物件, 存入快取區中。 這就是爲什麼修改了 Class 後,必須重新啓動 JVM, 程式所做的修改纔會生效的原因。

程式存取 JVM 的類載入器:

import java.util.*;
import java.net.*;
import java.io.*;

public class ClassLoaderPropTest {
    public static void main(String[] args) throws IOException {
        // 獲取系統類載入器
        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系統類載入器:" + systemLoader);
        /*
         * 獲取系統類載入器的載入路徑——通常由CLASSPATH環境變數指定 如果操作系統沒有指定CLASSPATH環境變數,預設以當前路徑作爲
         * 系統類載入器的載入路徑
         */
        Enumeration<URL> em1 = systemLoader.getResources("");
        while (em1.hasMoreElements()) {
            System.out.println(em1.nextElement());
        }
        // 獲取系統類載入器的父類別載入器:得到擴充套件類載入器
        ClassLoader extensionLader = systemLoader.getParent();
        System.out.println("擴充套件類載入器:" + extensionLader);
        System.out.println("擴充套件類載入器的載入路徑:" + System.getProperty("java.ext.dirs"));
        System.out.println("擴充套件類載入器的parent: " + extensionLader.getParent());
    }
}

執行結果:
在这里插入图片描述系統類載入器是 AppClassLoader 的範例, 擴充套件類載入器 PlatformClassLoader
的範例。 實際上, 這兩個類都是 URLClassLoader 類的範例。

根類載入器並不是Java實現的,而且由於程式通常須存取根載入器,因此存取擴充套件類載入器的父類別載入器時返回NULL。

類載入器載入 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() 方法來實現自己的載入過程。

API:java.lang.ClassLoader

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

JVM 中除根類載入器之外的所有類載入器都是 ClassLoader 子類的範例, 開發者可以通過擴充套件ClassLoader 的子類, 並重寫該 ClassLoader 所包含的方法來實現自定義的類載入器。 查閱 API 文件中關於 ClassLoader的方法不難發現, ClassLoader 中包含了大量的 protected 方法 這些方法都可被子類重寫。

ClassLoader 類有如下兩個關鍵方法:

  • loadClass(String name,boolean resolve) 該方法爲 ClassLoader 的入口點, 根據指定名稱來載入類, 系統就是呼叫 ClassLoader 的該方法來獲取指定類對應的 Class 物件。
  • findClass(String name) 根據指定名稱來查詢類。如果需要實現自定義的ClassLoader, 則可以通過重寫以上兩個方法來實現, 通常推薦重寫 fmdClass()方法, 而不是重寫 loadClass()方法。

loadClass()方法的執行步驟如下:

  • 用 flndLoadedClass(String) 來檢查是否已經載入類, 如果己經載入則直接返回。
  • 在父類別載入器上呼叫loadClass()方法。如果父類別載入器爲null,則使用根類載入器來載入。
  • 呼叫findClass(String)方法查詢類。

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

在 ClassLoader 裡還有一個核心方法:Class defineClass(String name, byte[] b, int off, int len),該方法 負責將指定類的位元組碼檔案(即Class檔案,如Hello.class)讀入位元組陣列byte[] b內,並把它轉換爲 Class物件,該位元組碼檔案可以來源於檔案、網路等。

defileClass()方法管理JVM的許多複雜的實現,它負責將位元組碼分析成執行時數據結構,並校驗有效性等。

除此之外,ClassLoader裡還包含如下一些普通方法。

  • findSystemClass(String name):從本地檔案系統裝入檔案。它在本地檔案系統中尋找類檔案,如 果存在,就使用defineClass()方法將原始位元組轉換成Class物件,以將該檔案轉換成類。
  • static getSystemClassLoader():這是一個靜態方法,用於返回系統類載入器。
  • getParent():獲取該類載入器的父類別載入器。
  • resolveClass(CIass<?> c):鏈接指定的類。類載入器可以使用此方法來鏈接類c。
  • findLoadedClass(String name):如果此Java虛擬機器已載入了名爲name的類,則直接返回該類對 應的Class範例,否則返回null。該方法是Java類載入快取機制 機製的體現。

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

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.neo.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定義類載入器的核心在於對位元組碼檔案的獲取,如果是加密的位元組碼則需要在該類中對檔案進行解密。由於這裏只是演示,我並未對class檔案進行加密,因此沒有解密的過程。這裏有幾點需要注意:

  • 1、這裏傳遞的檔名需要是類的全限定性名稱,即 com. paddx.test . classloading.Test格式的,因爲 defineClass 方法是按這種格式進行處理的。
  • 2、最好不要重寫loadClass方法,因爲這樣容易破壞雙親委託模式。
  • 3、這類Test 類本身可以被 AppClassLoader類載入,因此我們不能把com/paddx/test/classloading/Test.class放在類路徑下。否則,由於雙親委託機制 機製的存在,會直接導致該類由AppClassLoader載入,而不會通過我們自定義類載入器來載入。

URLCIassLoader 類

Java 爲ClassLoader 提供了一個 URLClassLoader 實現類, 該類也是系統類載入器和擴充套件類載入器的父類別( 此處的父類別, 就是指類與類之間的繼承關係)。 URLClassLoader 功能比較強大, 它既可以從本地檔案系統獲取二進制檔案來載入類, 也可以從遠端主機獲取二進制檔案來載入類。

在應用程式中可以直接使用 URLClassLoader 載入類, URLClassLoader 類提供瞭如下兩個構造器:

  • URLClassLoader(URL[] urls): 使用預設的父類別載入器建立一個 ClassLoader 物件, 該物件將從urls 所指定的系列路徑來查詢並載入類。
  • URLClassLoader(URL[] urls,ClassLoader parent): 使用指定的父類別載入器建立一個 ClassLoader物件, 其他功能與前一個構造器相同。

一旦得到了 URLClassLoader 物件之後, 就可以呼叫該物件的 loadClass()方法來載入指定類。 下面 下麪程式示範瞭如何直接從檔案系統中載入 MySQL 驅動, 並使用該驅動來獲取數據庫連線。 通過這種方式來獲取數據庫連線, 可以無須將 MySQL 驅動新增到 CLASSPATH 環境變數中。

import java.sql.*;
import java.util.*;
import java.net.*;

public class URLClassLoaderTest {
    private static Connection conn;

    // 定義一個獲取數據庫連線方法
    public static Connection getConn(String url, String user, String pass) throws Exception {
        if (conn == null) {
            // 建立一個URL陣列
            URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };
            // 以預設的ClassLoader作爲父ClassLoader,建立URLClassLoader
            URLClassLoader myClassLoader = new URLClassLoader(urls);
            // 載入MySQL的JDBC驅動,並建立預設範例
            Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
            // 建立一個設定JDBC連線屬性的Properties物件
            Properties props = new Properties();
            // 至少需要爲該物件傳入user和password兩個屬性
            props.setProperty("user", user);
            props.setProperty("password", pass);
            // 呼叫Driver物件的connect方法來取得數據庫連線
            conn = driver.connect(url, props);
        }
        return conn;
    }

    public static void main(String[] args) throws Exception {
        System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));
    }
}

API:java.net.URLClassLoader

通過反射檢視類資訊

Java 程式中的許多物件在執行時都會出現兩種型別: 編譯時型別和執行時型別, 例如程式碼: Person p=new Student();,這行程式碼將會生成一個 p 變數, 該變數的編譯時型別爲 Person,執行時型別爲 Student;除此之外, 還有更極端的情形, 程式在執行時接收到外部傳入的一個物件, 該物件的編譯時型別是 Object,但程式又需要呼叫該物件執行時型別的方法。

獲得 Class 物件

在 Java 程式中獲得 Class 物件通常有如下三種方式:

  • 使用 Class 類的 forName(String clazzName)靜態方法。 該方法需要傳入字串參數, 該字串參數的值是某個類的全限定類名( 必須新增完整包名)。
  • 呼叫某個類的 class 屬性來獲取該類對應的 Class 物件。 例如, Person.class 將會返回 Person 類對應的 Class 物件。
  • 呼叫某個物件的 getClass()方法。 該方法是 java.lang.Object 類中的一個方法, 所以所有的 Java物件都可以呼叫該方法, 該方法將會返回該物件所屬類對應的 Class 物件。

對於第一種方式和第二種方式都是直接根據類來取得該類的 Class 物件, 相比之下, 第二種方式有如下兩種優勢:

  • 程式碼更安全。 程式在編譯階段就可以檢查需要存取的 Class 物件是否存在。
  • 程式效能更好。 因爲這種方式無須呼叫方法, 所以效能更好。

也就是說, 大部分時候都應該使用第二種方式來獲取指定類的 Class 物件。 但如果程式只能獲得一個字串, 例如」java.lang.String」, 若需要獲取該字串對應的 Class 物件, 則只能使用第一種方式, 使用Class 的 forName(String clazzName)方法獲取 Class 物件時, 該方法可能拋出一個 ClassNotFoundException異常。

一旦獲得了某個類所對應的 Class 物件之後, 程式就可以呼叫 Class 物件的方法來獲得該物件和該類的資訊了。

從 Class 中獲取資訊

Class 類提供了大量的實體方法來獲取該 Class 物件所對應類的詳細資訊,Class 類大致包含如下方法, 下面 下麪每個方法都可能包括多個過載的版本, 應該參照官方API。

下面 下麪 4 個方法用於獲取 Class 對應類所包含的構造器:

  • Connstructor<\T> getConstructor(Class<?>…parameterTypes): 返回此 Class 物件對應類的、 帶指定形參列表的 public 構造器。
  • Constructor<?>[]getConstructors(): 返回此 Class 物件對應類的所有 public 構造器。
  • Constructor<\T> getDeclaredConstructor(Class<?>…parameterTypes): 返回此 Class 物件對應類的、帶指定形參列表的構造器, 與構造器的存取許可權無關。
  • Constructor<?>[]getDeclaredConstructors(): 返回此 Class 物件對應類的所有構造器, 與構造器的存取許可權無關。

下面 下麪 4 個方法用於獲取 Class 對應類所包含的方法:

  • Method getMethod(String name,Class<?>…parameterTypes): 返回此 Class 物件對應類的、 帶指定形參列表的 public 方法。
  • Method[]getMethods(): 返回此 Class 物件所表示的類的所有 public 方法。
  • Method getDeclaredMethod(String name,Class<?>…parameterTypes): 返回此 Class 物件對應類的、帶指定形參列表的方法, 與方法的存取許可權無關。
  • Method[]getDeclaredMethods(): 返回此 Class 物件對應類的全部方法, 與方法的存取許可權無關。

下 4 個方法用於存取 Class 對應類所包含的成員變數:

  • Field getField(String name): 返回此 Class 物件對應類的、 指定名稱的 public 成員變數。
  • Field[]getFields(): 返回此 Class 物件對應類的所有 public 成員變數。
  • Field getDeclaredField(String name): 返回此 Class 物件對應類的、 指定名稱的成員變數, 與成員變數的存取許可權無關。
  • Field[]getDeclaredFields(): 返回此 Class 物件對應類的全部成員變數, 與成員變數的存取許可權無關。

如下幾個方法用於存取 Class 對應類上所包含的 Annotation:

  • <A extends Annotation〉 A getAnnotation(Class<\A> annotationClass): 嘗試獲取該 Class 物件對應類上存在的、 指定型別的 Annotation; 如果該型別的註解不存在, 則返回 null。
  • <A extends Annotation〉 A getDeclaredAnnotation(Class<\A> annotationClass): 這是 Java 8 新增的方法, 該方法嘗試獲取直接修飾該 Class 物件對應類的、 指定型別的 Annotation; 如果該型別的註解不存在, 則返回 null。
  • Annotation[] getAnnotations(): 返回修飾該 Class 物件對應類上存在的所有 Annotation。
  • Annotation[] getDeclaredAnnotations(): 返回直接修飾該 Class 對應類的所有 Annotation。
  • < A extends Annotation> A[] getAnnotationsByType(Class<\A> annotationClass): 該方法的功能與前面介紹的 getAnnotation()方法基本相似。 但由於 Java 8 增加了重複註解功能, 因此需要使用該方法獲取修飾該類的、 指定型別的多個 Annotation。
  • < A extends Annotation> A[] getDeclaredAnnotationsByType(Class<\A> annotationClass): 該方法的
    功能與前面介紹的 getDeclaredAnnotations ()方法基本相似。 但由於 Java 8 增加了重複註解功能,因此需要使用該方法獲取直接修飾該類的、 指定型別的多個 Annotation。

如下方法用於存取該 Class 物件對應類包含的內部類:

  • Class<?>[] getDeclaredClasses(): 返回該 Class 物件對應類裡包含的全部內部類。

如下方法用於存取該 Class 物件對應類所在的外部類:

  • Class<?> getDeclaringClass(): 返回該 Class 物件對應類所在的外部類。

如下方法用於存取該 Class 物件對應類所實現的介面:

  • Class<?>[] getlnterfaces(): 返回該 Class 物件對應類所實現的全部介面。

如下幾個方法用於存取該 Class 物件對應類所繼承的父類別:

  • Class<? super T> getSuperclass(): 返回該 Class 物件對應類的超類的 Class 物件。

如下方法用於獲取 Class 物件對應類的修飾符、 所在包、 類名等基本資訊:

  • int getModifiers(): 返回此類或介面的所有修飾符。 修飾符由 public、 protected、 private、 final、static、 abstract 等對應的常數組成, 返回的整數應使用 Modifier 工具類的方法來解碼, 纔可以獲取真實的修飾符。
  • Package getPackage(): 獲取此類的包。
  • String getName(): 以字串形式返回此 Class 物件所表示的類的名稱。
  • String getSimpleName(): 以字串形式返回此 Class 物件所表不的類的簡稱。

除此之外, Class 物件還可呼叫如下幾個判斷方法來判斷該類是否爲介面、 列舉、 註解型別等:

  • boolean isAnnotation(): 返回此 Class 物件是否表示一個註解型別(由@interface 定義)。
  • boolean isAnnotationPresent(Class<? extends Annotation〉 annotationClass): 判斷此 Class 物件是否使用了 Annotation 修飾。
  • boolean isAnonymousClass(): 返回此 Class 物件是否是一個匿名類。
  • boolean isArray(): 返回此 Class 物件是否表不一個數組類。
  • boolean isEnum(): 返回此 Class 物件是否表不一個列舉(由
  • boolean islnterface(): 返回此 Class 物件是否表示一個介面( 使用 interface 定義)。
  • boolean isInstance(Object obj): 判斷 obj 是否是此 Class 物件的範例, 該方法可以完全代替instanceof 操作符。

上面的多個 getMethod()方法和 getConstructor()方法中, 都需要傳入多個型別爲 Class<?〉的參數, 用於獲取指定的方法或指定的構造器。 關於這個參數的作用, 假設某個類內包含如下三個 info 方法簽名:

  • public void info()
  • public void info(String str)
  • public void info(String str , Integer num)

在程式中獲取該方法使用如下程式碼:

// 前一個參數指定方法名, 後面的個數可變的 Class 參數指定形參型別列表
clazz.getMethod("info" , String.class)
// 前一個參數指定方法名, 後面的個數可變的 Class 參數指定形參型別列表
clazz.getMethod("info" , String.class, Integer.class)
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;


// 定義可重複註解
@Repeatable(Annos.class)
@interface Anno {
}

@Retention(value = RetentionPolicy.RUNTIME)
@interface Annos {
    Anno[] value();
}

// 使用4個註解修飾該類
@SuppressWarnings(value = "unchecked")
@Deprecated
// 使用重複註解修飾該類
@Anno
@Anno
public class ClassTest {
    // 爲該類定義一個私有的構造器
    private ClassTest() {
    }

    // 定義一個有參數的構造器
    public ClassTest(String name) {
        System.out.println("執行有參數的構造器");
    }

    // 定義一個無參數的info方法
    public void info() {
        System.out.println("執行無參數的info方法");
    }

    // 定義一個有參數的info方法
    public void info(String str) {
        System.out.println("執行有參數的info方法" + ",其str參數值:" + str);
    }

    // 定義一個測試用的內部類
    class Inner {
    }

    public static void main(String[] args) throws Exception {
        // 下面 下麪程式碼可以獲取ClassTest對應的Class
        Class<ClassTest> clazz = ClassTest.class;
        // 獲取該Class物件所對應類的全部構造器
        Constructor[] ctors = clazz.getDeclaredConstructors();
        System.out.println("ClassTest的全部構造器如下:");
        for (Constructor c : ctors) {
            System.out.println(c);
        }
        // 獲取該Class物件所對應類的全部public構造器
        Constructor[] publicCtors = clazz.getConstructors();
        System.out.println("ClassTest的全部public構造器如下:");
        for (Constructor c : publicCtors) {
            System.out.println(c);
        }
        // 獲取該Class物件所對應類的全部public方法
        Method[] mtds = clazz.getMethods();
        System.out.println("ClassTest的全部public方法如下:");
        for (Method md : mtds) {
            System.out.println(md);
        }
        // 獲取該Class物件所對應類的指定方法
        System.out.println("ClassTest裏帶一個字串參數的info()方法爲:" + clazz.getMethod("info", String.class));
        // 獲取該Class物件所對應類的上的全部註解
        Annotation[] anns = clazz.getAnnotations();
        System.out.println("ClassTest的全部Annotation如下:");
        for (Annotation an : anns) {
            System.out.println(an);
        }
        System.out.println("該Class元素上的@SuppressWarnings註解爲:"
                + Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
        System.out.println("該Class元素上的@Anno註解爲:" + Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
        // 獲取該Class物件所對應類的全部內部類
        Class<?>[] inners = clazz.getDeclaredClasses();
        System.out.println("ClassTest的全部內部類如下:");
        for (Class c : inners) {
            System.out.println(c);
        }
        // 使用Class.forName方法載入ClassTest的Inner內部類
        Class inClazz = Class.forName("ClassTest$Inner");
        // 通過getDeclaringClass()存取該類所在的外部類
        System.out.println("inClazz對應類的外部類爲:" + inClazz.getDeclaringClass());
        System.out.println("ClassTest的包爲:" + clazz.getPackage());
        System.out.println("ClassTest的父類別爲:" + clazz.getSuperclass());
    }
}

API:java.lang.Class

使用反射生成並操作物件

Class 物件可以獲得該類裡的方法( 由 Method 物件表示)、 構造器( 由 Constructor 物件表示)、 成員變數( 由 Field 物件表示), 這三個類都位於 java.lang.reflect 包下, 並實現了 java.lang.reflect.Member介面。 程式可以通過 Method 物件來執行對應的方法, 通過 Constructor 物件來呼叫對應的構造器建立範例, 能通過 Field 物件直接存取並修改物件的成員變數值。

建立物件

通過反射來生成物件需要先使用 Class 物件獲取指定的 Constructor 物件, 再呼叫 Constructor 物件的 newlnstance()方法來建立該 Class 物件對應類的範例。 通過這種方式可以選擇使用指定的構造器來建立範例。

在很多 Java EE (例如Spring)框架中都需要根據組態檔資訊來建立 Java 物件,從組態檔讀取的只是某個類的字串類名, 程式需要根據該字串來建立對應的範例, 就必須使用反射。

下面 下麪程式就實現了一個簡單的物件池, 該物件池會根據組態檔讀取 key-value 對, 然後建立這些物件, 並將這些物件放入一個 HashMap 中:

import java.util.*;
import java.io.*;


public class ObjectPoolFactory {
    // 定義一個物件池,前面是物件名,後面是實際物件
    private Map<String, Object> objectPool = new HashMap<>();

    // 定義一個建立物件的方法
    // 該方法只要傳入一個字串類名,程式可以根據該類名生成Java物件
    private Object createObject(String clazzName) throws Exception, IllegalAccessException, ClassNotFoundException {
        // 根據字串來獲取對應的Class物件
        Class<?> clazz = Class.forName(clazzName);
        // 使用clazz對應類的預設構造器建立範例
        return clazz.getConstructor().newInstance();
    }

    // 該方法根據指定檔案來初始化物件池
    // 它會根據組態檔來建立物件
    public void initPool(String fileName)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream(fileName)) {
            Properties props = new Properties();
            props.load(fis);
            for (String name : props.stringPropertyNames()) {
                // 每取出一對key-value對,就根據value建立一個物件
                // 呼叫createObject()建立物件,並將物件新增到物件池中
                objectPool.put(name, createObject(props.getProperty(name)));
            }
        } catch (Exception ex) {
            System.out.println("讀取" + fileName + "異常");
        }
    }

    public Object getObject(String name) {
        // 從objectPool中取出指定name對應的物件
        return objectPool.get(name);
    }

    public static void main(String[] args) throws Exception {
        ObjectPoolFactory pf = new ObjectPoolFactory();
        pf.initPool("obj.txt");
        System.out.println(pf.getObject("a")); // ①
        System.out.println(pf.getObject("b")); // ②
    }
}

程式呼叫 Class 物件的 newlnstance()方法即可建立一個 Java 物件。 程式中的 initPool()方法會讀取屬性檔案, 對屬性檔案中每個 key-value 對建立一個 Java 物件, 其中 value 是該 Java 物件的實現類, 而 key 是該 Java 物件放入物件池中的名字。 爲該程式提供如下屬性組態檔:

obj.txt

a=java.util.Date
b=javax.swing.JFrame

如果不想利用預設構造器來建立 Java 物件, 而想利用指定的構造器來建立 Java 物件, 則需要利用Constructor 物件, 每個 Constructor 對應一個構造器。 爲了利用指定的構造器來建立 Java 物件, 需要如下三個步驟:

  • 獲取該類的 Class 物件:
  • 利用 Class 物件的 getConstructor()方法來獲取指定的構造器。
  • 呼叫 Constructor 的 newlnstance()方法來建立 Java 物件。

下面 下麪程式利用反射來建立一個 JFrame 物件, 而且使用指定的構造器:

import java.lang.reflect.*;

public class CreateJFrame {
    public static void main(String[] args) throws Exception {
        // 獲取JFrame對應的Class物件
        Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
        // 獲取JFrame中帶一個字串參數的構造器
        Constructor ctor = jframeClazz.getConstructor(String.class);
        // 呼叫Constructor的newInstance方法建立物件
        Object obj = ctor.newInstance("測試視窗");
        // 輸出JFrame物件
        System.out.println(obj);
    }
}

呼叫方法

當獲得某個類對應的 Class 物件後, 就可以通過該 Class 物件的 getMethods()方法或者 getMethod()方法來獲取全部方法或指定方法—這兩個方法的返回值是 Method 陣列, 或者 Method 物件。每個 Method 物件對應一個方法, 獲得 Method 物件後, 程式就可通過該 Method 來呼叫它對應的方法。 在 Method 裡包含一個 invoke()方法, 該方法的簽名如下:

  • Object invoke(Object obj,Object…args): 該方法中的 obj 是執行該方法的主調, 後面的 args 是執行該方法時傳入該方法的實參。

下面 下麪程式對前面的物件池工廠進行加強, 允許在組態檔中增加設定物件的成員變數的值, 物件池工廠會讀取爲該物件設定的成員變數值, 並利用該物件對應的 setter 方法設定成員變數的值:

import java.util.*;
import java.io.*;
import java.lang.reflect.*;


public class ExtendedObjectPoolFactory {
    // 定義一個物件池,前面是物件名,後面是實際物件
    private Map<String, Object> objectPool = new HashMap<>();
    private Properties config = new Properties();

    // 從指定屬性檔案中初始化Properties物件
    public void init(String fileName) {
        try (FileInputStream fis = new FileInputStream(fileName)) {
            config.load(fis);
        } catch (IOException ex) {
            System.out.println("讀取" + fileName + "異常");
        }
    }

    // 定義一個建立物件的方法
    // 該方法只要傳入一個字串類名,程式可以根據該類名生成Java物件
    private Object createObject(String clazzName) throws Exception {
        // 根據字串來獲取對應的Class物件
        Class<?> clazz = Class.forName(clazzName);
        // 使用clazz對應類的預設構造器建立範例
        return clazz.getConstructor().newInstance();
    }

    // 該方法根據指定檔案來初始化物件池
    // 它會根據組態檔來建立物件
    public void initPool() throws Exception {
        for (String name : config.stringPropertyNames()) {
            // 每取出一個key-value對,如果key中不包含百分號(%)
            // 這就表明是根據value來建立一個物件
            // 呼叫createObject建立物件,並將物件新增到物件池中
            if (!name.contains("%")) {
                objectPool.put(name, createObject(config.getProperty(name)));
            }
        }
    }

    // 該方法將會根據屬性檔案來呼叫指定物件的setter方法
    public void initProperty() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        for (String name : config.stringPropertyNames()) {
            // 每取出一對key-value對,如果key中包含百分號(%)
            // 即可認爲該key用於控制呼叫物件的setter方法設定值
            // %前半爲物件名字,後半控制setter方法名
            if (name.contains("%")) {
                // 將組態檔中的key按%分割
                String[] objAndProp = name.split("%");
                // 取出調用setter方法的參數值
                Object target = getObject(objAndProp[0]);
                // 獲取setter方法名:set + "首字母大寫" + 剩下部分
                String mtdName = "set" + objAndProp[1].substring(0, 1).toUpperCase() + objAndProp[1].substring(1);
                // 通過target的getClass()獲取它的實現類所對應的Class物件
                Class<?> targetClass = target.getClass();
                // 獲取希望呼叫的setter方法
                Method mtd = targetClass.getMethod(mtdName, String.class);
                // 通過Method的invoke方法執行setter方法
                // 將config.getProperty(name)的值作爲呼叫setter方法的參數
                mtd.invoke(target, config.getProperty(name));
            }
        }
    }

    public Object getObject(String name) {
        // 從objectPool中取出指定name對應的物件
        return objectPool.get(name);
    }

    public static void main(String[] args) throws Exception {
        ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
        epf.init("extObj.txt");
        epf.initPool();
        epf.initProperty();
        System.out.println(epf.getObject("a"));
    }
}
78

爲上面程式提供如下組態檔:

ext0bj.text

a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title

Spring 框架就是通過這種方式將成員變數值以及依賴物件等都放在組態檔中進行 ,管理的, 從而實現了較好的解耦。 這也是 Spring 框架的 IOC 的原理。

當通過Method的invoke()方法來呼叫對應的方法時,Java會要求程式必須有呼叫該方法的許可權。 如果程式確實需要呼叫某個物件的private方法,則可以先呼叫Method物件的如下方法: * setAccessible(boolean flag):將Method物件的accessible設定爲指定的布爾值。值爲true,指示 該Method在使用時應該取消Java語言的存取許可權檢査;值爲false,則指示該Method在使用時 要實施Java語言的存取許可權檢查。

API:java.lang.reflect.Method

存取成員變數值

通過 Class 物件的 getFields()或 getField()方法可以獲取該類所包括的全部成員變數或指定成員變數。
Field 提供瞭如下兩組方法來讀取或設定成員變數值:

  • getXxx(Object obj): 獲取 obj 物件的該成員變數的值。 此處的 Xxx 對應 8 種基本型別, 如果該成員變數的型別是參照型別, 則取消 get 後面的 Xxx。
  • setXxx(Object obj ,Xxx val): 將 obj 物件的該成員變數設定成 val 值。 此處的 Xxx 對 應 8 種 基 本型別, 如果該成員變數的型別是參照型別, 則取消 set 後面的 Xxx。

使用這兩個方法可以隨意地存取指定物件的所有成員變數, 包括 private 修飾的成員變數。

import java.lang.reflect.*;

class Person {
    private String name;
    private int age;

    public String toString() {
        return "Person[name:" + name + " , age:" + age + " ]";
    }
}

public class FieldTest {
    public static void main(String[] args) throws Exception {
        // 建立一個Person物件
        Person p = new Person();
        // 獲取Person類對應的Class物件
        Class<Person> personClazz = Person.class;
        // 獲取Person的名爲name的成員變數
        // 使用getDeclaredField()方法表明可獲取各種存取控制符的成員變數
        Field nameField = personClazz.getDeclaredField("name");
        // 設定通過反射存取該成員變數時取消存取許可權檢查
        nameField.setAccessible(true);
        // 呼叫set()方法爲p物件的name成員變數設定值
        nameField.set(p, "Yeeku.H.Lee");
        // 獲取Person類名爲age的成員變數
        Field ageField = personClazz.getDeclaredField("age");
        // 設定通過反射存取該成員變數時取消存取許可權檢查
        ageField.setAccessible(true);
        // 呼叫setInt()方法爲p物件的age成員變數設定值
        ageField.setInt(p, 30);
        System.out.println(p);
    }
}

API:java.lang.reflect.Field

運算元組

在 java.lang.reflect 包下還提供了一個 Array 類, Array 物件可以代表所有的陣列。 程式可以通過使用 Array 來動態地建立陣列, 運算元組元素等。

Array 提供瞭如下幾類方法:

  • static Object newInstance(Class<?> componentType,int… length): 建立一個具有指定的元素型別、指定維度的新陣列。
  • static xxx getXxx(Object array,int index): 返回 array 陣列中第 index 個元素。 其中 xxx 是各種基本數據型別, 如果陣列元素是參照型別, 則該方法變爲 get(Object array,int index)。
  • static void setXxx(Object array,int index,xxx val): 將 array 陣列中第 index 個元素的值設爲 val。其中 XXX 是各種基本數據型別, 如果陣列元素是參照型別, 則該方法變成 set(Object array, int index,Object val)。

下面 下麪程式示範瞭如何使用 Array 來生成陣列, 爲指定陣列元素賦值, 並獲取指定陣列元素的方式:

import java.lang.reflect.*;

public class ArrayTest1 {
	public static void main(String args[]) {
		try {
			// 建立一個元素型別爲String ,長度爲10的陣列
			Object arr = Array.newInstance(String.class, 10);
			// 依次爲arr陣列中index爲5、6的元素賦值
			Array.set(arr, 5, "瘋狂Java講義");
			Array.set(arr, 6, "輕量級Java EE企業應用實戰");
			// 依次取出arr陣列中index爲5、6的元素的值
			Object book1 = Array.get(arr, 5);
			Object book2 = Array.get(arr, 6);
			// 輸出arr陣列中index爲5、6的元素
			System.out.println(book1);
			System.out.println(book2);
		} catch (Throwable e) {
			System.err.println(e);
		}
	}
}
123456789101112131415161718192021

API:java.lang.reflect.Array

使用反射生成 JDK 動態代理

代理分爲靜態代理和動態代理,靜態代理是在編譯時就將介面、實現類、代理類一股腦兒全部手動完成,但如果我們需要很多的代理,每一個都這麼手動的去建立實屬浪費時間,而且會有大量的重複程式碼,此時我們就可以採用動態代理,動態代理可以在程式執行期間根據需要動態的建立代理類及其範例,來完成具體的功能。

使用 Proxy 和 InvocationHandler 建立動態代理


代理(Proxy)是一種設計模式,提供了對目標物件另外的存取方式;即通過代理物件存取目標物件.這樣做的好處是:可以在目標物件實現的基礎上,增強額外的功能操作,即擴充套件目標物件的功能。 這裏用到程式設計中的一個思想:不要隨意去修改別人已經寫好的程式碼或者方法,如果需改修改,可以通過代理的方式來擴充套件該方法。 舉個例子來說明代理的作用:假設我們想邀請一位明星,那麼並不是直接連線明星,而是聯繫明星的經紀人,來達到同樣的目的.明星就是一個目標物件,他只要負責活動中的節目,而其他瑣碎的事情就交給他的代理人(經紀人)來解決.這就是代理思想在現實中的一個例子。

在这里插入图片描述


Proxy 提供了用於建立動態代理類和代理物件的靜態方法, 它也是所有動態代理類的父類別。 如果在程式中爲一個或多個介面動態地生成實現類, 就可以使用 Proxy 來建立動態代理類; 如果需要爲一個或多個介面動態地建立範例, 也可以使用 Proxy 來建立動態代理範例。

​ Java動態代理UML圖

在这里插入图片描述

Proxy 提供瞭如下兩個方法來建立動態代理類和動態代理範例:

  • static Class<?> getProxyClass(ClassLoader loader,Class<?>… interfaces): 建立一個動態代理類所對應的 Class 物件, 該代理類將實現 interfaces 所指定的多個介面。 第一個 ClassLoader 參數指定生成動態代理類的類載入器。
  • static Object newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h): 直接建立一個動態代理物件, 該代理物件的實現類實現了 interfaces 指定的系列介面, 執行代理物件的每個方法時都會被替換執行 InvocationHandler 物件的 invoke 方法。

實際上, 即使採用第一個方法生成動態代理類之後, 如果程式需要通過該代理類來建立物件, 依然需要傳入一個 InvocationHandler 物件。 也就是說, 系統生成的每個代理物件都有一個與之關聯的InvocationHandler 物件。

程式中可以採用先生成一個動態代理類, 然後通過動態代理類來建立代理物件的方式生成一個動態代理物件。 程式碼片段如下:

程式中可以採用先生成一個動態代理類, 然後通過動態代理類來建立代理物件的方式生成一個動態代理物件。 程式碼片段如下:

// 建立一個 InvocationHandler 物件
InvocationHandler handler = new MylnvocationHandler ( ...)// 使 用 Proxy 生成一個動態代理類 proxyClass
Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader()
, new Class[] { Foo.class });
// 會取 proxyClass_中帶一個
Constructor ctor = proxyClass. getConstructor(new Class[]
InvocationHandler 參數的構造器
{ InvocationHandler•class });
// 呼叫 ctor 的 newlnstance 方法來建立動態範例
Foo f (Foo)ctor.newlnstance(new Object[]{handler});
1234567891011

上面程式碼也可以簡化成如下程式碼:

// 建立一個 InvocationHandler 物件
InvocationHandler handler = new MyInvocationHandler(...);
// 使用 Proxy 直接生成一個動態代理物件
Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader()
, new Class[]{Foo.class} , handler);
12345

下面 下麪程式示範了使用 Proxy 和 InvocationHandler 來生成動態代理物件:

import java.lang.reflect.*;


interface Person {
	void walk();

	void sayHello(String name);
}

class MyInvokationHandler implements InvocationHandler {
	/*
	 * 執行動態代理物件的所有方法時,都會被替換成執行如下的invoke方法 其中: proxy:代表動態代理物件 method:代表正在執行的方法
	 * args:代表呼叫目標方法時傳入的實參。
	 */
	public Object invoke(Object proxy, Method method, Object[] args) {
		System.out.println("----正在執行的方法:" + method);
		if (args != null) {
			System.out.println("下面 下麪是執行該方法時傳入的實參爲:");
			for (Object val : args) {
				System.out.println(val);
			}
		} else {
			System.out.println("呼叫該方法沒有實參!");
		}
		return null;
	}
}

public class ProxyTest {
	public static void main(String[] args) throws Exception {
		// 建立一個InvocationHandler物件
		InvocationHandler handler = new MyInvokationHandler();
		// 使用指定的InvocationHandler來生成一個動態代理物件
		Person p = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[] { Person.class },
				handler);
		// 呼叫動態代理物件的walk()和sayHello()方法
		p.walk();
		p.sayHello("孫悟空");
	}
}
12345678910111213141516171819202122232425262728293031323334353637383940

上面程式首先提供了一個 Person 介面, 該介面中包含了 walk()和 sayHello()兩個抽象方法, 接着定義了一個簡單的 InvocationHandler 實現類, 定義該實現類時需要重寫 invoke()方法—呼叫代理物件的所有方法時都會被替換成呼叫該 invoke()方法。 該 invoke()方法中的三個參數解釋如下。

  • proxy:代表動態代理物件。
  • method: 代表正在執行的方法。
  • args: 代表呼叫目標方法時傳入的實參。

API:java.lang.reflect.Proxy
API:java.lang.reflect.InvocationHandler

動態代理和 AOP

根據前面介紹的 Proxy 和 InvocationHandler, 實在很難看出這種動態代理的優勢。 下面 下麪介紹一種更實用的動態代理機制 機製。

開發實際應用的軟件系統時, 通常會存在相同程式碼段重複出現的情況, 在這種情況下, 對於許多剛開始從事軟件開發的人而言, 他們的做法是: 選中那些程式碼, 一路「 複製「 、「 貼上」, 立即實現了系統功能, 如果僅僅從軟體功能上來看, 他們確實己經完成了軟件開發。通過這種「 複製」、 「 貼上」 方式開發出來的軟體如圖a所示。

圖 a:多個地方包含相同程式碼的軟體

在这里插入图片描述
採用圖 a所示結構實現的軟件系統, 在軟件開發期間可能會覺得無所謂, 但如果有一天需要修改程式的深色程式碼的實現, 則意味着開啓三份原始碼進行修改。 如果有 100 個地方甚至 1000 個地方使用了這段深色程式碼段, 那麼修改、 維護這段程式碼的工作量將變成噩夢。

這種情況下, 大部分稍有經驗的開發者都會將這段深色程式碼段定義成一個方法, 然後讓另外三段程式碼段直接呼叫該方法即可。 在這種方式下, 軟件系統的結構如圖 b 所示。

圖 b:通過方法呼叫實現程式碼複用
在这里插入图片描述

對於如圖 b 所示的軟件系統, 如果需要修改深色部分的程式碼, 則只要修改一個地方即可, 而呼叫該方法的程式碼段, 不管有多少個地方呼叫了該方法, 都完全無須任何修改, 只要被呼叫方法被修改了,所有呼叫該方法的地方就會自然改變——通過這種方式, 大大降低了軟體後期維護的複雜度。

但採用這種方式來實現程式碼複用依然產生一個重要問題: 程式碼段 1、 程式碼段 2、 程式碼段 3 和深色程式碼段分離開了, 但程式碼段 1、 程式碼段 2 和程式碼段 3 又和一個特定方法耦合了! 最理想的效果是: 程式碼塊1、 程式碼塊 2 和程式碼塊 3 既可以執行深色程式碼部分, 又無須在程式中以寫死方式直接呼叫深色程式碼的方法, 這時就可以通過動態代理來達到這種效果。

由於 JDK 動態代理只能爲介面建立動態代理, 所以下面 下麪先提供一個 Dog 介面, 該介面程式碼非常簡單, 僅僅在該介面裏定義了兩個方法。

public interface Dog {
	// info方法宣告
	void info();

	// run方法宣告
	void run();
}
1234567

上面介面裏只是簡單地定義了兩個方法, 並未提供方法實現。 如果直接使用 Proxy 爲該介面建立動態代理物件, 則動態代理物件的所有方法的執行效果又將完全一樣。 實際情況通常是, 軟件系統會爲該Dog 介面提供一個或多個實現類。 此處先提供一個簡單的實現類: GunDog:

public class GunDog implements Dog {
	// 實現info()方法,僅僅列印一個字串
	public void info() {
		System.out.println("我是一隻獵狗");
	}

	// 實現run()方法,僅僅列印一個字串
	public void run() {
		System.out.println("我奔跑迅速");
	}
}
1234567891011

Dog 的實現類爲每個方法提供了一個簡單實現。 再看需要實現的功能: 讓程式碼段 1、 程式碼段 2 和程式碼段 3 既可以執行深色程式碼部分, 又無須在程式中以寫死方式直接呼叫深色程式碼的方法。 此處假設 info()、 run()兩個方法代表程式碼段 1、 程式碼段 2, 那麼要求: 程式執行 info()、 nm()方法時能呼叫某個通用方法, 但又不想以寫死方式呼叫該方法。

下面 下麪提供一個 DogUtil類, 該類裡包含兩個通用方法:

public class DogUtil {
	// 第一個攔截器方法
	public void method1() {
		System.out.println("=====模擬第一個通用方法=====");
	}

	// 第二個攔截器方法
	public void method2() {
		System.out.println("=====模擬通用方法二=====");
	}
}

123456789101112

藉助於 Proxy 和 InvocationHandler 就可以實現 當程式呼叫 info()方法和 run()方法時, 系統可以「自動」 將 methodl()和 method2()兩個通用方法插入 info()和 run()方法中執行。

這個程式的關鍵在於下面 下麪的 MylnvokationHandler 類, 該類是一個 InvocationHandler 實現類, 該實現類的 invoke()方法將會作爲代理物件的方法實現:

import java.lang.reflect.*;

public class MyInvokationHandler implements InvocationHandler {
	// 需要被代理的物件
	private Object target;

	public void setTarget(Object target) {
		this.target = target;
	}

	// 執行動態代理物件的所有方法時,都會被替換成執行如下的invoke方法
	public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
		DogUtil du = new DogUtil();
		// 執行DogUtil物件中的method1。
		du.method1();
		// 以target作爲主調來執行method方法
		Object result = method.invoke(target, args);
		// 執行DogUtil物件中的method2。
		du.method2();
		return result;
	}
}
12345678910111213141516171819202122

上面程式實現 invoke()方法時包含了一行關鍵程式碼, 這行程式碼通過反射以 target作爲主調來執行 method 方法, 這就是回調了 target 物件的原有方法。 在粗體字程式碼之前呼叫 DogUtil物件的 methodl()方法, 在粗體字程式碼之後呼叫 DogUtil 物件的 method2()方法。

下面 下麪再爲程式提供一個 MyProxyFactory 類, 該物件專爲指定的 target 生成動態代理範例:

import java.lang.reflect.*;


public class MyProxyFactory {
	// 爲指定target生成動態代理物件
	public static Object getProxy(Object target) throws Exception {
		// 建立一個MyInvokationHandler物件
		MyInvokationHandler handler = new MyInvokationHandler();
		// 爲MyInvokationHandler設定target物件
		handler.setTarget(target);
		// 建立、並返回一個動態代理
		return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
	}
}
1234567891011121314

上面的動態代理工廠類提供了一個 getProxy()方法, 該方法爲 target 物件生成一個動態代理物件,這個動態代理物件與 target 實現了相同的介面, 所以具有相同的 public 方法—從這個意義上來看, 動態代理物件可以當成 target 物件使用。 當程式呼叫動態代理物件的指定方法時,實際上將變爲執行MylnvokationH andler 物件的 invoke()方法。

例如, 呼叫動態代理物件的 info()方法, 程式將幵始執行invoke()方法, 其執行步驟如下:

  • 建立 DogUtil 範例
  • 執行 DogUtil 範例的 method1()方法。
  • 使用反射以 target 作爲呼叫者執行 info()方法。
  • 執行 DogUtil 範例的 method2()方法。

通過上面的執行過程可以發現: 當使用動態代理物件來代替 target 物件時, 代理物件的方法就實現了前面的要求—程式執行 info()、 nm()方法時既能「 插入」 methodl()、 method2()通用方法,但 GunDog 的方法中又沒有以寫死方式呼叫 methodl()和 method2()方法。

以一個主程式來測試這種動態代理的效果。

public class Test {
	public static void main(String[] args) throws Exception {
		// 建立一個原始的GunDog物件,作爲target
		Dog target = new GunDog();
		// 以指定的target來建立動態代理
		Dog dog = (Dog) MyProxyFactory.getProxy(target);
		dog.info();
		dog.run();
	}
}
12345678910

上面程式中的 dog 物件實際上是動態代理物件, 只是該動態代理物件也實現了 Dog 介面, 所以也可以當成 Dog 物件使用。 程式執行 dog 的 info()和 run()方法時, 實際上會先執行 DogUtil 的 method1()方法, 再執行 target 物件的 info()和 run()方法, 最後執行 DogUtil 的 method2()方法。

執行上面程式, 會看到如圖 c 所示的執行結果:

圖 c:執行結果
在这里插入图片描述
通過圖 c 所示的執行結果來看, 可以發現採用動態代理可以非常靈活地實現解耦。 通常而言,使用 Proxy 生成一個動態代理時, 往往並不會憑空產生一個動態代理, 這樣沒有太大的實際意義。 通常都是爲指定的目標物件生成動態代理。

這種動態代理在 AOP ( Aspect Orient Programming, 面向切面程式設計) 中被稱爲 AOP 代理, AOP 代理可代替目標物件, AOP 代理包含了目標物件的全部方法。 但 AOP 代理中的方法與目標物件的方法存在差異: AOP 代理裡的方法可以在執行目標方法之前、 之後插入一些通用處理。

AOP 代理包含的方法與目標物件包含的方法示意圖如圖 d所示。

圖d AOP 代理的方法與目標物件的方法示意圖
在这里插入图片描述

CGLIB動態代理:
JDK中提供的生成動態代理類的機制 機製有個鮮明的特點是:
某個類必須有實現的介面,而生成的代理類也只能代理某個類介面定義的方法。
比如:如果上面例子的GunDog實現了繼承自Dog介面的方法外,另外實現了方法eat(),則在產生的動態代理類中不會有這個方法了!更極端的情況是:如果某個類沒有實現介面,那麼這個類就不能用JDK產生動態代理了!
這時候就應該引入CGLIB動態代理了,「CGLIB(Code Generation Library),是一個強大的,高效能,高品質的Code生成類庫,它可以在執行期擴充套件Java類與實現Java介面。」
CGLIB建立某個類A的動態代理類的模式是:
1.查詢A上的所有非final的public型別的方法定義;
2.將這些方法的定義轉換成位元組碼;
3.將組成的位元組碼轉換成相應的代理的class物件;
4.實現 MethodInterceptor介面,用來處理對代理類上所有方法的請求(這個介面和JDK動態代理InvocationHandler的功能和角色是一樣的)