溫馨提示×

溫馨提示×

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

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

靜態(tài)代理和動態(tài)代理有哪些區(qū)別

發(fā)布時間:2020-12-07 10:39:57 來源:億速云 閱讀:215 作者:小新 欄目:編程語言

小編給大家分享一下靜態(tài)代理和動態(tài)代理有哪些區(qū)別,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

區(qū)別:靜態(tài)代理由程序員創(chuàng)建或工具生成代理類的源碼,再編譯代理類;程序運行前已經(jīng)存在代理類的字節(jié)碼文件,代理類和委托類的關系在運行前就確定了。動態(tài)代理類的源碼是在程序運行期間由JVM根據(jù)反射等機制動態(tài)的生成,所以不存在代理類的字節(jié)碼文件。

圖1:代理模式

靜態(tài)代理和動態(tài)代理有哪些區(qū)別靜態(tài)代理和動態(tài)代理有哪些區(qū)別

從圖中可以看出,代理接口(Subject)、代理類(ProxySubject)、委托類(RealSubject)形成一個“品”字結構。
根據(jù)代理類的生成時間不同可以將代理分為靜態(tài)代理和動態(tài)代理兩種。

下面以一個模擬需求說明靜態(tài)代理和動態(tài)代理:委托類要處理一項耗時較長的任務,客戶類需要打印出執(zhí)行任務消耗的時間。解決這個問題需要記錄任務執(zhí)行前時間和任務執(zhí)行后時間,兩個時間差就是任務執(zhí)行消耗的時間。

二、靜態(tài)代理

由程序員創(chuàng)建或工具生成代理類的源碼,再編譯代理類。所謂靜態(tài)也就是在程序運行前就已經(jīng)存在代理類的字節(jié)碼文件,代理類和委托類的關系在運行前就確定了。

清單1:代理接口

/**  
 * 代理接口。處理給定名字的任務。 
 */  
public interface Subject {  
  /** 
   * 執(zhí)行給定名字的任務。 
    * @param taskName 任務名 
   */  
   public void dealTask(String taskName);   
}

清單2:委托類,具體處理業(yè)務。

/** 
 * 真正執(zhí)行任務的類,實現(xiàn)了代理接口。 
 */  
public class RealSubject implements Subject {  
  
 /** 
  * 執(zhí)行給定名字的任務。這里打印出任務名,并休眠500ms模擬任務執(zhí)行了很長時間 
  * @param taskName  
  */  
   @Override  
   public void dealTask(String taskName) {  
      System.out.println("正在執(zhí)行任務:"+taskName);  
      try {  
         Thread.sleep(500);  
      } catch (InterruptedException e) {  
         e.printStackTrace();  
      }  
   }  
}

清單3:靜態(tài)代理類

/** 
 * 代理類,實現(xiàn)了代理接口。 
 */  
public class ProxySubject implements Subject {  
 //代理類持有一個委托類的對象引用  
 private Subject delegate;  
   
 public ProxySubject(Subject delegate) {  
  this.delegate = delegate;  
 }  
  
 /** 
  * 將請求分派給委托類執(zhí)行,記錄任務執(zhí)行前后的時間,時間差即為任務的處理時間 
  *  
  * @param taskName 
  */  
 @Override  
 public void dealTask(String taskName) {  
  long stime = System.currentTimeMillis();   
  //將請求分派給委托類處理  
  delegate.dealTask(taskName);  
  long ftime = System.currentTimeMillis();   
  System.out.println("執(zhí)行任務耗時"+(ftime - stime)+"毫秒");  
    
 }  
}

清單4:生成靜態(tài)代理類工廠

public class SubjectStaticFactory {  
 //客戶類調用此工廠方法獲得代理對象。  
 //對客戶類來說,其并不知道返回的是代理類對象還是委托類對象。  
 public static Subject getInstance(){   
  return new ProxySubject(new RealSubject());  
 }  
}

清單5:客戶類

public class Client1 {  
  
