Java學習十七—反射機制:解鎖程式碼的無限可能

ccm03發表於2024-11-03

Java學習十七—反射機制:解鎖程式碼的無限可能

一、關於反射

1.1 簡介

Java 反射(Reflection)是Java 的特徵之一,它允許程式在執行時動態地訪問和操作類的資訊,包括類的屬性、方法和建構函式。

反射機制能夠使程式具備更大的靈活性和擴充套件性

5f4435ae-1d9b-4267-a355-cf75d4ee49861

Oracle 官方對反射的解釋:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

1.2 發展

Java 反射機制的發展歷程可以分為幾個重要的階段,隨著 Java 語言和平臺的演進,反射的功能和使用場景也不斷豐富。

1. Java 1.0 版本(1996 年)

  • 初步引入:Java 在最初的版本中就引入了反射機制的基礎概念,但功能較為有限。主要是為了支援動態載入類和基礎的類資訊查詢。

2. Java 1.1 版本(1997 年)

  • 增強功能:Java 1.1 對反射機制進行了增強,增加了對介面的支援,使得開發者可以更靈活地操作物件。
  • 引入 java.lang.reflect:這個包提供了獲取類的資訊、呼叫方法、訪問屬性的功能。

3. Java 2(JDK 1.2,1998 年)

  • 引入集合框架:反射與集合框架的結合使用得到了廣泛關注,開發者能夠透過反射建立和操作集合中的物件。
  • 安全性增強:反射機制也開始關注安全性,引入了安全管理器,限制某些反射操作。

4. Java 5(JDK 1.5,2004 年)

  • 泛型支援:Java 5 引入了泛型,反射也隨之支援泛型型別的查詢和操作,提升了型別安全性。
  • 註解機制:引入了註解(Annotations),反射機制開始被廣泛應用於框架中,透過反射讀取和處理註解資訊。

5. Java 6(2006 年)及後續版本

  • 效能最佳化:隨著反射在大型框架(如 Spring、Hibernate)中的廣泛應用,Java 的開發團隊逐步對反射的效能進行了最佳化,儘量減少反射操作的開銷。
  • 動態代理:Java 6 中的 java.lang.reflect.Proxy​ 類使得動態代理的實現成為可能,進一步增強了反射的應用場景。

6. Java 8(2014 年)

  • Lambda 表示式:與反射結合使用,提升了函數語言程式設計的靈活性。
  • 增強的型別推斷:使得反射在處理複雜資料型別時變得更加高效和安全。

7. Java 9 及後續版本

  • 模組化系統:Java 9 引入了模組化(Project Jigsaw),反射的使用在模組間的訪問控制上得到了新的關注。
  • API 和效能持續改進:持續最佳化反射的 API 和效能,以適應現代應用程式的需求。

1.3 特點

優點

  • 靈活性:能夠在執行時決定使用哪個類或方法。
  • 動態性:支援在程式執行過程中動態生成類和物件。

缺點

  • 效能開銷:反射操作相對較慢,因為涉及動態解析。
  • 安全性問題:使用反射可能會破壞封裝性,訪問私有成員。
  • 程式碼可讀性:反射使程式碼難以理解和維護。

1.4 應用場景

很多人都認為反射在實際的 Java 開發應用中並不廣泛,其實不然。當我們在使用 IDE(如 Eclipse,IDEA)時,當我們輸入一個物件或類並想呼叫它的屬性或方法時,一按點號,編譯器就會自動列出它的屬性或方法,這裡就會用到反射。

反射最重要的用途就是開發各種通用框架。 很多框架(比如 Spring)都是配置化的(比如透過 XML 檔案配置 Bean),為了保證框架的通用性,它們可能需要根據配置檔案載入不同的物件或類,呼叫不同的方法,這個時候就必須用到反射,執行時動態載入需要載入的物件。

