溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java動態(tài)代理機制是什么

發(fā)布時間:2021-07-20 09:23:32 來源:億速云 閱讀:155 作者:chen 欄目:編程語言

這篇文章主要講解了“Java動態(tài)代理機制是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java動態(tài)代理機制是什么”吧!

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

代理:設計模式

代理是一種常用的設計模式,其目的就是為其他對象提供一個代理以控制對某個對象的訪問。代理類負責為委托類預處理消息,過濾消息并轉發(fā)消息,以及進行消息被委托類執(zhí)行后的后續(xù)處理。

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

相關的類和接口

要了解Java動態(tài)代理的機制,首先需要了解以下相關的類或接口:java.lang.reflect.Proxy:這是Java動態(tài)代理機制的主類,它提供了一組靜態(tài)方法來為一組接口動態(tài)地生成代理類及其對象。

清單1.Proxy的靜態(tài)方法  //方法1:該方法用于獲取指定代理對象所關聯(lián)的調用處理器  staticInvocationHandlergetInvocationHandler(Objectproxy)  //方法2:該方法用于獲取關聯(lián)于指定類裝載器和一組接口的動態(tài)代理類的類對象  staticClassgetProxyClass(ClassLoaderloader,Class[]interfaces)  //方法3:該方法用于判斷指定類對象是否是一個動態(tài)代理類  staticbooleanisProxyClass(Classcl)  //方法4:該方法用于為指定類裝載器、一組接口及調用處理器生成動態(tài)代理類實例  staticObjectnewProxyInstance(ClassLoaderloader,Class[]interfaces,  InvocationHandlerh)

java.lang.reflect.InvocationHandler:這是調用處理器接口,它自定義了一個invoke方法,用于集中處理在動態(tài)代理類對象上的方法調用,通常在該方法中實現(xiàn)對委托類的代理訪問。

清單2.InvocationHandler的核心方法  //該方法負責集中處理動態(tài)代理類上的所有方法調用。第一個參數(shù)既是代理類實例,第二個參數(shù)是被調用的方法對象  //第三個方法是調用參數(shù)。調用處理器根據(jù)這三個參數(shù)進行預處理或分派到委托類實例上發(fā)射執(zhí)行  Objectinvoke(Objectproxy,Methodmethod,Object[]args)

每次生成動態(tài)代理類對象時都需要指定一個實現(xiàn)了該接口的調用處理器對象(參見Proxy靜態(tài)方法4的第三個參數(shù))。java.lang.ClassLoader:這是類裝載器類,負責將類的字節(jié)碼裝載到Java虛擬機(JVM)中并為其定義類對象,然后該類才能被使用。Proxy靜態(tài)方法生成動態(tài)代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一區(qū)別就是其字節(jié)碼是由JVM在運行時動態(tài)生成的而非預存在于任何一個.class文件中。
每次生成動態(tài)代理類對象時都需要指定一個類裝載器對象(參見Proxy靜態(tài)方法4的第一個參數(shù))

代理機制及其特點

首先讓我們來了解一下如何使用Java動態(tài)代理。具體有如下四步驟:

1.通過實現(xiàn)InvocationHandler接口創(chuàng)建自己的調用處理器;

2.通過為Proxy類指定ClassLoader對象和一組interface來創(chuàng)建動態(tài)代理類;

3.通過反射機制獲得動態(tài)代理類的構造函數(shù),其唯一參數(shù)類型是調用處理器接口類型;

4.通過構造函數(shù)創(chuàng)建動態(tài)代理類實例,構造時調用處理器對象作為參數(shù)被傳入。

清單3.動態(tài)代理對象創(chuàng)建過程  //InvocationHandlerImpl實現(xiàn)了InvocationHandler接口,并能實現(xiàn)方法調用從代理類到委托類的分派轉發(fā)  //其內部通常包含指向委托類實例的引用,用于真正執(zhí)行分派轉發(fā)過來的方法調用  InvocationHandlerhandler=newInvocationHandlerImpl(..);  //通過Proxy為包括Interface接口在內的一組接口動態(tài)創(chuàng)建代理類的類對象  Classclazz=Proxy.getProxyClass(classLoader,newClass[]{Interface.class,...});  //通過反射從生成的類對象獲得構造函數(shù)對象  Constructorconstructor=clazz.getConstructor(newClass[]{InvocationHandler.class});  //通過構造函數(shù)對象創(chuàng)建動態(tài)代理類實例  InterfaceProxy=(Interface)constructor.newInstance(newObject[]{handler});