 public static void main(String[] args) {  
  Subject proxy = SubjectStaticFactory.getInstance();  
  proxy.dealTask("DBQueryTask");  
 }   
  
}

靜態(tài)代理類優(yōu)缺點

優(yōu)點:業(yè)務類只需要關注業(yè)務邏輯本身,保證了業(yè)務類的重用性。這是代理的共有優(yōu)點。

缺點:

1)代理對象的一個接口只服務于一種類型的對象,如果要代理的方法很多,勢必要為每一種方法都進行代理,靜態(tài)代理在程序規(guī)模稍大時就無法勝任了。

2)如果接口增加一個方法,除了所有實現(xiàn)類需要實現(xiàn)這個方法外,所有代理類也需要實現(xiàn)此方法。增加了代碼維護的復雜度。

三、動態(tài)代理

動態(tài)代理類的源碼是在程序運行期間由JVM根據(jù)反射等機制動態(tài)的生成,所以不存在代理類的字節(jié)碼文件。代理類和委托類的關系是在程序運行時確定。

1、先看看與動態(tài)代理緊密關聯(lián)的Java API。

1)java.lang.reflect.Proxy

這是 Java 動態(tài)代理機制生成的所有動態(tài)代理類的父類,它提供了一組靜態(tài)方法來為一組接口動態(tài)地生成代理類及其對象。

清單6:Proxy類的靜態(tài)方法

// 方法 1: 該方法用于獲取指定代理對象所關聯(lián)的調用處理器  
static InvocationHandler getInvocationHandler(Object proxy)   
  
// 方法 2:該方法用于獲取關聯(lián)于指定類裝載器和一組接口的動態(tài)代理類的類對象  
static Class getProxyClass(ClassLoader loader, Class[] interfaces)   
  
// 方法 3:該方法用于判斷指定類對象是否是一個動態(tài)代理類  
static boolean isProxyClass(Class cl)   
  
// 方法 4:該方法用于為指定類裝載器、一組接口及調用處理器生成動態(tài)代理類實例  
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

2)java.lang.reflect.InvocationHandler

這是調用處理器接口,它自定義了一個 invoke 方法,用于集中處理在動態(tài)代理類對象上的方法調用,通常在該方法中實現(xiàn)對委托類的代理訪問。每次生成動態(tài)代理類對象時都要指定一個對應的調用處理器對象。

清單7:InvocationHandler的核心方法

// 該方法負責集中處理動態(tài)代理類上的所有方法調用。第一個參數(shù)既是代理類實例,第二個參數(shù)是被調用的方法對象  
// 第三個方法是調用參數(shù)。調用處理器根據(jù)這三個參數(shù)進行預處理或分派到委托類實例上反射執(zhí)行  
Object invoke(Object proxy, Method method, Object[] args)

3)java.lang.ClassLoader

這是類裝載器類,負責將類的字節(jié)碼裝載到 Java 虛擬機(JVM)中并為其定義類對象,然后該類才能被使用。Proxy 靜態(tài)方法生成動態(tài)代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一區(qū)別就是其字節(jié)碼是由 JVM 在運行時動態(tài)生成的而非預存在于任何一個 .class 文件中。

每次生成動態(tài)代理類對象時都需要指定一個類裝載器對象

2、動態(tài)代理實現(xiàn)步驟

具體步驟是:

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

b. 給Proxy類提供ClassLoader和代理接口類型數(shù)組創(chuàng)建動態(tài)代理類

c. 以調用處理器類型為參數(shù),利用反射機制得到動態(tài)代理類的構造函數(shù)

d. 以調用處理器對象為參數(shù),利用動態(tài)代理類的構造函數(shù)創(chuàng)建動態(tài)代理類對象

清單8:分步驟實現(xiàn)動態(tài)代理

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

Proxy類的靜態(tài)方法newProxyInstance對上面具體步驟的后三步做了封裝,簡化了動態(tài)代理對象的獲取過程。
清單9:簡化后的動態(tài)代理實現(xiàn)