舉一個例子,在運用 Struts 2 框架的開發中我們一般會在 struts.xml​ 裡去配置 Action​,比如:

	   <action name="login"
               class="org.ScZyhSoft.test.action.SimpleLoginAction"
               method="execute">
           <result>/shop/shop-index.jsp</result>
           <result name="error">login.jsp</result>
       </action>

配置檔案與 Action​ 建立了一種對映關係,當 View 層發出請求時,請求會被 StrutsPrepareAndExecuteFilter​ 攔截,然後 StrutsPrepareAndExecuteFilter​ 會去動態地建立 Action 例項。比如我們請求 login.action​,那麼 StrutsPrepareAndExecuteFilter​就會去解析struts.xml檔案,檢索action中name為login的Action,並根據class屬性建立SimpleLoginAction例項,並用invoke方法來呼叫execute方法,這個過程離不開反射。

對與框架開發人員來說,反射雖小但作用非常大,它是各種容器實現的核心。而對於一般的開發者來說,不深入框架開發則用反射用的就會少一點,不過了解一下框架的底層機制有助於豐富自己的程式設計思想,也是很有益的。

以下是一些常見的應用場景:

2736216443-61df8d5be03c61

1. 框架開發

  • 依賴注入(Dependency Injection, DI) :框架如Spring使用反射來建立和管理Bean物件,實現依賴注入。透過反射,框架可以在執行時動態地將依賴物件注入到目標物件中。
  • 面向切面程式設計(Aspect-Oriented Programming, AOP) :框架如Spring AOP使用反射來織入切面,實現橫切關注點的分離。
  • 物件關係對映(Object-Relational Mapping, ORM) :框架如Hibernate使用反射來對映資料庫表和Java物件,實現持久化操作。

2. 單元測試

  • 單元測試框架:框架如JUnit使用反射來發現和執行測試方法。透過反射,測試框架可以自動掃描類中的測試方法並執行它們。
  • Mock物件:在單元測試中,經常需要模擬(mock)一些物件的行為。框架如Mockito使用反射來建立和管理這些Mock物件。

3. 動態代理

  • Java動態代理:透過反射生成實現了特定介面的代理物件,用於實現AOP、遠端呼叫(RPC)等功能。
  • CGLIB動態代理:CGLIB是一個強大的高效能程式碼生成庫,它可以在執行時生成一個類的子類物件,並使用反射來實現方法攔截。

4. 配置檔案解析

  • XML/JSON/YAML配置檔案:許多框架和庫使用反射來解析配置檔案,並根據配置檔案中的資訊動態建立和配置物件。例如,Spring可以透過XML或註解配置檔案來管理Bean。

5. 序列化和反序列化

  • JSON庫:庫如Gson和Jackson使用反射來將Java物件轉換為JSON字串,或將JSON字串轉換為Java物件。
  • XML庫:庫如JAXB使用反射來將Java物件轉換為XML文件,或將XML文件轉換為Java物件。

6. 外掛系統

  • 動態載入外掛:透過反射,可以在執行時動態載入和解除安裝外掛。這種機制常用於開發可擴充套件的應用程式,如IDE、遊戲引擎等。

7. 資料繫結

  • 資料繫結框架:框架如JavaFX和Vaadin使用反射來將使用者介面元件的資料繫結到模型物件上,實現雙向資料繫結。

8. 指令碼語言整合

  • 嵌入指令碼語言:Java可以透過反射來呼叫嵌入的指令碼語言(如JavaScript、Python)中的函式和方法,實現混合程式設計。

9. 安全性和許可權管理

  • 安全管理:透過反射可以動態地檢查和設定物件的訪問許可權,實現細粒度的安全控制。

10. 日誌記錄

  • 日誌框架:框架如Log4j和SLF4J使用反射來獲取類的資訊,以便在日誌記錄中包含詳細的上下文資訊。

1.5 主要API