實際使用過程更加簡單,因為Proxy的靜態(tài)方法newProxyInstance已經為我們封裝了步驟2到步驟4的過程,所以簡化后的過程如下:

清單4.簡化的動態(tài)代理對象創(chuàng)建過程  //InvocationHandlerImpl實現(xiàn)了InvocationHandler接口,并能實現(xiàn)方法調用從代理類到委托類的分派轉發(fā)  InvocationHandlerhandler=newInvocationHandlerImpl(..);  //通過Proxy直接創(chuàng)建動態(tài)代理類實例  Interfaceproxy=(Interface)Proxy.newProxyInstance(classLoader,  newClass[]{Interface.class},  handler);


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

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

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

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

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

由圖可見,Proxy類是它的父類,這個規(guī)則適用于所有由Proxy創(chuàng)建的動態(tài)代理類。而且該類還實現(xiàn)了其所代理的一組接口,這就是為什么它能夠被安全地類型轉換到其所代理的某接口的根本原因。

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

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

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

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

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

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

代碼是最好的老師

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

清單5.Proxy的重要靜態(tài)變量  //映射表:用于維護類裝載器對象到其對應的代理類緩存  privatestaticMaploaderToCache=newWeakHashMap();  //標記:用于標記一個動態(tài)代理類正在被創(chuàng)建中  privatestaticObjectpendingGenerationMarker=newObject();  //同步表:記錄已經被創(chuàng)建的動態(tài)代理類類型,主要被方法isProxyClass進行相關的判斷  privatestaticMapproxyClasses=Collections.synchronizedMap(newWeakHashMap());  //關聯(lián)的調用處理器引用  protectedInvocationHandlerh;

然后,來看一下Proxy的構造方法:

清單6.Proxy構造方法  //由于Proxy內部從不直接調用構造函數(shù),所以private類型意味著禁止任何調用  privateProxy(){}  //由于Proxy內部從不直接調用構造函數(shù),所以protected意味著只有子類可以調用  protectedProxy(InvocationHandlerh){this.h=h;}

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

