深入理解 Java 反射和動態代理

靜默虛空發表於2019-03-26

:notebook: 本文已歸檔到:「blog

:keyboard: 本文中的示例程式碼已歸檔到:「javacore

簡介

什麼是反射

反射(Reflection)是 Java 程式開發語言的特徵之一,它允許執行中的 Java 程式獲取自身的資訊,並且可以操作類或物件的內部屬性。

通過反射機制,可以在執行時訪問 Java 物件的屬性,方法,構造方法等。

反射的應用場景

反射的主要應用場景有:

  • 開發通用框架 - 反射最重要的用途就是開發各種通用框架。很多框架(比如 Spring)都是配置化的(比如通過 XML 檔案配置 JavaBean、Filter 等),為了保證框架的通用性,它們可能需要根據配置檔案載入不同的物件或類,呼叫不同的方法,這個時候就必須用到反射——執行時動態載入需要載入的物件。
  • 動態代理 - 在切面程式設計(AOP)中,需要攔截特定的方法,通常,會選擇動態代理方式。這時,就需要反射技術來實現了。
  • 註解 - 註解本身僅僅是起到標記作用,它需要利用反射機制,根據註解標記去呼叫註解直譯器,執行行為。如果沒有反射機制,註解並不比註釋更有用。
  • 可擴充套件性功能 - 應用程式可以通過使用完全限定名稱建立可擴充套件性物件例項來使用外部的使用者定義類。

反射的缺點

  • 效能開銷 - 由於反射涉及動態解析的型別,因此無法執行某些 Java 虛擬機器優化。因此,反射操作的效能要比非反射操作的效能要差,應該在效能敏感的應用程式中頻繁呼叫的程式碼段中避免。
  • 破壞封裝性 - 反射呼叫方法時可以忽略許可權檢查,因此可能會破壞封裝性而導致安全問題。
  • 內部曝光 - 由於反射允許程式碼執行在非反射程式碼中非法的操作,例如訪問私有欄位和方法,所以反射的使用可能會導致意想不到的副作用,這可能會導致程式碼功能失常並可能破壞可移植性。反射程式碼打破了抽象,因此可能會隨著平臺的升級而改變行為。

反射機制

類載入過程


深入理解 Java 反射和動態代理

類載入的完整過程如下:

(1)在編譯時,Java 編譯器編譯好 .java 檔案之後,在磁碟中產生 .class 檔案。.class 檔案是二進位制檔案,內容是隻有 JVM 能夠識別的機器碼。

(2)JVM 中的類載入器讀取位元組碼檔案,取出二進位制資料,載入到記憶體中,解析.class 檔案內的資訊。類載入器會根據類的全限定名來獲取此類的二進位制位元組流;然後,將位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;接著,在記憶體中生成代表這個類的 java.lang.Class 物件。

(3)載入結束後,JVM 開始進行連線階段(包含驗證、準備、初始化)。經過這一系列操作,類的變數會被初始化。

Class 物件

要想使用反射,首先需要獲得待操作的類所對應的 Class 物件。Java 中,無論生成某個類的多少個物件,這些物件都會對應於同一個 Class 物件。這個 Class 物件是由 JVM 生成的,通過它能夠獲悉整個類的結構。所以,java.lang.Class 可以視為所有反射 API 的入口點。

反射的本質就是:在執行時,把 Java 類中的各種成分對映成一個個的 Java 物件。

舉例來說,假如定義了以下程式碼:

User user = new User();
複製程式碼

步驟說明:

  1. JVM 載入方法的時候,遇到 new User(),JVM 會根據 User 的全限定名去載入 User.class
  2. JVM 會去本地磁碟查詢 User.class 檔案並載入 JVM 記憶體中。
  3. JVM 通過呼叫類載入器自動建立這個類對應的 Class 物件,並且儲存在 JVM 的方法區。注意:一個類有且只有一個 Class 物件

使用反射

java.lang.reflect 包