在Java中,反射主要涉及以下幾個重要的類和介面:

  1. Class類:

    • 是進行反射的核心類。每個類在Java中都有一個與之對應的Class物件。

    • 常用方法:

      • forName(String className)​:透過類的完全限定名獲取Class物件。
      • getDeclaredMethods()​:獲取所有宣告的方法,包括私有、保護和公共方法。
      • getDeclaredFields()​:獲取所有宣告的欄位。
      • getDeclaredConstructors()​:獲取所有宣告的建構函式。
      • newInstance()​:建立一個類的新例項。
  2. Method類:

    • 表示一個類的方法。

    • 常用方法:

      • invoke(Object obj, Object... args)​:呼叫此Method物件所表示的方法。
      • getName()​:獲取方法的名稱。
      • getParameterTypes()​:獲取方法的引數型別。
      • getReturnType()​:獲取方法的返回型別。
  3. Field類:

    • 表示一個類的欄位(屬性)。

    • 常用方法:

      • get(Object obj)​:獲取指定物件的欄位值。
      • set(Object obj, Object value)​:設定指定物件的欄位值。
      • getType()​:獲取欄位的型別。
  4. Constructor類:

    • 表示類的建構函式。

    • 常用方法:

      • newInstance(Object... initargs)​:建立一個新物件的例項。
      • getParameterTypes()​:獲取建構函式的引數型別。
  5. AccessibleObject類:

    • Method、Field和Constructor類都繼承自AccessibleObject,提供了控制訪問許可權的能力。

    • 常用方法:

      • setAccessible(boolean flag)​:設定物件是否可以進行訪問(即使是私有)。

二、反射工作原理

呼叫反射的總體流程如下:

1、當編寫完一個Java專案之後,每個java檔案都會被編譯成一個.class檔案。

2307111361-61df8d3a17ba4_fix7321

2、這些class檔案在程式執行時會被ClassLoader載入到JVM中,當一個類被載入以後,JVM就會在記憶體中自動產生一個Class物件。

3287271321-61df8d4274427_fix7321

3、透過Class物件獲取Field/Method/Construcor

4270769209-61df8d48e0216_fix7321

我們一般平時是透過new的形式建立物件實際上就是透過這些Class來建立的,只不過這個class檔案是編譯的時候就生成的,程式相當於寫死了給jvm去跑。

3494671770-61df8d54cc33f_fix7321

反射是什麼呢?當我們的程式在執行時,需要動態的載入一些類這些類可能之前用不到所以不用載入到jvm,而是在執行時根據需要才載入。

原來使用new的時候,需要明確的指定類名,這個時候屬於硬編碼實現,而在使用反射的時候,可以只傳入類名引數,就可以生成物件,降低了耦合性,使得程式更具靈活性。

二、反射的基本運用

反射可以用於判斷任意物件所屬的類,獲得 Class 物件,構造任意一個物件以及呼叫一個物件。

這裡介紹基本反射功能的使用和實現(反射相關的類一般都在 java.lang.relfect 包裡)。

2.1 獲取Class 物件

在Java中,獲取一個類的Class​物件是使用反射的第一步,Class​物件用於操作類的資訊。

方式

有以下集中方式可以獲取Class類的例項:

  1. 若已知具體的類,可以透過類的class屬性獲取,該方式最為安全可靠,且程式效能最高。

    //類的class屬性
    Class classOne = User.class;
    
  2. 已知某個類的例項,透過呼叫該例項的getClass方法獲取Class物件。

    //已有類物件的getClass方法
    Class collatz = user.getClass();
    
  3. 已知一個類的全類名,且該類在類路徑下,可以透過靜態方法forName()獲取。

    Class c = Class.forName("com.dcone.zhuzqc.demo.User");
    
  4. 內建基本資料型別可以直接使用類名.Type獲取。

    //內建物件才有的TYPE屬性,較大的侷限性
    Class<Integer> type = Integer.TYPE;
    
  5. 利用ClassLoader(類載入器)獲取。

    使用類載入器可以載入類並獲取 Class​ 物件,特別是在自定義類載入的場景中:

ClassLoader classLoader = MyClass.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.MyClass");