清單7.Proxy靜態(tài)方法newProxyInstance  publicstaticObjectnewProxyInstance(ClassLoaderloader,  Class<?>[]interfaces,  InvocationHandlerh)  throwsIllegalArgumentException{   //檢查h不為空,否則拋異常  if(h==null){  thrownewNullPointerException();  }   //獲得與制定類裝載器和一組接口相關的代理類類型對象  Classcl=getProxyClass(loader,interfaces);   //通過反射獲取構造函數(shù)對象并生成代理類實例  try{  Constructorcons=cl.getConstructor(constructorParams);  return(Object)cons.newInstance(newObject[]{h});  }catch(NoSuchMethodExceptione){thrownewInternalError(e.toString());  }catch(IllegalAccessExceptione){thrownewInternalError(e.toString());  }catch(InstantiationExceptione){thrownewInternalError(e.toString());  }catch(InvocationTargetExceptione){thrownewInternalError(e.toString());  }  }

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

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

清單8.通過Class.forName方法判接口的可見性  try{  //指定接口名字、類裝載器對象,同時制定initializeBoolean為false表示無須初始化類  //如果方法返回正常這表示可見,否則會拋出ClassNotFoundException異常表示不可見  interfaceClass=Class.forName(interfaceName,false,loader);  }catch(ClassNotFoundExceptione){  }


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

清單9.緩存表的使用  do{  //以接口名字列表作為關鍵字獲得對應cache值  Objectvalue=cache.get(key);  if(valueinstanceofReference){  proxyClass=(Class)((Reference)value).get();  }  if(proxyClass!=null){  //如果已經創(chuàng)建,直接返回  returnproxyClass;  }elseif(value==pendingGenerationMarker){  //代理類正在被創(chuàng)建,保持等待  try{  cache.wait();  }catch(InterruptedExceptione){  }  //等待被喚醒,繼續(xù)循環(huán)并通過二次檢查以確保創(chuàng)建完成,否則重新等待  continue;  }else{  //標記代理類正在被創(chuàng)建  cache.put(key,pendingGenerationMarker);  //break跳出循環(huán)已進入創(chuàng)建過程  break;  }while(true);

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

清單10.動態(tài)生成代理類  //動態(tài)地生成代理類的字節(jié)碼數(shù)組  byte[]proxyClassFile=ProxyGenerator.generateProxyClass(proxyName,interfaces);  try{  //動態(tài)地定義新生成的代理類  proxyClass=defineClass0(loader,proxyName,proxyClassFile,0,  proxyClassFile.length);  }catch(ClassFormatErrore){  thrownewIllegalArgumentException(e.toString());  }  //把生成的代理類的類對象記錄進proxyClasses表  proxyClasses.put(proxyClass,null);

由此可見,所有的代碼生成的工作都由神秘的ProxyGenerator所完成了,當你嘗試去探索這個類時,你所能獲得的信息僅僅是它位于并未公開的sun.misc包,有若干常量、變量和方法以完成這個神奇的代碼生成的過程,但是sun并沒有提供源代碼以供研讀。至于動態(tài)類的定義,則由Proxy的native靜態(tài)方法defineClass0執(zhí)行。

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

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

代理類實現(xiàn)推演

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

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

清單11.代理類中方法調用的分派轉發(fā)推演實現(xiàn)  //假設需代理接口Simulator  publicinterfaceSimulator{  shortsimulate(intarg1,longarg2,Stringarg3)throwsExceptionA,ExceptionB;  }   //假設代理類為SimulatorProxy,其類聲明將如下  finalpublicclassSimulatorProxyimplementsSimulator{   //調用處理器對象的引用  protectedInvocationHandlerhandler;   //以調用處理器為參數(shù)的構造函數(shù)  publicSimulatorProxy(InvocationHandlerhandler){  this.handler=handler;  }   //實現(xiàn)接口方法simulate  publicshortsimulate(intarg1,longarg2,Stringarg3)  throwsExceptionA,ExceptionB{   //第一步是獲取simulate方法的Method對象  java.lang.reflect.Methodmethod=null;  try{  method=Simulator.class.getMethod(  "simulate",  newClass[]{int.class,long.class,String.class});  }catch(Exceptione){  //異常處理1(略)  }   //第二步是調用handler的invoke方法分派轉發(fā)方法調用  Objectr=null;  try{  r=handler.invoke(this,  method,  //對于原始類型參數(shù)需要進行裝箱操作  newObject[]{newInteger(arg1),newLong(arg2),arg3});  }catch(Throwablee){  //異常處理2(略)  }  //第三步是返回結果(返回類型是原始類型則需要進行拆箱操作)  return((Short)r).shortValue();  }  }

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

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


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

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

清單12.細化的異常處理2  Objectr=null;   try{  r=handler.invoke(this,  method,  newObject[]{newInteger(arg1),newLong(arg2),arg3});   }catch(ExceptionAe){   //接口方法支持ExceptionA,可以拋出  throwe;   }catch(ExceptionBe){  //接口方法支持ExceptionB,可以拋出  throwe;   }catch(Throwablee){  //其他不支持的異常,一律拋UndeclaredThrowableException  thrownewUndeclaredThrowableException(e);  }

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

美中不足

誠然,Proxy已經設計得非常優(yōu)美,但是還是有一點點小小的遺憾之處,那就是它始終無法擺脫僅支持interface代理的桎梏,因為它的設計注定了這個遺憾?;叵胍幌履切﹦討B(tài)生成的代理類的繼承關系圖,它們已經注定有一個共同的父類叫Proxy。Java的繼承機制注定了這些動態(tài)代理類們無法實現(xiàn)對class的動態(tài)代理,原因是多繼承在Java中本質上就行不通。

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

感謝各位的閱讀,以上就是“Java動態(tài)代理機制是什么”的內容了,經過本文的學習后,相信大家對Java動態(tài)代理機制是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI