編程語言通常有各種不同的分類角度,動態類型和靜態類型就是其中一種分類角度,簡單區分就是語言類型信息是在運行時檢查,還是編譯期檢查。
與其近似的還有一個對比,就是所謂強類型和弱類型,就是不同類型變量賦值時,是否需要顯式地(強制)進行類型轉換。
那么,如何分類 Java 語言呢?通常認為,Java 是靜態的強類型語言,但是因為提供了類似反射等機制,也具備了部分動態類型語言的能力。
言歸正傳,今天我要問你的問題是,談談 Java 反射機制,動態代理是基于什么原理?
## 典型回答
反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至可以運行時修改類定義。
動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 調用、面向切面的編程(AOP)。
實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其他的實現方式,比如利用傳說中更高性能的字節碼操作機制,類似 ASM、cglib(基于 ASM)、Javassist 等。
## 考點分析
這個題目給我的第一印象是稍微有點誘導的嫌疑,可能會下意識地以為動態代理就是利用反射機制實現的,這么說也不算錯但稍微有些不全面。功能才是目的,實現的方法有很多。總的來說,這道題目考察的是 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 語言演進中正在發生的變化,并且進一步探討了動態代理機制和相關的切面編程,分析了其解決的問題,并探討了生產實踐中的選擇考量。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?留一道思考題給你,你在工作中哪些場景使用到了動態代理?相應選擇了什么實現技術?選擇的依據是什么?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?