// InvocationHandlerImpl 實現(xiàn)了 InvocationHandler 接口,并能實現(xiàn)方法調用從代理類到委托類的分派轉發(fā)  
InvocationHandler handler = new InvocationHandlerImpl(..);   
  
// 通過 Proxy 直接創(chuàng)建動態(tài)代理類實例  
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,   
     new Class[] { Interface.class },  handler );

3、動態(tài)代理實現(xiàn)示例

清單10:創(chuàng)建自己的調用處理器

/** 
 * 動態(tài)代理類對應的調用處理程序類 
 */  
public class SubjectInvocationHandler implements InvocationHandler {  
   
 //代理類持有一個委托類的對象引用  
 private Object delegate;  
   
 public SubjectInvocationHandler(Object delegate) {  
  this.delegate = delegate;  
 }  
   
 @Override  
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  long stime = System.currentTimeMillis();   
  //利用反射機制將請求分派給委托類處理。Method的invoke返回Object對象作為方法執(zhí)行結果。  
  //因為示例程序沒有返回值,所以這里忽略了返回值處理  
  method.invoke(delegate, args);  
  long ftime = System.currentTimeMillis();   
  System.out.println("執(zhí)行任務耗時"+(ftime - stime)+"毫秒");  
    
  return null;  
 }  
}

清單11:生成動態(tài)代理對象的工廠,工廠方法列出了如何生成動態(tài)代理類對象的步驟。

/** 
 * 生成動態(tài)代理對象的工廠. 
 */  
public class DynProxyFactory {  
 //客戶類調用此工廠方法獲得代理對象。  
 //對客戶類來說,其并不知道返回的是代理類對象還是委托類對象。  
 public static Subject getInstance(){   
  Subject delegate = new RealSubject();  
  InvocationHandler handler = new SubjectInvocationHandler(delegate);  
  Subject proxy = null;  
  proxy = (Subject)Proxy.newProxyInstance(  
    delegate.getClass().getClassLoader(),   
    delegate.getClass().getInterfaces(),   
    handler);  
  return proxy;  
 }  
}

清單12:動態(tài)代理客戶類

public class Client {  
  
 public static void main(String[] args) {  
  
  Subject proxy = DynProxyFactory.getInstance();  
  proxy.dealTask("DBQueryTask");  
 }   
  
}

4、動態(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)代理類,它會很聰明地返回先前已經(jīng)創(chuàng)建好的代理類的類對象,而不會再嘗試去創(chuàng)建一個全新的代理類,這樣可以節(jié)省不必要的代碼重復生成,提高了代理類的創(chuàng)建效率。4)類繼承關系:該類的繼承關系如圖:

圖2:動態(tài)代理類的繼承關系

靜態(tài)代理和動態(tài)代理有哪些區(qū)別

由圖可見,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)代理類已經(jīng)為我們設計好了解決方法:它將會拋出 UndeclaredThrowableException 異常。這個異常是一個 RuntimeException 類型,所以不會引起編譯錯誤。通過該異常的 getCause 方法,還可以獲得原來那個不受支持的異常對象,以便于錯誤診斷。

5、動態(tài)代理的優(yōu)點和美中不足

優(yōu)點:

動態(tài)代理與靜態(tài)代理相比較,最大的好處是接口中聲明的所有方法都被轉移到調用處理器一個集中的方法中處理(InvocationHandler.invoke)。這樣,在接口方法數(shù)量比較多的時候,我們可以進行靈活處理,而不需要像靜態(tài)代理那樣每一個方法進行中轉。在本示例中看不出來,因為invoke方法體內嵌入了具體的外圍業(yè)務(記錄任務處理前后時間并計算時間差),實際中可以類似Spring AOP那樣配置外圍業(yè)務。

美中不足:

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

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

以上是“靜態(tài)代理和動態(tài)代理有哪些區(qū)別”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

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

AI