1. 程式人生 > >JAVA語言核心精講5-反射機制與動態代理原理

JAVA語言核心精講5-反射機制與動態代理原理

談談 Java 反射機制,動態代理是基於什麼原理?

典型回答

反射機制是 Java 語言提供的一種基礎功能,賦予程式在執行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者物件,比如獲取某個物件的類定義,獲取類宣告的屬性和方法,呼叫方法或者構造物件,甚至可以執行時修改類定義。

動態代理是一種方便執行時動態構建代理、動態處理代理方法呼叫的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 呼叫、面向切面的程式設計(AOP)。

實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其他的實現方式,比如利用傳說中更高效能的位元組碼操作機制,類似 ASM、cglib(基於 ASM)、Javassist 等。

詳細答案:

1 關於反射
反射最大的作用之一就在於我們可以不在編譯時知道某個物件的型別,而在執行時通過提供完整的”包名+類名.class”得到。注意:不是在編譯時,而是在執行時。

功能:
•在執行時能判斷任意一個物件所屬的類。
•在執行時能構造任意一個類的物件。
•在執行時判斷任意一個類所具有的成員變數和方法。
•在執行時呼叫任意一個物件的方法。
說大白話就是,利用Java反射機制我們可以載入一個執行時才得知名稱的class,獲悉其構造方法,並生成其物件實體,能對其fields設值並喚起其methods。

應用場景:
反射技術常用在各類通用框架開發中。因為為了保證框架的通用性,需要根據配置檔案載入不同的物件或類,並呼叫不同的方法,這個時候就會用到反射——執行時動態載入需要載入的物件。



特點:
由於反射會額外消耗一定的系統資源,因此如果不需要動態地建立一個物件,那麼就不需要用反射。另外,反射呼叫方法時可以忽略許可權檢查,因此可能會破壞封裝性而導致安全問題。

2 動態代理
為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個物件不適合或者不能直接引用另一個物件,而代理物件可以在兩者之間起到中介的作用(可類比房屋中介,房東委託中介銷售房屋、簽訂合同等)。
所謂動態代理,就是實現階段不用關心代理誰,而是在執行階段才指定代理哪個一個物件(不確定性)。如果是自己寫代理類的方式就是靜態代理(確定性)。

組成要素:
(動態)代理模式主要涉及三個要素:
其一:抽象類介面
其二:被代理類(具體實現抽象介面的類)

其三:動態代理類:實際呼叫被代理類的方法和屬性的類

實現方式:
實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了反射機制。還有其他的實現方式,比如利用位元組碼操作機制,類似 ASM、CGLIB(基於 ASM)、Javassist 等。
舉例,常可採用的JDK提供的動態代理介面InvocationHandler來實現動態代理類。其中invoke方法是該介面定義必須實現的,它完成對真實方法的呼叫。通過InvocationHandler介面,所有方法都由該Handler來進行處理,即所有被代理的方法都由InvocationHandler接管實際的處理任務。此外,我們常可以在invoke方法實現中增加自定義的邏輯實現,實現對被代理類的業務邏輯無侵入。

考點分析

這個題目給我的第一印象是稍微有點誘導的嫌疑,可能會下意識地以為動態代理就是利用反射機制實現的,這麼說也不算錯但稍微有些不全面。功能才是目的,實現的方法有很多。總的來說,這道題目考察的是 Java 語言的另外一種基礎機制: 反射,它就像是一種魔法,引入執行時自省能力,賦予了 Java 語言令人意外的活力,通過執行時操作元資料或物件,Java 可以靈活地操作執行時才能確定的資訊。而動態代理,則是延伸出來的一種廣泛應用於產品開發中的技術,很多繁瑣的重複程式設計,都可以被動態代理機制優雅地解決。

從考察知識點的角度,這道題涉及的知識點比較龐雜,所以面試官能夠擴充套件或者深挖的內容非常多,比如:

  • 考察你對反射機制的瞭解和掌握程度。

  • 動態代理解決了什麼問題,在你業務系統中的應用場景是什麼?

  • JDK 動態代理在設計和實現上與 cglib 等方式有什麼不同,進而如何取捨?

這些考點似乎不是短短一篇文章能夠囊括的,我會在知識擴充套件部分儘量梳理一下。