Java 中的 java.lang.reflect 包提供了反射功能。java.lang.reflect 包中的類都沒有 public 構造方法。

java.lang.reflect 包的核心介面和類如下:

  • Member 介面 - 反映關於單個成員(欄位或方法)或建構函式的標識資訊。
  • Field 類 - 提供一個類的域的資訊以及訪問類的域的介面。
  • Method 類 - 提供一個類的方法的資訊以及訪問類的方法的介面。
  • Constructor 類 - 提供一個類的建構函式的資訊以及訪問類的建構函式的介面。
  • Array 類 - 該類提供動態地生成和訪問 JAVA 陣列的方法。
  • Modifier 類 - 提供了 static 方法和常量,對類和成員訪問修飾符進行解碼。
  • Proxy 類 - 提供動態地生成代理類和類例項的靜態方法。

獲得 Class 物件

獲得 Class 的三種方法:

(1)使用 Class 類的 forName 靜態方法

示例:

package io.github.dunwu.javacore.reflect;

public class ReflectClassDemo01 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = Class.forName("io.github.dunwu.javacore.reflect.ReflectClassDemo01");
        System.out.println(c1.getCanonicalName());

        Class c2 = Class.forName("[D");
        System.out.println(c2.getCanonicalName());

        Class c3 = Class.forName("[[Ljava.lang.String;");
        System.out.println(c3.getCanonicalName());
    }
}
//Output:
//io.github.dunwu.javacore.reflect.ReflectClassDemo01
//double[]
//java.lang.String[][]
複製程式碼

使用類的完全限定名來反射物件的類。常見的應用場景為:在 JDBC 開發中常用此方法載入資料庫驅動。

(2)直接獲取某一個物件的 class

示例:

public class ReflectClassDemo02 {
    public static void main(String[] args) {
        boolean b;
        // Class c = b.getClass(); // 編譯錯誤
        Class c1 = boolean.class;
        System.out.println(c1.getCanonicalName());

        Class c2 = java.io.PrintStream.class;
        System.out.println(c2.getCanonicalName());

        Class c3 = int[][][].class;
        System.out.println(c3.getCanonicalName());
    }
}
//Output:
//boolean
//java.io.PrintStream
//int[][][]
複製程式碼

(3)呼叫 Object 的 getClass 方法,示例:

Object 類中有 getClass 方法,因為所有類都繼承 Object 類。從而呼叫 Object 類來獲取

示例:

package io.github.dunwu.javacore.reflect;

import java.util.HashSet;
import java.util.Set;

public class ReflectClassDemo03 {
    enum E {A, B}

    public static void main(String[] args) {
        Class c = "foo".getClass();
        System.out.println(c.getCanonicalName());

        Class c2 = ReflectClassDemo03.E.A.getClass();
        System.out.println(c2.getCanonicalName());

        byte[] bytes = new byte[1024];
        Class c3 = bytes.getClass();
        System.out.println(c3.getCanonicalName());

        Set<String> set = new HashSet<>();
        Class c4 = set.getClass();
        System.out.println(c4.getCanonicalName());
    }
}
//Output:
//java.lang.String
//io.github.dunwu.javacore.reflect.ReflectClassDemo.E
//byte[]
//java.util.HashSet
複製程式碼

判斷是否為某個類的例項

判斷是否為某個類的例項有兩種方式:

  1. instanceof 關鍵字
  2. Class 物件的 isInstance 方法(它是一個 Native 方法)

示例:

public class InstanceofDemo {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        if (arrayList instanceof List) {
            System.out.println("ArrayList is List");
        }
        if (List.class.isInstance(arrayList)) {
            System.out.println("ArrayList is List");
        }
    }
}
//Output:
//ArrayList is List
//ArrayList is List
複製程式碼

建立例項

通過反射來建立例項物件主要有兩種方式:

  • Class 物件的 newInstance 方法。
  • Constructor 物件的 newInstance 方法。

示例:

public class NewInstanceDemo {
    public static void main(String[] args)
        throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class<?> c1 = StringBuilder.class;
        StringBuilder sb = (StringBuilder) c1.newInstance();
        sb.append("aaa");
        System.out.println(sb.toString());

        //獲取String所對應的Class物件
        Class<?> c2 = String.class;
        //獲取String類帶一個String引數的構造器
        Constructor constructor = c2.getConstructor(String.class);
        //根據構造器建立例項
        String str2 = (String) constructor.newInstance("bbb");
        System.out.println(str2);
    }
}
//Output:
//aaa
//bbb
複製程式碼

Field

Class 物件提供以下方法獲取物件的成員(Field):

  • getFiled - 根據名稱獲取公有的(public)類成員。
  • getDeclaredField - 根據名稱獲取已宣告的類成員。但不能得到其父類的類成員。
  • getFields - 獲取所有公有的(public)類成員。
  • getDeclaredFields - 獲取所有已宣告的類成員。

示例如下:

public class ReflectFieldDemo {
    class FieldSpy<T> {
        public boolean[][] b = {{false, false}, {true, true}};
        public String name = "Alice";
        public List<Integer> list;
        public T val;
    }

    public static void main(String[] args) throws NoSuchFieldException {
        Field f1 = FieldSpy.class.getField("b");
        System.out.format("Type: %s%n", f1.getType());

        Field f2 = FieldSpy.class.getField("name");
        System.out.format("Type: %s%n", f2.getType());

        Field f3 = FieldSpy.class.getField("list");
        System.out.format("Type: %s%n", f3.getType());

        Field f4 = FieldSpy.class.getField("val");
        System.out.format("Type: %s%n", f4.getType());
    }
}
//Output:
//Type: class [[Z
//Type: class java.lang.String
//Type: interface java.util.List
//Type: class java.lang.Object
複製程式碼

Method

Class 物件提供以下方法獲取物件的方法(Method):

  • getMethod - 返回類或介面的特定方法。其中第一個引數為方法名稱,後面的引數為方法引數對應 Class 的物件。
  • getDeclaredMethod - 返回類或介面的特定宣告方法。其中第一個引數為方法名稱,後面的引數為方法引數對應 Class 的物件。
  • getMethods - 返回類或介面的所有 public 方法,包括其父類的 public 方法。
  • getDeclaredMethods - 返回類或介面宣告的所有方法,包括 public、protected、預設(包)訪問和 private 方法,但不包括繼承的方法。

獲取一個 Method 物件後,可以用 invoke 方法來呼叫這個方法。

invoke 方法的原型為:

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
複製程式碼

示例:

public class ReflectMethodDemo {
    public static void main(String[] args)
        throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        // 返回所有方法
        Method[] methods1 = System.class.getDeclaredMethods();
        System.out.println("System getDeclaredMethods 清單(數量 = " + methods1.length + "):");
        for (Method m : methods1) {
            System.out.println(m);
        }

        // 返回所有 public 方法
        Method[] methods2 = System.class.getMethods();
        System.out.println("System getMethods 清單(數量 = " + methods2.length + "):");
        for (Method m : methods2) {
            System.out.println(m);
        }

        // 利用 Method 的 invoke 方法呼叫 System.currentTimeMillis()
        Method method = System.class.getMethod("currentTimeMillis");
        System.out.println(method);
        System.out.println(method.invoke(null));
    }
}
複製程式碼

Constructor

Class 物件提供以下方法獲取物件的構造方法(Constructor):

  • getConstructor - 返回類的特定 public 構造方法。引數為方法引數對應 Class 的物件。
  • getDeclaredConstructor - 返回類的特定構造方法。引數為方法引數對應 Class 的物件。
  • getConstructors - 返回類的所有 public 構造方法。
  • getDeclaredConstructors - 返回類的所有構造方法。

獲取一個 Constructor 物件後,可以用 newInstance 方法來建立類例項。

示例:

