熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java動態代理機制綜合分析及擴展

2022-06-13   來源: Java核心技術 

  Java動態代理機制的出現使得Java開發人員不用手工編寫代理類只要簡單地指定一組接口及委托類對象便能動態地獲得代理類這是一套非常靈活有彈性的代理框架

  代理設計模式

  代理是一種常用的設計模式其目的就是為其他對象提供一個代理以控制對某個對象的訪問代理類負責為委托類預處理消息過濾消息並轉發消息以及進行消息被委托類執行後的後續處理

  為了保持行為的一致性代理類和委托類通常會實現相同的接口所以在訪問者看來兩者沒有絲毫的區別通過代理類這中間一層能有效控制對委托類對象的直接訪問也可以很好地隱藏和保護委托類對象同時也為實施不同控制策略預留了空間從而在設計上獲得了更大的靈活性Java動態代理機制以巧妙的方式近乎完美地實踐了代理模式的設計理念

  相關的類和接口

  要了解Java動態代理的機制首先需要了解以下相關的類或接口javalangreflectProxy這是Java動態代理機制的主類它提供了一組靜態方法來為一組接口動態地生成代理類及其對象

  清單Proxy的靜態方法

  //方法:該方法用於獲取指定代理對象所關聯的調用處理器

  staticInvocationHandlergetInvocationHandler(Objectproxy)

  //方法該方法用於獲取關聯於指定類裝載器和一組接口的動態代理類的類對象

  staticClassgetProxyClass(ClassLoaderloaderClass[]interfaces)

  //方法該方法用於判斷指定類對象是否是一個動態代理類

  staticbooleanisProxyClass(Classcl)

  //方法該方法用於為指定類裝載器一組接口及調用處理器生成動態代理類實例

  staticObjectnewProxyInstance(ClassLoaderloaderClass[]interfaces

  InvocationHandlerh)

  javalangreflectInvocationHandler這是調用處理器接口它自定義了一個invoke方法用於集中處理在動態代理類對象上的方法調用通常在該方法中實現對委托類的代理訪問

  清單InvocationHandler的核心方法

  //該方法負責集中處理動態代理類上的所有方法調用第一個參數既是代理類實例第二個參數是被調用的方法對象

  //第三個方法是調用參數調用處理器根據這三個參數進行預處理或分派到委托類實例上發射執行

  Objectinvoke(ObjectproxyMethodmethodObject[]args)

  每次生成動態代理類對象時都需要指定一個實現了該接口的調用處理器對象(參見Proxy靜態方法的第三個參數)javalangClassLoader這是類裝載器類負責將類的字節碼裝載到Java虛擬機(JVM)中並為其定義類對象然後該類才能被使用Proxy靜態方法生成動態代理類同樣需要通過類裝載器來進行裝載才能使用它與普通類的唯一區別就是其字節碼是由JVM在運行時動態生成的而非預存在於任何一個class文件中

  每次生成動態代理類對象時都需要指定一個類裝載器對象(參見Proxy靜態方法的第一個參數)

  代理機制及其特點

  首先讓我們來了解一下如何使用Java動態代理具體有如下四步驟

  通過實現InvocationHandler接口創建自己的調用處理器

  通過為Proxy類指定ClassLoader對象和一組interface來創建動態代理類

  通過反射機制獲得動態代理類的構造函數其唯一參數類型是調用處理器接口類型

  通過構造函數創建動態代理類實例構造時調用處理器對象作為參數被傳入

  清單動態代理對象創建過程

  //InvocationHandlerImpl實現了InvocationHandler接口並能實現方法調用從代理類到委托類的分派轉發

  //其內部通常包含指向委托類實例的引用用於真正執行分派轉發過來的方法調用

  InvocationHandlerhandler=newInvocationHandlerImpl();

  //通過Proxy為包括Interface接口在內的一組接口動態創建代理類的類對象

  Classclazz=ProxygetProxyClass(classLoadernewClass[]{Interfaceclass});

  //通過反射從生成的類對象獲得構造函數對象

  Constructorconstructor=clazzgetConstructor(newClass[]{InvocationHandlerclass});

  //通過構造函數對象創建動態代理類實例

  InterfaceProxy=(Interface)constructornewInstance(newObject[]{handler});

  實際使用過程更加簡單因為Proxy的靜態方法newProxyInstance已經為我們封裝了步驟到步驟的過程所以簡化後的過程如下

  清單簡化的動態代理對象創建過程

  //InvocationHandlerImpl實現了InvocationHandler接口並能實現方法調用從代理類到委托類的分派轉發

  InvocationHandlerhandler=newInvocationHandlerImpl();

  //通過Proxy直接創建動態代理類實例

  Interfaceproxy=(Interface)ProxynewProxyInstance(classLoader

  newClass[]{Interfaceclass}

  handler);

  接下來讓我們來了解一下Java動態代理機制的一些特點首先是動態生成的代理類本身的一些特點

  )包如果所代理的接口都是public的那麼它將被定義在頂層包(即包路徑為空)如果所代理的接口中有非public的接口(因為接口不能被定義為protect或private所以除public之外就是默認的package訪問級別)那麼它將被定義在該接口所在包(假設代理了comibmdeveloperworks包中的某非public接口A那麼新生成的代理類所在的包就是comibmdeveloperworks)這樣設計的目的是為了最大程度的保證動態代理類不會因為包管理的問題而無法被成功定義並訪問

  )類修飾符該代理類具有final和public修飾符意味著它可以被所有的類訪問但是不能被再度繼承

  )類名格式是$ProxyN其中N是一個逐一遞增的阿拉伯數字代表Proxy類第N次生成的動態代理類值得注意的一點是並不是每次調用Proxy的靜態方法創建動態代理類都會使得N值增加原因是如果對同一組接口(包括接口排列的順序相同)試圖重復創建動態代理類它會很聰明地返回先前已經創建好的代理類的類對象而不會再嘗試去創建一個全新的代理類這樣可以節省不必要的代碼重復生成提高了代理類的創建效率

  )類繼承關系該類的繼承關系如圖

  由圖可見Proxy類是它的父類這個規則適用於所有由Proxy創建的動態代理類而且該類還實現了其所代理的一組接口這就是為什麼它能夠被安全地類型轉換到其所代理的某接口的根本原因

  接下來讓我們了解一下代理類實例的一些特點每個實例都會關聯一個調用處理器對象可以通過Proxy提供的靜態方法getInvocationHandler去獲得代理類實例的調用處理器對象

  在代理類實例上調用其代理的接口中所聲明的方法時這些方法最終都會由調用處理器的invoke方法執行此外值得注意的是代理類的根類javalangObject中有三個方法也同樣會被分派到調用處理器的invoke方法執行它們是hashCodeequals和toString可能的原因有一是因為這些方法為public且非final類型能夠被代理類覆蓋二是因為這些方法往往呈現出一個類的某種特征屬性具有一定的區分度所以為了保證代理類與委托類對外的一致性這三個方法也應該被分派到委托類執行

  當代理的一組接口有重復聲明的方法且該方法被調用時代理類總是從排在最前面的接口中獲取方法對象並分派給調用處理器而無論代理類實例是否正在以該接口(或繼承於該接口的某子接口)的形式被外部引用因為在代理類內部無法區分其當前的被引用類型

  接著來了解一下被代理的一組接口有哪些特點首先要注意不能有重復的接口以避免動態代理類代碼生成時的編譯錯誤其次這些接口對於類裝載器必須可見否則類裝載器將無法鏈接它們將會導致類定義失敗再次需被代理的所有非public的接口必須在同一個包中否則代理類生成也會失敗最後接口的數目不能超過這是JVM設定的限制

  最後再來了解一下異常處理方面的特點從調用處理器接口聲明的方法中可以看到理論上它能夠拋出任何類型的異常因為所有的異常都繼承於Throwable接口但事實是否如此呢?答案是否定的原因是我們必須遵守一個繼承原則即子類覆蓋父類或實現父接口的方法時拋出的異常必須在原方法支持的異常列表之內所以雖然調用處理器理論上講能夠但實際上往往受限制除非父接口中的方法支持拋Throwable異常

  那麼如果在invoke方法中的確產生了接口方法聲明中不支持的異常那將如何呢?放心Java動態代理類已經為我們設計好了解決方法它將會拋出UndeclaredThrowableException異常這個異常是一個RuntimeException類型所以不會引起編譯錯誤通過該異常的getCause方法還可以獲得原來那個不受支持的異常對象以便於錯誤診斷

  代碼是最好的老師

  機制和特點都介紹過了接下來讓我們通過源代碼來了解一下Proxy到底是如何實現的首先記住Proxy的幾個重要的靜態變量

  清單Proxy的重要靜態變量

  //映射表用於維護類裝載器對象到其對應的代理類緩存

  privatestaticMaploaderToCache=newWeakHashMap();

  //標記用於標記一個動態代理類正在被創建中

  privatestaticObjectpendingGenerationMarker=newObject();

  //同步表記錄已經被創建的動態代理類類型主要被方法isProxyClass進行相關的判斷

  privatestaticMapproxyClasses=CollectionssynchronizedMap(newWeakHashMap());

  //關聯的調用處理器引用

  protectedInvocationHandlerh;

  然後來看一下Proxy的構造方法

  清單Proxy構造方法

  //由於Proxy內部從不直接調用構造函數所以private類型意味著禁止任何調用

  privateProxy(){}

  //由於Proxy內部從不直接調用構造函數所以protected意味著只有子類可以調用

  protectedProxy(InvocationHandlerh){thish=h;}

  接著可以快速浏覽一下newProxyInstance方法因為其相當簡單

  清單Proxy靜態方法newProxyInstance

  publicstaticObjectnewProxyInstance(ClassLoaderloader

  Class<?>[]interfaces

  InvocationHandlerh)

  throwsIllegalArgumentException{

  

  //檢查h不為空否則拋異常

  if(h==null){

  thrownewNullPointerException();

  }

  

  //獲得與制定類裝載器和一組接口相關的代理類類型對象

  Classcl=getProxyClass(loaderinterfaces);

  

  //通過反射獲取構造函數對象並生成代理類實例

  try{

  Constructorcons=clgetConstructor(constructorParams);

  return(Object)consnewInstance(newObject[]{h});

  }catch(NoSuchMethodExceptione){thrownewInternalError(etoString());

  }catch(IllegalAccessExceptione){thrownewInternalError(etoString());

  }catch(InstantiationExceptione){thrownewInternalError(etoString());

  }catch(InvocationTargetExceptione){thrownewInternalError(etoString());

  }

  }

  由此可見動態代理真正的關鍵是在getProxyClass方法該方法負責為一組接口動態地生成代理類類型對象在該方法內部您將能看到Proxy內的各路英雄(靜態變量)悉數登場有點迫不及待了麼?那就讓我們一起走進Proxy最最神秘的殿堂去欣賞一番吧該方法總共可以分為四個步驟

  對這組接口進行一定程度的安全檢查包括檢查接口類對象是否對類裝載器可見並且與類裝載器所能識別的接口類對象是完全相同的還會檢查確保是interface類型而不是class類型這個步驟通過一個循環來完成檢查通過後將會得到一個包含所有接口名稱的字符串數組記為String[]interfaceNames總體上這部分實現比較直觀所以略去大部分代碼僅保留留如何判斷某類或接口是否對特定類裝載器可見的相關代碼

  清單通過ClassforName方法判接口的可見性

  try{

  //指定接口名字類裝載器對象同時制定initializeBoolean為false表示無須初始化類

  //如果方法返回正常這表示可見否則會拋出ClassNotFoundException異常表示不可見

  interfaceClass=ClassforName(interfaceNamefalseloader);

  }catch(ClassNotFoundExceptione){

  }

  從loaderToCache映射表中獲取以類裝載器對象為關鍵字所對應的緩存表如果不存在就創建一個新的緩存表並更新到loaderToCache緩存表是一個HashMap實例正常情況下它將存放鍵值對(接口名字列表動態生成的代理類的類對象引用)當代理類正在被創建時它會臨時保存(接口名字列表pendingGenerationMarker)標記pendingGenerationMarke的作用是通知後續的同類請求(接口數組相同且組內接口排列順序也相同)代理類正在被創建請保持等待直至創建完成

  清單緩存表的使用

  do{

  //以接口名字列表作為關鍵字獲得對應cache值

  Objectvalue=cacheget(key);

  if(valueinstanceofReference){

  proxyClass=(Class)((Reference)value)get();

  }

  if(proxyClass!=null){

  //如果已經創建直接返回

  returnproxyClass;

  }elseif(value==pendingGenerationMarker){

  //代理類正在被創建保持等待

  try{

  cachewait();

  }catch(InterruptedExceptione){

  }

  //等待被喚醒繼續循環並通過二次檢查以確保創建完成否則重新等待

  ntinue;

  }else{

  //標記代理類正在被創建

  cacheput(keypendingGenerationMarker);

  //break跳出循環已進入創建過程

  break;

  }while(true);

  動態創建代理類的類對象首先是確定代理類所在的包其原則如前所述如果都為public接口則包名為空字符串表示頂層包如果所有非public接口都在同一個包則包名與這些接口的包名相同如果有多個非public接口且不同包則拋異常終止代理類的生成確定了包後就開始生成代理類的類名同樣如前所述按格式$ProxyN生成類名也確定了接下來就是見證奇跡的發生動態生成代理類

  清單動態生成代理類

  //動態地生成代理類的字節碼數組

  byte[]proxyClassFile=ProxyGeneratorgenerateProxyClass(proxyNameinterfaces);

  try{

  //動態地定義新生成的代理類

  proxyClass=defineClass(loaderproxyNameproxyClassFile

  proxyClassFilelength);

  }catch(ClassFormatErrore){

  thrownewIllegalArgumentException(etoString());

  }

  //把生成的代理類的類對象記錄進proxyClasses表

  proxyClassesput(proxyClassnull);

  由此可見所有的代碼生成的工作都由神秘的ProxyGenerator所完成了當你嘗試去探索這個類時你所能獲得的信息僅僅是它位於並未公開的sunmisc包有若干常量變量和方法以完成這個神奇的代碼生成的過程但是sun並沒有提供源代碼以供研讀至於動態類的定義則由Proxy的native靜態方法defineClass執行

  代碼生成過程進入結尾部分根據結果更新緩存表如果成功則將代理類的類對象引用更新進緩存表否則清楚緩存表中對應關鍵值最後喚醒所有可能的正在等待的線程

  走完了以上四個步驟後至此所有的代理類生成細節都已介紹完畢剩下的靜態方法如getInvocationHandler和isProxyClass就顯得如此的直觀只需通過查詢相關變量就可以完成所以對其的代碼分析就省略了

  代理類實現推演

  分析了Proxy類的源代碼相信在讀者的腦海中會對Java動態代理機制形成一個更加清晰的理解但是當探索之旅在sunmiscProxyGenerator類處嘎然而止所有的神秘都匯聚於此時相信不少讀者也會對這個ProxyGenerator類產生有類似的疑惑它到底做了什麼呢?它是如何生成動態代理類的代碼的呢?誠然這裡也無法給出確切的答案還是讓我們帶著這些疑惑一起開始探索之旅吧

  事物往往不像其看起來的復雜需要的是我們能夠化繁為簡這樣也許就能有更多撥雲見日的機會拋開所有想象中的未知而復雜的神秘因素如果讓我們用最簡單的方法去實現一個代理類唯一的要求是同樣結合調用處理器實施方法的分派轉發您的第一反應將是什麼呢?聽起來似乎並不是很復雜的確掐指算算所涉及的工作無非包括幾個反射調用以及對原始類型數據的裝箱或拆箱過程其他的似乎都已經水到渠成非常地好讓我們整理一下思緒一起來完成一次完整的推演過程吧

  清單代理類中方法調用的分派轉發推演實現

  //假設需代理接口Simulator

  publicinterfaceSimulator{

  shortsimulate(intarglongargStringarg)throwsExceptionAExceptionB;

  }

  

  //假設代理類為SimulatorProxy其類聲明將如下

  finalpublicclassSimulatorProxyimplementsSimulator{

  

  //調用處理器對象的引用

  protectedInvocationHandlerhandler;

  

  //以調用處理器為參數的構造函數

  publicSimulatorProxy(InvocationHandlerhandler){

  thishandler=handler;

  }

  

  //實現接口方法simulate

  publicshortsimulate(intarglongargStringarg)

  throwsExceptionAExceptionB{

  

  //第一步是獲取simulate方法的Method對象

  javalangreflectMethodmethod=null;

  try{

  thod=SimulatorclassgetMethod(

  simulate

  newClass[]{intclasslongclassStringclass});

  }catch(Exceptione){

  //異常處理(略)

  }

  

  //第二步是調用handler的invoke方法分派轉發方法調用

  Objectr=null;

  try{

  r=handlerinvoke(this

  thod

  //對於原始類型參數需要進行裝箱操作

  newObject[]{newInteger(arg)newLong(arg)arg});

  }catch(Throwablee){

  //異常處理(略)

  }

  //第三步是返回結果(返回類型是原始類型則需要進行拆箱操作)

  return((Short)r)shortValue();

  }

  }

  模擬推演為了突出通用邏輯所以更多地關注正常流程而淡化了錯誤處理但在實際中錯誤處理同樣非常重要從以上的推演中我們可以得出一個非常通用的結構化流程第一步從代理接口獲取被調用的方法對象第二步分派方法到調用處理器執行第三步返回結果

  在這之中所有的信息都是可以已知的比如接口名方法名參數類型返回類型以及所需的裝箱和拆箱操作那麼既然我們手工編寫是如此那又有什麼理由不相信ProxyGenerator不會做類似的實現呢?至少這是一種比較可能的實現

  接下來讓我們把注意力重新回到先前被淡化的錯誤處理上來在異常處理由於我們有理由確保所有的信息如接口名方法名和參數類型都准確無誤所以這部分異常發生的概率基本為零所以基本可以忽略而異常處理我們需要思考得更多一些

  回想一下接口方法可能聲明支持一個異常列表而調用處理器invoke方法又可能拋出與接口方法不支持的異常再回想一下先前提及的Java動態代理的關於異常處理的特點對於不支持的異常必須拋UndeclaredThrowableException運行時異常所以通過再次推演我們可以得出一個更加清晰的異常處理的情況

  清單細化的異常處理

  Objectr=null;

  

  try{

  r=handlerinvoke(this

  thod

  newObject[]{newInteger(arg)newLong(arg)arg});

  

  }catch(ExceptionAe){

  

  //接口方法支持ExceptionA可以拋出

  throwe;

  

  }catch(ExceptionBe){

  //接口方法支持ExceptionB可以拋出

  throwe;

  

  }catch(Throwablee){

  //其他不支持的異常一律拋UndeclaredThrowableException

  thrownewUndeclaredThrowableException(e);

  }

  這樣我們就完成了對動態代理類的推演實現推演實現遵循了一個相對固定的模式可以適用於任意定義的任何接口而且代碼生成所需的信息都是可知的那麼有理由相信即使是機器自動編寫的代碼也有可能延續這樣的風格至少可以保證這是可行的

  美中不足

  誠然Proxy已經設計得非常優美但是還是有一點點小小的遺憾之處那就是它始終無法擺脫僅支持interface代理的桎梏因為它的設計注定了這個遺憾回想一下那些動態生成的代理類的繼承關系圖它們已經注定有一個共同的父類叫ProxyJava的繼承機制注定了這些動態代理類們無法實現對class的動態代理原因是多繼承在Java中本質上就行不通

  有很多條理由人們可以否定對class代理的必要性但是同樣有一些理由相信支持class動態代理會更美好接口和類的劃分本就不是很明顯只是到了Java中才變得如此的細化如果只從方法的聲明及是否被定義來考量有一種兩者的混合體它的名字叫抽象類實現對抽象類的動態代理相信也有其內在的價值此外還有一些歷史遺留的類它們將因為沒有實現任何接口而從此與動態代理永世無緣如此種種不得不說是一個小小的遺憾但是不完美並不等於不偉大偉大是一種本質Java動態代理就是佐例


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26357.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.