知識擴充套件

1. 反射機制及其演進

對於 Java 語言的反射機制本身,如果你去看一下 java.lang 或 java.lang.reflect 包下的相關抽象,就會有一個很直觀的印象了。Class、Field、Method、Constructor 等,這些完全就是我們去操作類和物件的元資料對應。反射各種典型用例的程式設計,相信有太多文章或書籍進行過詳細的介紹,我就不再贅述了,至少你需要掌握基本場景程式設計,這裡是官方提供的參考文件:https://docs.oracle.com/javase/tutorial/reflect/index.html 。

關於反射,有一點我需要特意提一下,就是反射提供的 AccessibleObject.setAccessible​(boolean flag)。它的子類也大都重寫了這個方法,這裡的所謂 accessible 可以理解成修飾成員的 public、protected、private,這意味著我們可以在執行時修改成員訪問限制!

setAccessible 的應用場景非常普遍,遍佈我們的日常開發、測試、依賴注入等各種框架中。比如,在 O/R Mapping 框架中,我們為一個 Java 實體物件,執行時自動生成 setter、getter 的邏輯,這是載入或者持久化資料非常必要的,框架通常可以利用反射做這個事情,而不需要開發者手動寫類似的重複程式碼。

另一個典型場景就是繞過 API 訪問控制。我們日常開發時可能被迫要呼叫內部 API 去做些事情,比如,自定義的高效能 NIO 框架需要顯式地釋放 DirectBuffer,使用反射繞開限制是一種常見辦法。

但是,在 Java 9 以後,這個方法的使用可能會存在一些爭議,因為 Jigsaw 專案新增的模組化系統,出於強封裝性的考慮,對反射訪問進行了限制。Jigsaw 引入了所謂 Open 的概念,只有當被反射操作的模組和指定的包對反射呼叫者模組 Open,才能使用 setAccessible;否則,被認為是不合法(illegal)操作。如果我們的實體類是定義在模組裡面,我們需要在模組描述符中明確宣告:

module MyEntities {
    // Open for reflection
    opens com.mycorp to java.persistence;
}

因為反射機制使用廣泛,根據社群討論,目前,Java 9 仍然保留了相容 Java 8 的行為,但是很有可能在未來版本,完全啟用前面提到的針對 setAccessible 的限制,即只有當被反射操作的模組和指定的包對反射呼叫者模組 Open,才能使用 setAccessible,我們可以使用下面引數顯式設定。

--illegal-access={ permit | warn | deny }

2. 動態代理

前面的問題問到了動態代理,我們一起看看,它到底是解決什麼問題?

首先,它是一個代理機制。如果熟悉設計模式中的代理模式,我們會知道,代理可以看作是對呼叫目標的一個包裝,這樣我們對目的碼的呼叫不是直接發生的,而是通過代理完成。其實很多動態代理場景,我認為也可以看作是裝飾器(Decorator)模式的應用,我會在後面的專欄設計模式主題予以補充。

通過代理可以讓呼叫者與實現者之間解耦。比如進行 RPC 呼叫,框架內部的定址、序列化、反序列化等,對於呼叫者往往是沒有太大意義的,通過代理,可以提供更加友善的介面。

代理的發展經歷了靜態到動態的過程,源於靜態代理引入的額外工作。類似早期的 RMI 之類古董技術,還需要 rmic 之類工具生成靜態 stub 等各種檔案,增加了很多繁瑣的準備工作,而這又和我們的業務邏輯沒有關係。利用動態代理機制,相應的 stub 等類,可以在執行時生成,對應的呼叫操作也是動態完成,極大地提高了我們的生產力。改進後的 RMI 已經不再需要手動去準備這些了,雖然它仍然是相對古老落後的技術,未來也許會逐步被移除。

這麼說可能不夠直觀,我們可以看 JDK 動態代理的一個簡單例子。下面只是加了一句 print,在生產系統中,我們可以輕鬆擴充套件類似邏輯進行診斷、限流等。

public class MyDynamicProxy {
    public static  void main (String[] args) {
        HelloImpl hello = new HelloImpl();
        MyInvocationHandler handler = new MyInvocationHandler(hello);
        // 構造程式碼例項
        Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
        // 呼叫代理方法
        proxyHello.sayHello();
    }
}
interface Hello {
    void sayHello();
}
class HelloImpl implements  Hello {
    @Override
    public void sayHello() {
        System.out.println("Hello World");
    }
}
 class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println("Invoking sayHello");
        Object result = method.invoke(target, args);
        return result;
    }
}

上面的 JDK Proxy 例子,非常簡單地實現了動態代理的構建和代理操作。首先,實現對應的 InvocationHandler;然後,以介面 Hello 為紐帶,為被呼叫目標構建代理物件,進而應用程式就可以使用代理物件間接執行呼叫目標的邏輯,代理為應用插入額外邏輯(這裡是 println)提供了便利的入口。

從 API 設計和實現的角度,這種實現仍然有侷限性,因為它是以介面為中心的,相當於添加了一種對於被呼叫者沒有太大意義的限制。我們例項化的是 Proxy 物件,而不是真正的被呼叫型別,這在實踐中還是可能帶來各種不便和能力退化。

如果被呼叫者沒有實現介面,而我們還是希望利用動態代理機制,那麼可以考慮其他方式。我們知道 Spring AOP 支援兩種模式的動態代理,JDK Proxy 或者 cglib,如果我們選擇 cglib 方式,你會發現對介面的依賴被克服了。

cglib 動態代理採取的是建立目標類的子類的方式,因為是子類化,我們可以達到近似使用被呼叫者本身的效果。在 Spring 程式設計中,框架通常會處理這種情況,當然我們也可以顯式指定。關於類似方案的實現細節,我就不再詳細討論了。

那我們在開發中怎樣選擇呢?我來簡單對比下兩種方式各自優勢。

JDK Proxy 的優勢:

  • 最小化依賴關係,減少依賴意味著簡化開發和維護,JDK 本身的支援,可能比 cglib 更加可靠。

  • 平滑進行 JDK 版本升級,而位元組碼類庫通常需要進行更新以保證在新版 Java 上能夠使用。

  • 程式碼實現簡單。

基於類似 cglib 框架的優勢:

  • 有的時候呼叫目標可能不便實現額外介面,從某種角度看,限定呼叫者實現介面是有些侵入性的實踐,類似 cglib 動態代理就沒有這種限制。

  • 只操作我們關心的類,而不必為其他相關類增加工作量。

  • 高效能。

另外,從效能角度,我想補充幾句。記得有人曾經得出結論說 JDK Proxy 比 cglib 或者 Javassist 慢幾十倍。坦白說,不去爭論具體的 benchmark 細節,在主流 JDK 版本中,JDK Proxy 在典型場景可以提供對等的效能水平,數量級的差距基本上不是廣泛存在的。而且,反射機制效能在現代 JDK 中,自身已經得到了極大的改進和優化,同時,JDK 很多功能也不完全是反射,同樣使用了 ASM 進行位元組碼操作。

我們在選型中,效能未必是唯一考量,可靠性、可維護性、程式設計工作量等往往是更主要的考慮因素,畢竟標準類庫和反射程式設計的門檻要低得多,程式碼量也是更加可控的,如果我們比較下不同開源專案在動態代理開發上的投入,也能看到這一點。

動態代理應用非常廣泛,雖然最初多是因為 RPC 等使用進入我們視線,但是動態代理的使用場景遠遠不僅如此,它完美符合 Spring AOP 等切面程式設計。我在後面的專欄還會進一步詳細分析 AOP 的目的和能力。簡單來說它可以看作是對 OOP 的一個補充,因為 OOP 對於跨越不同物件或類的分散、糾纏邏輯表現力不夠,比如在不同模組的特定階段做一些事情,類似日誌、使用者鑑權、全域性性異常處理、效能監控,甚至事務處理等,你可以參考下面這張圖。



AOP 通過(動態)代理機制可以讓開發者從這些繁瑣事項中抽身出來,大幅度提高了程式碼的抽象程度和複用度。從邏輯上來說,我們在軟體設計和實現中的類似代理,如 Facade、Observer 等很多設計目的,都可以通過動態代理優雅地實現。

今天我簡要回顧了反射機制,談了反射在 Java 語言演進中正在發生的變化,並且進一步探討了動態代理機制和相關的切面程式設計,分析了其解決的問題,並探討了生產實踐中的選擇考量。