public class ReflectMethodConstructorDemo {
    public static void main(String[] args)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<?>[] constructors1 = String.class.getDeclaredConstructors();
        System.out.println("String getDeclaredConstructors 清單(數量 = " + constructors1.length + "):");
        for (Constructor c : constructors1) {
            System.out.println(c);
        }

        Constructor<?>[] constructors2 = String.class.getConstructors();
        System.out.println("String getConstructors 清單(數量 = " + constructors2.length + "):");
        for (Constructor c : constructors2) {
            System.out.println(c);
        }

        System.out.println("====================");
        Constructor constructor = String.class.getConstructor(String.class);
        System.out.println(constructor);
        String str = (String) constructor.newInstance("bbb");
        System.out.println(str);
    }
}
複製程式碼

Array

陣列在 Java 裡是比較特殊的一種型別,它可以賦值給一個物件引用。下面我們看一看利用反射建立陣列的例子:

public class ReflectArrayDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> cls = Class.forName("java.lang.String");
        Object array = Array.newInstance(cls, 25);
        //往陣列裡新增內容
        Array.set(array, 0, "Scala");
        Array.set(array, 1, "Java");
        Array.set(array, 2, "Groovy");
        Array.set(array, 3, "Scala");
        Array.set(array, 4, "Clojure");
        //獲取某一項的內容
        System.out.println(Array.get(array, 3));
    }
}
//Output:
//Scala
複製程式碼

其中的 Array 類為 java.lang.reflect.Array 類。我們通過 Array.newInstance 建立陣列物件,它的原型是:

public static Object newInstance(Class<?> componentType, int length)
    throws NegativeArraySizeException {
    return newArray(componentType, length);
}
複製程式碼

動態代理

動態代理是反射的一個非常重要的應用場景。動態代理常被用於一些 Java 框架中。例如 Spring 的 AOP ,Dubbo 的 SPI 介面,就是基於 Java 動態代理實現的。

靜態代理

靜態代理其實就是指設計模式中的代理模式。

代理模式為其他物件提供一種代理以控制對這個物件的訪問。


深入理解 Java 反射和動態代理

Subject 定義了 RealSubject 和 Proxy 的公共介面,這樣就在任何使用 RealSubject 的地方都可以使用 Proxy 。

abstract class Subject {
    public abstract void Request();
}
複製程式碼

RealSubject 定義 Proxy 所代表的真實實體。

class RealSubject extends Subject {
    @Override
    public void Request() {
        System.out.println("真實的請求");
    }
}
複製程式碼

Proxy 儲存一個引用使得代理可以訪問實體,並提供一個與 Subject 的介面相同的介面,這樣代理就可以用來替代實體。

class Proxy extends Subject {
    private RealSubject real;

    @Override
    public void Request() {
        if (null == real) {
            real = new RealSubject();
        }
        real.Request();
    }
}
複製程式碼

說明:

靜態代理模式固然在訪問無法訪問的資源,增強現有的介面業務功能方面有很大的優點,但是大量使用這種靜態代理,會使我們系統內的類的規模增大,並且不易維護;並且由於 Proxy 和 RealSubject 的功能本質上是相同的,Proxy 只是起到了中介的作用,這種代理在系統中的存在,導致系統結構比較臃腫和鬆散。

動態代理

為了解決靜態代理的問題,就有了建立動態代理的想法:

在執行狀態中,需要代理的地方,根據 Subject 和 RealSubject,動態地建立一個 Proxy,用完之後,就會銷燬,這樣就可以避免了 Proxy 角色的 class 在系統中冗雜的問題了。


深入理解 Java 反射和動態代理

Java 動態代理基於經典代理模式,引入了一個 InvocationHandler,InvocationHandler 負責統一管理所有的方法呼叫。

