編程語言通常有各種不同的分類角度,動態(tài)類型和靜態(tài)類型就是其中一種分類角度轴总,簡單區(qū)分就是語言類型信息是在運行時檢查直颅,還是編譯期檢查。
與其近似的還有一個對比怀樟,就是所謂強類型和弱類型际乘,就是不同類型變量賦值時,是否需要顯式地(強制)進(jìn)行類型轉(zhuǎn)換漂佩。
那么脖含,如何分類 Java 語言呢?通常認(rèn)為投蝉, Java 是靜態(tài)的強類型語言养葵,但是因為提供了類似反射等機制,也具備了部分動態(tài)類型語言的能力瘩缆。
言歸正傳关拒,今天我要問你的問題是,談?wù)凧ava反射機制庸娱,動態(tài)代理是基于什么原理着绊?
典型回答:
反射機制是Java語言提供的一種基礎(chǔ)功能,賦予程序在運行時 自省 (introspect熟尉,官方用語)的能力归露。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義斤儿,獲取類聲明的屬性和方法剧包,調(diào)用方法或者構(gòu)造對象恐锦,甚至可以運行時修改類定義。
動態(tài)代理是一種方便運行時動態(tài)構(gòu)建代理疆液、動態(tài)處理代理方法調(diào)用的機制一铅,很多場景都是利用類似機制做到的,比如用來包裝 RPC 調(diào)用堕油、面向切面的編程( AOP )潘飘。
實現(xiàn)動態(tài)代理的方式很多,比如 JDK 自身提供的動態(tài)代理掉缺,就是主要利用了上面提到的反射機制卜录。還有其他的實現(xiàn)方式,比如利用傳說中更高性能的字節(jié)碼操作機制攀圈,類似 ASM 暴凑、 cglib (基于 ASM )、 Javassist 等赘来。
從考察知識點的角度现喳,這道題涉及的知識點比較龐雜,所以面試官能夠擴展或者深挖的內(nèi)容非常多犬辰,比如:
1.考察你對反射機制的了解和掌握程度嗦篱。
2.動態(tài)代理解決了什么問題,在你業(yè)務(wù)系統(tǒng)中的應(yīng)用場景是什么幌缝?
3.JDK動態(tài)代理在設(shè)計和實現(xiàn)上與cglib等方式有什么不同灸促,進(jìn)而如何取舍?
知識擴展
1. 反射機制及其演進(jìn)
對于 Java 語言的反射機制本身涵卵,如果你去看一下 java.lang 或 java.lang.refect 包下的相關(guān)抽象浴栽,就會有一個很直觀的印象了。 Class 轿偎、 Field 典鸡、 Method 、 Constructor 等坏晦,這些完全就是我們?nèi)ゲ僮黝惡蛯ο蟮脑獢?shù)據(jù)對應(yīng)萝玷。反射各種典型用例的編程,相信有太多文章或書籍進(jìn)行過詳細(xì)的介紹昆婿,我就不再贅述了球碉,至少你需要掌握基本場景編程,這里是官方提供的參考文檔:https://docs.oracle.com/javase/tutorial/refect/index.html 仓蛆。
關(guān)于反射睁冬,有一點我需要特意提一下,就是反射提供的 AccessibleObject.setAccessible?(boolean fag) 多律。它的子類也大都重寫了這個方法痴突,這里的所謂 accessible 可以理解成修飾成員的 public 搂蜓、 protected 狼荞、 private 辽装,這意味著我們可以在運行時修改成員訪問限制!
setAccessible 的應(yīng)用場景非常普遍相味,遍布我們的日常開發(fā)拾积、測試、依賴注入等各種框架中丰涉。比如拓巧,在 O/R Mapping 框架中,我們?yōu)橐粋€ Java 實體對象一死,運行時自動生成 setter 肛度、 getter 的邏輯,這是加載或者持久化數(shù)據(jù)非常必要的投慈,框架通吵泄ⅲ可以利用反射做這個事情,而不需要開發(fā)者手動寫類似的重復(fù)代碼伪煤。
另一個典型場景就是繞過 API 訪問控制加袋。我們?nèi)粘i_發(fā)時可能被迫要調(diào)用內(nèi)部 API 去做些事情,比如抱既,自定義的高性能 NIO 框架需要顯式地釋放 DirectBufer 职烧,使用反射繞開限制是一種常見辦法。
但是防泵,在 Java 9 以后蚀之,這個方法的使用可能會存在一些爭議,因為 Jigsaw 項目新增的模塊化系統(tǒng)捷泞,出于強封裝性的考慮足删,對反射訪問進(jìn)行了限制。 Jigsaw 引入了所謂 Open 的概念肚邢,只有當(dāng)被反射操作的模塊和指定的包對反射調(diào)用者模塊 Open 壹堰,才能使用 setAccessible ;否則骡湖,被認(rèn)為是不合法( illegal )操作贱纠。如果我們的實體類是定義在模塊里面,我們需要在模塊描述符中明確聲明:
因為反射機制使用廣泛响蕴,根據(jù)社區(qū)討論谆焊,目前, Java 9 仍然保留了兼容 Java 8 的行為浦夷,但是很有可能在未來版本辖试,完全啟用前面提到的針對 setAccessible 的限制辜王,即只有當(dāng)被反射操作的模塊和指定的包對反射調(diào)用者模塊 Open ,才能使用 setAccessible 罐孝,我們可以使用下面參數(shù)顯式設(shè)置呐馆。
2. 動態(tài)代理
前面的問題問到了動態(tài)代理,我們一起看看莲兢,它到底是解決什么問題汹来?
首先,它是一個 代理機制 改艇。如果熟悉設(shè)計模式中的代理模式收班,我們會知道,代理可以看作是對調(diào)用目標(biāo)的一個包裝谒兄,這樣我們對目標(biāo)代碼的調(diào)用不是直接發(fā)生的摔桦,而是通過代理完成。其實很多動態(tài)代理場景承疲,我認(rèn)為也可以看作是裝飾器( Decorator )模式的應(yīng)用邻耕,我會在后面的專欄設(shè)計模式主題予以補充。
通過代理可以讓調(diào)用者與實現(xiàn)者之間 解耦 纪隙。比如進(jìn)行RPC調(diào)用赊豌,框架內(nèi)部的尋址、序列化绵咱、反序列化等碘饼,對于調(diào)用者往往是沒有太大意義的,通過代理悲伶,可以提供更加友善的界面艾恼。
代理的發(fā)展經(jīng)歷了靜態(tài)到動態(tài)的過程,源于靜態(tài)代理引入的額外工作麸锉。類似早期的 RMI 之類古董技術(shù)钠绍,還需要 rmic 之類工具生成靜態(tài) stub 等各種文件,增加了很多繁瑣的準(zhǔn)備工作花沉,而這又和我們的業(yè)務(wù)邏輯沒有關(guān)系柳爽。利用動態(tài)代理機制,相應(yīng)的 stub 等類碱屁,可以在運行時生成磷脯,對應(yīng)的調(diào)用操作也是動態(tài)完成,極大地提高了我們的生產(chǎn)力娩脾。改進(jìn)后的 RMI 已經(jīng)不再需要手動去準(zhǔn)備這些了赵誓,雖然它仍然是相對古老落后的技術(shù),未來也許會逐步被移除。
這么說可能不夠直觀俩功,我們可以看 JDK 動態(tài)代理的一個簡單例子幻枉。下面只是加了一句 print ,在生產(chǎn)系統(tǒng)中诡蜓,我們可以輕松擴展類似邏輯進(jìn)行診斷熬甫、限流等。
上面的 JDK Proxy 例子万牺,非常簡單地實現(xiàn)了動態(tài)代理的構(gòu)建和代理操作罗珍。首先洽腺,實現(xiàn)對應(yīng)的 InvocationHandler 脚粟;然后,以接口 Hello 為紐帶蘸朋,為被調(diào)用目標(biāo)構(gòu)建代理對象核无,進(jìn)而應(yīng)用程序就可以使用代理對象間接運行調(diào)用目標(biāo)的邏輯,代理為應(yīng)用插入額外邏輯(這里是 println )提供了便利的入口藕坯。
從 API 設(shè)計和實現(xiàn)的角度团南,這種實現(xiàn)仍然有局限性,因為它是以接口為中心的炼彪,相當(dāng)于添加了一種對于被調(diào)用者沒有太大意義的限制吐根。我們實例化的是 Proxy 對象,而不是真正的被調(diào)用類型辐马,這在實踐中還是可能帶來各種不便和能力退化拷橘。
如果被調(diào)用者沒有實現(xiàn)接口,而我們還是希望利用動態(tài)代理機制喜爷,那么可以考慮其他方式冗疮。我們知道 Spring AOP 支持兩種模式的動態(tài)代理, JDK Proxy 或者 cglib 檩帐,如果我們選擇 cglib 方式术幔,你會發(fā)現(xiàn)對接口的依賴被克服了。
cglib動態(tài)代理采取的是創(chuàng)建目標(biāo)類的子類的方式湃密,因為是子類化诅挑,我們可以達(dá)到近似使用被調(diào)用者本身的效果。在Spring編程中泛源,框架通常會處理這種情況拔妥,當(dāng)然我們也可以 顯式指定 。關(guān)于類似方案的實現(xiàn)細(xì)節(jié)俩由,我就不再詳細(xì)討論了毒嫡。
那我們在開發(fā)中怎樣選擇呢?我來簡單對比下兩種方式各自優(yōu)勢。
JDK Proxy 的優(yōu)勢:
1.最小化依賴關(guān)系兜畸,減少依賴意味著簡化開發(fā)和維護(hù)努释,JDK本身的支持,可能比cglib更加可靠咬摇。
2.平滑進(jìn)行JDK版本升級伐蒂,而字節(jié)碼類庫通常需要進(jìn)行更新以保證在新版Java上能夠使用。
3.代碼實現(xiàn)簡單肛鹏。
基于類似 cglib 框架的優(yōu)勢:
1.有的時候調(diào)用目標(biāo)可能不便實現(xiàn)額外接口逸邦,從某種角度看,限定調(diào)用者實現(xiàn)接口是有些侵入性的實踐在扰,類似cglib動態(tài)代理就沒有這種限制缕减。
2.只操作我們關(guān)心的類,而不必為其他相關(guān)類增加工作量芒珠。
3.高性能桥狡。
另外,從性能角度皱卓,我想補充幾句裹芝。記得有人曾經(jīng)得出結(jié)論說 JDK Proxy 比 cglib 或者 Javassist 慢幾十倍。坦白說娜汁,不去爭論具體的 benchmark 細(xì)節(jié)嫂易,在主流 JDK 版本中, JDKProxy 在典型場景可以提供對等的性能水平掐禁,數(shù)量級的差距基本上不是廣泛存在的怜械。而且,反射機制性能在現(xiàn)代 JDK 中穆桂,自身已經(jīng)得到了極大的改進(jìn)和優(yōu)化宫盔,同時, JDK 很多功能也不完全是反射享完,同樣使用了 ASM 進(jìn)行字節(jié)碼操作灼芭。
我們在選型中,性能未必是唯一考量般又,可靠性彼绷、可維護(hù)性、編程工作量等往往是更主要的考慮因素茴迁,畢竟標(biāo)準(zhǔn)類庫和反射編程的門檻要低得多寄悯,代碼量也是更加可控的,如果我們比較下不同開源項目在動態(tài)代理開發(fā)上的投入堕义,也能看到這一點猜旬。
動態(tài)代理應(yīng)用非常廣泛,雖然最初多是因為 RPC 等使用進(jìn)入我們視線,但是動態(tài)代理的使用場景遠(yuǎn)遠(yuǎn)不僅如此洒擦,它完美符合 Spring AOP 等切面編程椿争。我在后面的專欄還會進(jìn)一步詳細(xì)分析 AOP 的目的和能力。簡單來說它可以看作是對 OOP 的一個補充熟嫩,因為 OOP 對于跨越不同對象或類的分散秦踪、糾纏邏輯表現(xiàn)力不夠,比如在不同模塊的特定階段做一些事情掸茅,類似日志椅邓、用戶鑒權(quán)、全局性異常處理昧狮、性能監(jiān)控景馁,甚至事務(wù)處理等,你可以參考下面這張圖陵且。
AOP 通過(動態(tài))代理機制可以讓開發(fā)者從這些繁瑣事項中抽身出來裁僧,大幅度提高了代碼的抽象程度和復(fù)用度。從邏輯上來說慕购,我們在軟件設(shè)計和實現(xiàn)中的類似代理,如 Facade 茬底、 Observer 等很多設(shè)計目的急迂,都可以通過動態(tài)代理優(yōu)雅地實現(xiàn)漩氨。