示例

image

public class ReflectionDemo {

    public static void main(String[] args) throws ClassNotFoundException {

        //獲取 Class 物件
        //方法1:使用 Class 類的 forName 靜態方法(透過類的全名(包括包名)來獲取)
        Class<?> baseAreaInfoDTOClass = Class.forName("com.gree.gpurchase.finance.infrastructure.dto.BaseAreaInfoDTO");
        System.out.println(baseAreaInfoDTOClass);

        //方法2:使用類名.class
        Class<BaseAreaInfoDTO> dtoClass = BaseAreaInfoDTO.class;
        System.out.println(dtoClass);

        //方法3:呼叫某個物件的 getClass() 方法
        BaseAreaInfoDTO baseAreaInfoDTO = new BaseAreaInfoDTO();
        Class<? extends BaseAreaInfoDTO> infoDTOClass = baseAreaInfoDTO.getClass();
        System.out.println(infoDTOClass);

        //方法4:使用 ClassLoader
        ClassLoader classLoader = BaseAreaInfoDTO.class.getClassLoader();
        Class<?> bClass = classLoader.loadClass("com.gree.gpurchase.finance.infrastructure.dto.BaseAreaInfoDTO");
        System.out.println(bClass);

        //示例:獲取基本型別的 Class 物件
        Class<?> intClass = int.class;
        Class<?> voidClass = void.class;
        System.out.println("int class物件:" + intClass);
        System.out.println("void class物件:" + voidClass);

        //示例:獲取陣列的 Class 物件
        Class<BaseAreaInfoDTO[]> dClass = BaseAreaInfoDTO[].class;
        System.out.println("陣列的class物件:" + dClass);

        int[] intArray = {5, 7};
        Class<?> intArrayClass = intArray.getClass();
        System.out.println(intArrayClass);

        String[] strArray = {"a", "b"};
        Class<?> strArrayClass = strArray.getClass();
        System.out.println(strArrayClass);
    }

}

2.2 獲取類的欄位

使用反射獲取類的成員變數(欄位)的方法主要依賴於Class​類和Field​類。下面是詳細的步驟,展示如何利用反射獲取類的成員變數。

步驟

  1. 獲取Class物件

    • 使用Class.forName()​或者MyClass.class​方式獲取目標類的Class​物件。
  2. 獲取欄位

    • 可以使用getDeclaredFields()​方法獲取所有宣告的欄位,包括私有、保護和公共欄位。
    • 如果只想獲取公共欄位,可以使用getFields()​方法。
  3. 訪問欄位

    • 透過Field​物件,可以獲取欄位的型別、名稱和訪問欄位的值或者設定欄位的值。

示例

以下示例中定義了一個簡單的類Person​,然後透過反射獲取其成員變數。

import java.lang.reflect.Field;