動態代理步驟:

  1. 獲取 RealSubject 上的所有介面列表;
  2. 確定要生成的代理類的類名,預設為:com.sun.proxy.$ProxyXXXX
  3. 根據需要實現的介面資訊,在程式碼中動態建立 該 Proxy 類的位元組碼;
  4. 將對應的位元組碼轉換為對應的 class 物件;
  5. 建立 InvocationHandler 例項 handler,用來處理 Proxy 所有方法呼叫;
  6. Proxy 的 class 物件 以建立的 handler 物件為引數,例項化一個 proxy 物件。

從上面可以看出,JDK 動態代理的實現是基於實現介面的方式,使得 Proxy 和 RealSubject 具有相同的功能。

但其實還有一種思路:通過繼承。即:讓 Proxy 繼承 RealSubject,這樣二者同樣具有相同的功能,Proxy 還可以通過重寫 RealSubject 中的方法,來實現多型。CGLIB 就是基於這種思路設計的。

在 Java 的動態代理機制中,有兩個重要的類(介面),一個是 InvocationHandler 介面、另一個則是 Proxy 類,這一個類和一個介面是實現我們動態代理所必須用到的。

InvocationHandler 介面

InvocationHandler 介面定義:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
複製程式碼

每一個動態代理類都必須要實現 InvocationHandler 這個介面,並且每個代理類的例項都關聯到了一個 Handler,當我們通過代理物件呼叫一個方法的時候,這個方法的呼叫就會被轉發為由 InvocationHandler 這個介面的 invoke 方法來進行呼叫。

我們來看看 InvocationHandler 這個介面的唯一一個方法 invoke 方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
複製程式碼

引數說明:

  • proxy - 代理的真實物件。
  • method - 所要呼叫真實物件的某個方法的 Method 物件
  • args - 所要呼叫真實物件某個方法時接受的引數

如果不是很明白,等下通過一個例項會對這幾個引數進行更深的講解。

Proxy 類

Proxy 這個類的作用就是用來動態建立一個代理物件的類,它提供了許多的方法,但是我們用的最多的就是 newProxyInstance 這個方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,  InvocationHandler h)  throws IllegalArgumentException
複製程式碼

這個方法的作用就是得到一個動態的代理物件。

引數說明:

  • loader - 一個 ClassLoader 物件,定義了由哪個 ClassLoader 物件來對生成的代理物件進行載入。
  • interfaces - 一個 Interface 物件的陣列,表示的是我將要給我需要代理的物件提供一組什麼介面,如果我提供了一組介面給它,那麼這個代理物件就宣稱實現了該介面(多型),這樣我就能呼叫這組介面中的方法了
  • h - 一個 InvocationHandler 物件,表示的是當我這個動態代理物件在呼叫方法的時候,會關聯到哪一個 InvocationHandler 物件上

動態代理例項

上面的內容介紹完這兩個介面(類)以後,我們來通過一個例項來看看我們的動態代理模式是什麼樣的:

首先我們定義了一個 Subject 型別的介面,為其宣告瞭兩個方法:

public interface Subject {

    void hello(String str);

    String bye();
}
複製程式碼

接著,定義了一個類來實現這個介面,這個類就是我們的真實物件,RealSubject 類:

public class RealSubject implements Subject {

    @Override
    public void hello(String str) {
        System.out.println("Hello  " + str);
    }

    @Override
    public String bye() {
        System.out.println("Goodbye");
        return "Over";
    }
}
複製程式碼

下一步,我們就要定義一個動態代理類了,前面說個,每一個動態代理類都必須要實現 InvocationHandler 這個介面,因此我們這個動態代理類也不例外:

public class InvocationHandlerDemo implements InvocationHandler {
    // 這個就是我們要代理的真實物件
    private Object subject;

    // 構造方法,給我們要代理的真實物件賦初值
    public InvocationHandlerDemo(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object object, Method method, Object[] args)
        throws Throwable {
        // 在代理真實物件前我們可以新增一些自己的操作
        System.out.println("Before method");

        System.out.println("Call Method: " + method);

        // 當代理物件呼叫真實物件的方法時,其會自動的跳轉到代理物件關聯的handler物件的invoke方法來進行呼叫
        Object obj = method.invoke(subject, args);

        // 在代理真實物件後我們也可以新增一些自己的操作
        System.out.println("After method");
        System.out.println();

        return obj;
    }
}
複製程式碼

