封面 :洛小汐
做者 :潘潘html
事實上,對於不少Java編程人員來講,可能只須要達到從入門到上手的編程水準,就能很好的完成大部分研發工做。除非本身強主動獲取,或者工做倒逼你學習,不然咱們好像不必去真正瞭解Java編程,或者深刻研究JDK運行原理、或者在實際工做中某個模塊寫一套設計模式、或者糾結一個線程安全問題。java
我以爲徹底不必瞭解,由於不少知識內容,我技術儲備上僅僅點到爲止,就能勝任工做,何須深刻?確實,我也和有些朋友同樣,8年編程生涯以來大部分時候都存在這種思想,直到某一天忽然有機會來了,你就要負責某個系統的架構規劃設計、你就要全權保障整個企業的信息化安全、你就要管理底下幾十上百號研發兄弟,但你知道你只擅長if/else,你清楚的知道你寫過一個下單方法有9000行代碼,你剛剛接到一個運維同事的電話,說你某個SQL執行後CPU飆到100%...git
對,這就是你我如今或將來,都終將會面臨同時躲不過的問題,因此我想尋思着要不就來一個 「一文讀懂」 系列,咱們深刻淺出,跟你們一塊研究學習,從Java基礎到核心,從單體框架到微服務集羣,從數據庫到服務器,咱們都一塊兒分享,同步成長,Java之路,應不忘初心,終生學習。github
因而,有了咱們第一話 「Java動態代理」 。數據庫
最先的代理模式,咱們大體能夠聯想到三國時期,孟德君挾天子以令諸侯是代理模式,是權利代理;現此生活中相似房產中介、票務中介是代理模式,是業務代理;還有***瀏覽網頁是代理模式,是***代理;回到咱們編程世界裏呢,之前你用的遠程方法調用(RMI)是代理、企業JavaBeans(EJB)是代理,如今流行的衆多RPC框架(如dubbo)也是代理,包括咱們的Java動態代理,他們都是對象代理。編程
Java 動態代理機制的出現,使得 Java 開發人員不用手工編寫代理類,只要簡單地指定一組接口及委託類對象,便能動態地得到代理類。代理類會負責將全部的方法調用分派到委託對象上反射執行,在分派執行的過程當中,開發人員還能夠按需調整委託類對象及其功能,這是一套很是靈活有彈性的代理框架。設計模式
代理是一種常見的設計模式,其目的就是爲 」調用方「 提供一個代理類以控制對 」被調用方「 的訪問。代理類負責爲委託類預處理消息,過濾消息並轉發消息,以及進行消息被委託類執行後的後續處理。數組
能夠發現,代理類與委託類,實現了同一個接口,因此對於客戶端請求來講沒有絲毫的區別,這也是Java面向接口編程的特色。代理模式使用代理對象(代理類)完成用戶請求,有效的屏蔽了用戶對真實對象(委託類)的訪問,也能夠很好地隱藏和保護委託類對象。同時也爲添加不一樣控制策略爭取了空間,從而在設計上得到了更大的靈活性。Java 動態代理機制以巧妙的方式近乎完美地實踐了代理模式的設計理念。緩存
正如現實世界的代理人被受權執行當事人的一些事宜,無需當事人出面,從第三方的角度看,彷佛當事人並不存在,由於他只和代理人通訊。而事實上代理人是要有當事人的受權,而且在覈心問題上還須要請示當事人,同時代理人除了轉發完成當事人的受權事宜以外,還能夠在執行受權事宜先後增長額外的一些事務。安全
代理對象 = 目標對象 + 加強事務
要了解 Java 動態代理的機制,首先須要瞭解兩個相關的類或接口:
// 方法 1: 該方法用於獲取指定代理對象所關聯的調用處理器 static InvocationHandler getInvocationHandler(Object proxy) // 方法 2:該方法用於獲取關聯於指定類裝載器和一組接口的動態代理類的類對象 static Class getProxyClass( ClassLoader loader, Class[] interfaces) // 方法 3:該方法用於判斷指定類對象是不是一個動態代理類 static boolean isProxyClass(Class cl) // 方法 4:該方法用於爲指定類裝載器、一組接口及調用處理器生成動態代理類實例 static Object newProxyInstance( ClassLoader loader, Class[] interfaces, InvocationHandler h)
// 該方法負責集中處理動態代理類上的全部方法調用。 // 第一個參數既是代理類實例, // 第二個參數是被調用的方法對象 // 第三個方法是調用參數。 // 調用處理器根據這三個參數 // 進行預處理或分派到委託類實例上反射執行 Object invoke(Object proxy, Method method, Object[] args)
每次生成動態代理類對象時都須要指定一個實現了該接口的調用處理器對象(參見 Proxy 靜態方法 4 的第三個參數)。
每次生成動態代理類對象時都須要指定一個類裝載器對象(參見 Proxy 靜態方法 4 的第一個參數)
首先讓咱們來了解一下如何使用 Java 動態代理。具體有以下四步驟:
// InvocationHandlerImpl 實現了 InvocationHandler 接口, // 並能實現方法調用從代理類到委託類的分派轉發 // 其內部一般包含指向委託類實例的引用, // 用於真正執行分派轉發過來的方法調用 InvocationHandler handler = new InvocationHandlerImpl(..); // 經過 Proxy 爲包括 Interface 接口在內的一組接口 // 動態建立代理類的類對象 Class clazz = Proxy.getProxyClass( classLoader, new Class[] { Interface.class, ... }); // 經過反射從生成的類對象得到構造函數對象 Constructor constructor = clazz.getConstructor( new Class[] { InvocationHandler.class }); // 經過構造函數對象建立動態代理類實例 Interface Proxy = (Interface)constructor.newInstance( new Object[] { handler });
實際使用過程更加簡單,由於 Proxy 的靜態方法 newProxyInstance 已經爲咱們封裝了步驟 2 到步驟 4 的過程,因此簡化後的過程以下
// InvocationHandlerImpl 實現了 InvocationHandler 接口, // 並能實現方法調用從代理類到委託類的分派轉發 InvocationHandler handler = new InvocationHandlerImpl(..); // 經過 Proxy 直接建立動態代理類實例 Interface proxy = (Interface)Proxy.newProxyInstance( classLoader, new Class[] { Interface.class }, handler );
接下來讓咱們來了解一下 Java 動態代理機制的一些特色。
首先是動態生成的代理類自己的一些特色。
由圖可見,Proxy 類是它的父類,這個規則適用於全部由 Proxy 建立的動態代理類。並且該類還實現了其所代理的一組接口,這就是爲何它可以被安全地類型轉換到其所代理的某接口的根本緣由。
接下來讓咱們瞭解一下代理類實例的一些特色。每一個實例都會關聯一個調用處理器對象,能夠經過 Proxy 提供的靜態方法 getInvocationHandler 去得到代理類實例的調用處理器對象。在代理類實例上調用其代理的接口中所聲明的方法時,這些方法最終都會由調用處理器的 invoke 方法執行,此外,值得注意的是,代理類的根類 java.lang.Object 中有三個方法也一樣會被分派到調用處理器的 invoke 方法執行,它們是 hashCode,equals 和 toString,可能的緣由有:
首先,要注意不能有重複的接口,以免動態代理類代碼生成時的編譯錯誤。其次,這些接口對於類裝載器必須可見,不然類裝載器將沒法連接它們,將會致使類定義失敗。再次,需被代理的全部非 public 的接口必須在同一個包中,不然代理類生成也會失敗。最後,接口的數目不能超過 65535,這是 JVM 設定的限制。
/** * Proxy.java * Generate a proxy class. Must call the checkProxyAccess method * to perform permission checks before calling this. */ private static Class<?> getProxyClass0( ClassLoader loader,Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } // If the proxy class defined by the given loader implementing // the given interfaces exists, this will simply return the cached copy; // otherwise, it will create the proxy class via the ProxyClassFactory return proxyClassCache.get(loader, interfaces); }
最後再來了解一下異常處理方面的特色。從調用處理器接口聲明的方法中能夠看到理論上它可以拋出任何類型的異常,由於全部的異常都繼承於 Throwable 接口,但事實是否如此呢?答案是否認的,緣由是咱們必須遵照一個繼承原則:即子類覆蓋父類或實現父接口的方法時,拋出的異常必須在原方法支持的異常列表以內。因此雖然調用處理器理論上講可以,但實際上每每受限制,除非父接口中的方法支持拋 Throwable 異常。那麼若是在 invoke 方法中的確產生了接口方法聲明中不支持的異常,那將如何呢?放心,Java 動態代理類已經爲咱們設計好了解決方法:它將會拋出 UndeclaredThrowableException 異常。這個異常是一個 RuntimeException 類型,因此不會引發編譯錯誤。經過該異常的 getCause 方法,還能夠得到原來那個不受支持的異常對象,以便於錯誤診斷。
機制和特色都介紹過了,接下來讓咱們經過源代碼來了解一下 Proxy 究竟是如何實現的。
首先記住 Proxy 的幾個重要的靜態變量:
// 映射表:用於維護類裝載器對象到其對應的代理類緩存 private static Map loaderToCache = new WeakHashMap(); // 標記:用於標記一個動態代理類正在被建立中 private static Object pendingGenerationMarker = new Object(); // 同步表:記錄已經被建立的動態代理類類型,主要被方法 isProxyClass 進行相關的判斷 private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); // 關聯的調用處理器引用 protected InvocationHandler h;
而後,來看一下 Proxy 的構造方法:
// 因爲 Proxy 內部從不直接調用構造函數, // 因此 private 類型意味着禁止任何調用 private Proxy() {} // 因爲 Proxy 內部從不直接調用構造函數, // 因此 protected 意味着只有子類能夠調用 protected Proxy(InvocationHandler h) {this.h = h;}
接着,能夠快速瀏覽一下 newProxyInstance 方法,由於其至關簡單:
/** * 將方法調用分派到指定調用處理器 * 並返回指定接口的代理類實例 */ public static Object newProxyInstance( ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { // 檢查 h 不爲空,不然拋異常 if (h == null) { throw new NullPointerException(); } // 得到與制定類裝載器和一組接口相關的代理類類型對象 Class cl = getProxyClass(loader, interfaces); // 經過反射獲取構造函數對象並生成代理類實例 try { Constructor cons = cl.getConstructor(constructorParams); return (Object) cons.newInstance(new Object[]{h}); } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); } catch (IllegalAccessException e) { throw new InternalError(e.toString()); } catch (InstantiationException e) { throw new InternalError(e.toString()); } catch (InvocationTargetException e) { throw new InternalError(e.toString()); } }
因而可知,動態代理真正的關鍵是在 getProxyClass 方法,該方法負責爲一組接口動態地生成代理類類型對象。在該方法內部,您將能看到 Proxy 內的各路英雄(靜態變量)悉數登場。有點火燒眉毛了麼?那就讓咱們一塊兒走進 Proxy 最最神祕的殿堂去欣賞一番吧。該方法總共能夠分爲四個步驟:
第 1 步,對這組接口進行必定程度的安全檢查,包括檢查接口類對象是否對類裝載器可見而且與類裝載器所能識別的接口類對象是徹底相同的,還會檢查確保是 interface 類型而不是 class 類型。這個步驟經過一個循環來完成,檢查經過後將會獲得一個包含全部接口名稱的字符串數組,記爲 String[] interfaceNames
。整體上這部分實現比較直觀,因此略去大部分代碼,僅保留如何判斷某類或接口是否對特定類裝載器可見的相關代碼。
try { // 指定接口名字、類裝載器對象, // 同時制定 initializeBoolean // 爲 false 表示無須初始化類 // // 若是方法返回正常這表示可見, // 不然會拋出 ClassNotFoundException // 異常表示不可見 interfaceClass = Class.forName(interfaceName, false, loader); } catch (ClassNotFoundException e) { }
第 2 步,從 loaderToCache 映射表中獲取以類裝載器對象爲關鍵字所對應的緩存表,若是不存在就建立一個新的緩存表並更新到 loaderToCache。緩存表是一個 HashMap 實例,正常狀況下它將存放鍵值對(接口名字列表,動態生成的代理類的類對象引用)。當代理類正在被建立時它會臨時保存(接口名字列表,pendingGenerationMarker)。標記 pendingGenerationMarke 的做用是通知後續的同類請求(接口數組相同且組內接口排列順序也相同)代理類正在被建立,請保持等待直至建立完成。
do { // 以接口名字列表做爲關鍵字得到對應 cache 值 Object value = cache.get(key); if (value instanceof Reference) { proxyClass = (Class) ((Reference) value).get(); } if (proxyClass != null) { // 若是已經建立,直接返回 return proxyClass; } else if (value == pendingGenerationMarker) { // 代理類正在被建立,保持等待 try { cache.wait(); } catch (InterruptedException e) { } // 等待被喚醒後, // 再次循環檢查, // 以確保建立完成, // 不然從新等待 continue; } else { // 標記代理類正在被建立 cache.put(key, pendingGenerationMarker); // break 跳出循環已進入建立過程 break; } while (true);
第 3 步,動態建立代理類的類對象。首先是肯定代理類所在的包,其原則如前所述,若是都爲 public 接口,則包名爲空字符串表示頂層包;若是全部非 public 接口都在同一個包,則包名與這些接口的包名相同;若是有多個非 public 接口且不一樣包,則拋異常終止代理類的生成。肯定了包後,就開始生成代理類的類名,一樣如前所述按格式」 $ProxyN 」生成。類名也肯定了,接下來就是見證奇蹟的發生 —— 動態生成代理類:
// 動態地生成代理類的字節碼數組 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces); try { // 動態地定義新生成的代理類 proxyClass = defineClass0( loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); } // 把生成的代理類的類對象 // 記錄進 proxyClasses 表 proxyClasses.put(proxyClass, null);
因而可知,全部的代碼生成的工做都由神祕的 ProxyGenerator 所完成了,當你嘗試去探索這個類時,你所能得到的信息僅僅是它位於並未公開的 sun.misc 包,有若干常量、變量和方法以完成這個神奇的代碼生成的過程,可是 sun 並無提供源代碼以供研讀。至於動態類的定義,則由 Proxy 的 native 靜態方法 defineClass0 執行。
第 4 步,代碼生成過程進入結尾部分,根據結果更新緩存表,若是成功則將代理類的類對象引用更新進緩存表,不然清除緩存表中對應關鍵值,最後喚醒全部可能的正在等待的線程。
走完了以上四個步驟後,至此,全部的代理類生成細節都已介紹完畢,剩下的靜態方法如 getInvocationHandler 和 isProxyClass 就顯得如此的直觀,只需經過查詢相關變量就能夠完成,因此對其的代碼分析就省略了。
分析了 Proxy 類的源代碼,相信在讀者的腦海中會對 Java 動態代理機制造成一個更加清晰的理解,可是,當探索之旅在 sun.misc.ProxyGenerator 類處嘎然而止,全部的神祕都匯聚於此時,相信很多讀者也會對這個 ProxyGenerator 類產生有相似的疑惑:它到底作了什麼呢?它是如何生成動態代理類的代碼的呢?誠然,這裏也沒法給出確切的答案。仍是讓咱們帶着這些疑惑,一塊兒開始探索之旅吧。
事物每每不像其看起來的複雜,須要的是咱們可以化繁爲簡,這樣也許就能有更多撥雲見日的機會。拋開全部想象中的未知而複雜的神祕因素,若是讓咱們用最簡單的方法去實現一個代理類,惟一的要求是一樣結合調用處理器實施方法的分派轉發,您的第一反應將是什麼呢?」聽起來彷佛並非很複雜」。的確,掐指算算所涉及的工做無非包括幾個反射調用,以及對原始類型數據的裝箱或拆箱過程,其餘的彷佛都已經水到渠成。很是地好,讓咱們整理一下思緒,一塊兒來完成一次完整的推演過程吧。
// 假設需代理接口 Simulator public interface Simulator { short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB; } // 假設代理類爲 SimulatorProxy, 其類聲明將以下 final public class SimulatorProxy implements Simulator { // 調用處理器對象的引用 protected InvocationHandler handler; // 以調用處理器爲參數的構造函數 public SimulatorProxy(InvocationHandler handler){ this.handler = handler; } // 實現接口方法 simulate public short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB { // 第一步是獲取 simulate 方法的 Method 對象 java.lang.reflect.Method method = null; try{ method = Simulator.class.getMethod( "simulate", new Class[] {int.class, long.class, String.class} ); } catch(Exception e) { // 異常處理 1(略) } // 第二步是調用 handler 的 invoke 方法分派轉發方法調用 Object r = null; try { r = handler.invoke(this, method, // 對於原始類型參數須要進行裝箱操做 new Object[] {new Integer(arg1), new Long(arg2), arg3}); }catch(Throwable e) { // 異常處理 2(略) } // 第三步是返回結果(返回類型是原始類型則須要進行拆箱操做) return ((Short)r).shortValue(); } }
模擬推演爲了突出通用邏輯因此更多地關注正常流程,而淡化了錯誤處理,但在實際中錯誤處理一樣很是重要。從以上的推演中咱們能夠得出一個很是通用的結構化流程:
在這之中,全部的信息都是能夠已知的,好比接口名、方法名、參數類型、返回類型以及所需的裝箱和拆箱操做,那麼既然咱們手工編寫是如此,那又有什麼理由不相信 ProxyGenerator 不會作相似的實現呢?至少這是一種比較可能的實現。
接下來讓咱們把注意力從新回到先前被淡化的錯誤處理上來。在異常處理 1 處,因爲咱們有理由確保全部的信息如接口名、方法名和參數類型都準確無誤,因此這部分異常發生的機率基本爲零,因此基本能夠忽略。而異常處理 2 處,咱們須要思考得更多一些。回想一下,接口方法可能聲明支持一個異常列表,而調用處理器 invoke 方法又可能拋出與接口方法不支持的異常,再回想一下先前說起的 Java 動態代理的關於異常處理的特色,對於不支持的異常,必須拋 UndeclaredThrowableException 運行時異常。因此經過再次推演,咱們能夠得出一個更加清晰的異常處理 2 的狀況:
Object r = null; try { r = handler.invoke( this, method, new Object[] { new Integer(arg1), new Long(arg2), arg3 } ); } catch( ExceptionA e) { // 接口方法支持 ExceptionA,能夠拋出 throw e; } catch( ExceptionB e ) { // 接口方法支持 ExceptionB,能夠拋出 throw e; } catch(Throwable e) { // 其餘不支持的異常, // 一概拋 UndeclaredThrowableException throw new UndeclaredThrowableException(e); }
這樣咱們就完成了對動態代理類的推演實現。推演實現遵循了一個相對固定的模式,能夠適用於任意定義的任何接口,並且代碼生成所需的信息都是可知的,那麼有理由相信即便是機器自動編寫的代碼也有可能延續這樣的風格,至少能夠保證這是可行的。
誠然,Proxy 已經設計得很是優美,可是仍是有一點點小小的遺憾之處,那就是它始終沒法擺脫僅支持 interface 代理的桎梏,由於它的設計註定了這個遺憾。回想一下那些動態生成的代理類的繼承關係圖,它們已經註定有一個共同的父類叫 Proxy。Java 的繼承機制註定了這些動態代理類們沒法實現對 class 的動態代理,緣由是多繼承在 Java 中本質上就行不通。
有不少條理由,人們能夠否認對 class 代理的必要性,可是一樣有一些理由,相信支持 class 動態代理會更美好。接口和類的劃分,本就不是很明顯,只是到了 Java 中才變得如此的細化。若是隻從方法的聲明及是否被定義來考量,有一種二者的混合體,它的名字叫抽象類。實現對抽象類的動態代理,相信也有其內在的價值。此外,還有一些歷史遺留的類,它們將由於沒有實現任何接口而今後與動態代理永世無緣。如此種種,不得不說是一個小小的遺憾。
可是,不完美並不等於不偉大,偉大是一種本質,Java 動態代理就是佐例。
-- 來自IBM Developer 王忠平, 何平
代理模式是先人們針對一類特定問題總結出的經驗結晶,並在各個領域中得以靈活應用。特別是在編程領域,不一樣語言根據自身的設計規範和特色融會貫通,最終都可以落實到具體的解決方案中,譬如Spring AOP在性能事務上的加強提高,或者是攔截器實如今日誌和權限層面的控制過濾等等,都能作到對原接口事務的無侵入,同時還能靈活管控,大範圍實施,達到咱們的預期,這就是代理模式,巧妙之處。
[1]Java 動態代理機制分析及擴展:https://developer.ibm.com/zh/articles/j-lo-proxy1/
[2]代理模式原理及實例講解:https://developer.ibm.com/zh/articles/j-lo-proxy-pattern/
[3]Dynamic Proxy Classes:https://java.sun.com/j2se/1.4.2/docs/guide/reflection/proxy.html
[4]動態代理機制:https://www.ibm.com/developerworks/cn/java/j-jtp08305.html
[5]圖片素材來源:https://www.pexels.com/
[6]流程圖設計來源:https://www.processon.com/
BIU ~ 文章持續更新,微信搜索「潘潘和他的朋友們」第一時間閱讀,隨時有驚喜。本文會在 GitHub https://github.com/JavaWorld 收錄,熱騰騰的技術、框架、面經、解決方案,咱們都會以最美的姿式第一時間送達,歡迎 Star。