class Person {
    private String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class ReflectionExample {
    public static void main(String[] args) {
        try {
            // 獲取Person類的Class物件
            Class<?> personClass = Person.class;

            // 獲取所有宣告的欄位
            Field[] fields = personClass.getDeclaredFields();

            // 遍歷欄位並列印資訊
            for (Field field : fields) {
                // 獲取欄位名稱
                String fieldName = field.getName();
                // 獲取欄位型別
                Class<?> fieldType = field.getType();

                System.out.println("Field Name: " + fieldName + ", Field Type: " + fieldType.getName());

                // 如果欄位是私有的,需要設定可訪問性
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }

                // 建立例項以獲取欄位的值
                Person person = new Person("John Doe", 30);

                // 獲取欄位的值
                Object value = field.get(person);
                System.out.println("Value of " + fieldName + ": " + value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

輸出結果

執行該程式碼後,輸出如下所示:

Field Name: name, Field Type: java.lang.String
Value of name: John Doe
Field Name: age, Field Type: int
Value of age: 30

注意事項

  • 訪問控制:對於私有欄位,需要呼叫setAccessible(true)​來允許訪問。
  • 異常處理:在使用反射時,可能會遇到多種異常,例如ClassNotFoundException​、NoSuchFieldException​、IllegalAccessException​等,所以需要進行適當的異常處理。
  • 效能:過度使用反射可能會導致效能下降,通常應在必要時使用。

透過上述步驟和示例,您可以輕鬆獲取類的成員變數。使用反射時要謹慎,確保理解其影響和使用場景

2.3 獲取類的方法

使用反射機制可以動態地獲取類的資訊,包括類的方法。下面是如何使用反射獲取類的方法的步驟和示例程式碼。

步驟

  1. 獲取 Class物件:首先,需要獲取目標類的 Class​ 物件。

  2. 使用反射獲取方法

    • 使用 getMethods()​ 方法獲取所有公有的方法(包括繼承的方法)。
    • 使用 getDeclaredMethods()​ 方法獲取所有宣告的方法(包括私有方法)。
  3. 遍歷方法:遍歷獲取的方法,並列印出相關的資訊,例如方法名、返回型別和引數型別。

示例

以下是一個示例程式碼,展示瞭如何獲取某個類的方法資訊:

image

import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) {
        // 示例類
        Class<?> clazz = SampleClass.class; // 獲取SampleClass的Class物件

        // 獲取所有公有的方法
        Method[] methods = clazz.getMethods();
        System.out.println("公有方法:");
        for (Method method : methods) {
            System.out.println("方法名: " + method.getName());
            System.out.println("返回型別: " + method.getReturnType().getName());
            System.out.print("引數型別: ");
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (Class<?> paramType : parameterTypes) {
                System.out.print(paramType.getName() + " ");
            }
            System.out.println("\n");
        }

        // 獲取所有宣告的方法,包括私有方法
        Method[] declaredMethods = clazz.getDeclaredMethods();
        System.out.println("所有宣告的方法:");
        for (Method declaredMethod : declaredMethods) {
            System.out.println("方法名: " + declaredMethod.getName());
            System.out.println("返回型別: " + declaredMethod.getReturnType().getName());
            System.out.print("引數型別: ");
            Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
            for (Class<?> paramType : parameterTypes) {
                System.out.print(paramType.getName() + " ");
            }
            System.out.println("\n");
        }
    }
}

// 示例類
class SampleClass {
    public void publicMethod(String param) {
        // 方法實現
    }

    private void privateMethod() {
        // 方法實現
    }

    protected int protectedMethod(int x) {
        return x * 2;
    }

    void defaultMethod() {
        // 方法實現
    }
}

2.4 獲得類的建構函式

使用反射機制可以獲取類的建構函式。下面是獲取建構函式的步驟和示例程式碼。

步驟

  1. 獲取 Class物件:獲取目標類的 Class​ 物件。

  2. 使用反射獲取建構函式

    • 使用 getConstructors()​ 方法獲取所有公有建構函式。
    • 使用 getDeclaredConstructors()​ 方法獲取所有宣告的建構函式(包括私有建構函式)。
  3. 遍歷建構函式:遍歷獲取的建構函式,列印出建構函式的資訊,例如名稱和引數型別。

示例

image

import java.lang.reflect.Constructor;

public class ReflectionExample {
    public static void main(String[] args) {
        // 示例類
        Class<?> clazz = SampleClass.class; // 獲取SampleClass的Class物件

        // 獲取所有公有建構函式
        Constructor<?>[] constructors = clazz.getConstructors();
        System.out.println("公有建構函式:");
        for (Constructor<?> constructor : constructors) {
            System.out.println("建構函式名: " + constructor.getName());
            System.out.print("引數型別: ");
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            for (Class<?> paramType : parameterTypes) {
                System.out.print(paramType.getName() + " ");
            }
            System.out.println("\n");
        }

        // 獲取所有宣告的建構函式,包括私有建構函式
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        System.out.println("所有宣告的建構函式:");
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("建構函式名: " + declaredConstructor.getName());
            System.out.print("引數型別: ");
            Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
            for (Class<?> paramType : parameterTypes) {
                System.out.print(paramType.getName() + " ");
            }
            System.out.println("\n");
        }
    }
}

// 示例類
class SampleClass {
    public SampleClass() {
        // 預設建構函式
    }

    public SampleClass(String name) {
        // 帶引數的建構函式
    }

    private SampleClass(int age) {
        // 私有建構函式
    }
}

2.5 呼叫類的方法

使用反射機制可以動態地呼叫一個類的方法。下面是實現這一功能的步驟和示例程式碼。

步驟

  1. 獲取 Class物件:獲取目標類的 Class​ 物件。

  2. 獲取方法

    • 使用 getMethod()​ 方法獲取公有方法。
    • 使用 getDeclaredMethod()​ 方法獲取所有宣告的方法(包括私有方法)。
  3. 呼叫方法:透過 Method​ 物件呼叫目標方法。

示例

以下是一個示例程式碼,展示如何使用反射呼叫類的方法:

image

import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) {
        try {
            // 獲取SampleClass的Class物件
            Class<?> clazz = SampleClass.class;

            // 建立SampleClass的例項
            Object sampleInstance = clazz.getDeclaredConstructor().newInstance();

            // 獲取公有方法
            Method publicMethod = clazz.getMethod("publicMethod", String.class);
            publicMethod.invoke(sampleInstance, "Hello, World!");

            // 獲取私有方法
            Method privateMethod = clazz.getDeclaredMethod("privateMethod");
            privateMethod.setAccessible(true); // 允許訪問私有方法
            privateMethod.invoke(sampleInstance);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 示例類
class SampleClass {
    public void publicMethod(String message) {
        System.out.println("公有方法被呼叫,訊息: " + message);
    }

    private void privateMethod() {
        System.out.println("私有方法被呼叫");
    }
}

2.6 利用反射建立陣列

可以使用反射來動態建立陣列。透過 java.lang.reflect.Array​ 類,可以建立和運算元組。以下是建立陣列的步驟和示例程式碼。

步驟

  1. 獲取陣列的 Class物件:使用 Class.forName()​ 或 型別名.class​ 獲取陣列元素的 Class​ 物件。
  2. 使用 Array.newInstance()建立陣列:呼叫該方法並傳入陣列的 Class​ 物件和陣列的維度(長度)。

示例

以下示例展示如何使用反射建立一維和二維陣列:

image

import java.lang.reflect.Array;

public class ReflectionArrayExample {
    public static void main(String[] args) {
        try {
            // 建立一個一維陣列,型別為Integer,長度為5
            Class<?> arrayClass = Integer.class; // 陣列元素型別
            int length = 5;
            Object intArray = Array.newInstance(arrayClass, length);

            // 設定陣列元素
            for (int i = 0; i < length; i++) {
                Array.set(intArray, i, i * 10); // 設定陣列元素
            }

            // 列印陣列內容
            for (int i = 0; i < length; i++) {
                System.out.println(Array.get(intArray, i)); // 獲取陣列元素
            }

            // 建立一個二維陣列,型別為String,維度為3x2
            Class<?> strArrayClass = String.class;
            int rows = 3;
            int cols = 2;
            Object strArray = Array.newInstance(strArrayClass, rows, cols);

            // 設定二維陣列元素
            Array.set(strArray, 0, Array.newInstance(strArrayClass, cols)); // 第一行
            Array.set(Array.get(strArray, 0), 0, "Hello");
            Array.set(Array.get(strArray, 0), 1, "World");

            Array.set(strArray, 1, Array.newInstance(strArrayClass, cols)); // 第二行
            Array.set(Array.get(strArray, 1), 0, "Java");
            Array.set(Array.get(strArray, 1), 1, "Reflection");

            // 列印二維陣列內容
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    System.out.print(Array.get(Array.get(strArray, i), j) + " ");
                }
                System.out.println();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

相關文章