最後,來看看我們的 Client 類:

public class Client {
    public static void main(String[] args) {
        // 我們要代理的真實物件
        Subject realSubject = new RealSubject();

        // 我們要代理哪個真實物件,就將該物件傳進去,最後是通過該真實物件來呼叫其方法的
        InvocationHandler handler = new InvocationHandlerDemo(realSubject);

        /*
         * 通過Proxy的newProxyInstance方法來建立我們的代理物件,我們來看看其三個引數
         * 第一個引數 handler.getClass().getClassLoader() ,我們這裡使用handler這個類的ClassLoader物件來載入我們的代理物件
         * 第二個引數realSubject.getClass().getInterfaces(),我們這裡為代理物件提供的介面是真實物件所實行的介面,表示我要代理的是該真實物件,這樣我就能呼叫這組介面中的方法了
         * 第三個引數handler, 我們這裡將這個代理物件關聯到了上方的 InvocationHandler 這個物件上
         */
        Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject
                .getClass().getInterfaces(), handler);

        System.out.println(subject.getClass().getName());
        subject.hello("World");
        String result = subject.bye();
        System.out.println("Result is: " + result);
    }
}
複製程式碼

我們先來看看控制檯的輸出:

com.sun.proxy.$Proxy0
Before method
Call Method: public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String)
Hello  World
After method

Before method
Call Method: public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye()
Goodbye
After method

Result is: Over
複製程式碼

我們首先來看看 com.sun.proxy.$Proxy0 這東西,我們看到,這個東西是由 System.out.println(subject.getClass().getName()); 這條語句列印出來的,那麼為什麼我們返回的這個代理物件的類名是這樣的呢?

Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject
                .getClass().getInterfaces(), handler);
複製程式碼

可能我以為返回的這個代理物件會是 Subject 型別的物件,或者是 InvocationHandler 的物件,結果卻不是,首先我們解釋一下為什麼我們這裡可以將其轉化為 Subject 型別的物件?

原因就是:在 newProxyInstance 這個方法的第二個引數上,我們給這個代理物件提供了一組什麼介面,那麼我這個代理物件就會實現了這組介面,這個時候我們當然可以將這個代理物件強制型別轉化為這組介面中的任意一個,因為這裡的介面是 Subject 型別,所以就可以將其轉化為 Subject 型別了。

同時我們一定要記住,通過 Proxy.newProxyInstance 建立的代理物件是在 jvm 執行時動態生成的一個物件,它並不是我們的 InvocationHandler 型別,也不是我們定義的那組介面的型別,而是在執行是動態生成的一個物件,並且命名方式都是這樣的形式,以$開頭,proxy 為中,最後一個數字表示物件的標號

接著我們來看看這兩句

subject.hello("World");
String result = subject.bye();
複製程式碼

這裡是通過代理物件來呼叫實現的那種介面中的方法,這個時候程式就會跳轉到由這個代理物件關聯到的 handler 中的 invoke 方法去執行,而我們的這個 handler 物件又接受了一個 RealSubject 型別的引數,表示我要代理的就是這個真實物件,所以此時就會呼叫 handler 中的 invoke 方法去執行。

我們看到,在真正通過代理物件來呼叫真實物件的方法的時候,我們可以在該方法前後新增自己的一些操作,同時我們看到我們的這個 method 物件是這樣的:

public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String)
public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye()
複製程式碼

正好就是我們的 Subject 介面中的兩個方法,這也就證明了當我通過代理物件來呼叫方法的時候,起實際就是委託由其關聯到的 handler 物件的 invoke 方法中來呼叫,並不是自己來真實呼叫,而是通過代理的方式來呼叫的。

小結

反射應用


深入理解 Java 反射和動態代理


深入理解 Java 反射和動態代理

參考資料

相關文章