程式碼的分離與解耦,向移動架構師進階!

codeGoogle發表於2018-04-25

一、代理概念

為某個物件提供一個代理,以控制對這個物件的訪問。 代理類和委託類有共同的父類或父介面,這樣在任何使用委託類物件的地方都可以用代理物件替代。代理類負責請求的預處理、過濾、將請求分派給委託類處理、以及委託類執行完請求後的後續處理。

圖1:代理模式

從圖中可以看出,代理介面(Subject)、代理類(ProxySubject)、委託類(RealSubject)形成一個“品”字結構。

根據代理類的生成時間不同可以將代理分為靜態代理和動態代理兩種。

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

二、靜態代理

由我們自己建立或工具生成代理類的原始碼,再編譯代理類。所謂靜態也就是在程式執行前就已經存在代理類的位元組碼檔案,代理類和委託類的關係在執行前就確定了

1:代理介面

/** 
 * 代理類,實現了代理介面。 
 */  
public class ProxySubject implements Subject {  
 //代理類持有一個委託類的物件引用  
 private Subject delegate;  

 public ProxySubject(Subject delegate) {  
  this.delegate = delegate;  
 }  

 /** 
  * 將請求分派給委託類執行,記錄任務執行前後的時間,時間差即為任務的處理時間 
  *  
  * @param taskName 
  */  
 @Override  
 public void dealTask(String taskName) {  
  long stime = System.currentTimeMillis();   
  //將請求分派給委託類處理  
  delegate.dealTask(taskName);  
  long ftime = System.currentTimeMillis();   
  System.out.println("執行任務耗時"+(ftime - stime)+"毫秒");  

 }  
}  

複製程式碼

清單3:靜態代理類

public class SubjectStaticFactory {  
 //客戶類呼叫此工廠方法獲得代理物件。  
 //對客戶類來說,其並不知道返回的是代理類物件還是委託類物件。  
 public static Subject getInstance(){   
  return new ProxySubject(new RealSubject());  
 }  
}  

複製程式碼

清單4:生成靜態代理類工廠

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");  
 }   

} 

複製程式碼

靜態代理類優缺點

優點:業務類只需要關注業務邏輯本身,保證了業務類的重用性。這是代理的共有優點。 缺點: 1)代理物件的一個介面只服務於一種型別的物件,如果要代理的方法很多,勢必要為每一種方法都進行代理,靜態代理在程式規模稍大時就無法勝任了。 2)如果介面增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。

三、動態代理

動態代理類的原始碼是在程式執行期間由JVM根據反射等機制動態的生成,所以不存在代理類的位元組碼檔案。代理類和委託類的關係是在程式執行時確定。

1、先看看與動態代理緊密關聯的Java API。 1)java.lang.reflect.Proxy 這是 Java 動態代理機制生成的所有動態代理類的父類,它提供了一組靜態方法來為一組介面動態地生成代理類及其物件。

Proxy類的靜態方法

// 方法 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)   

複製程式碼

java.lang.reflect.InvocationHandler 這是呼叫處理器介面,它自定義了一個 invoke 方法,用於集中處理在動態代理類物件上的方法呼叫,通常在該方法中實現對委託類的代理訪問。每次生成動態代理類物件時都要指定一個對應的呼叫處理器物件。

清單7:InvocationHandler的核心方法

// 該方法負責集中處理動態代理類上的所有方法呼叫。第一個引數既是代理類例項,第二個引數是被呼叫的方法物件  
// 第三個方法是呼叫引數。呼叫處理器根據這三個引數進行預處理或分派到委託類例項上反射執行  
Object invoke(Object proxy, Method method, Object[] args)  

複製程式碼

java.lang.ClassLoader

具體步驟是: a. 實現InvocationHandler介面建立自己的呼叫處理器 b. 給Proxy類提供ClassLoader和代理介面型別陣列建立動態代理類 c. 以呼叫處理器型別為引數,利用反射機制得到動態代理類的建構函式 d. 以呼叫處理器物件為引數,利用動態代理類的建構函式建立動態代理類物件

清單8:分步驟實現動態代理

// 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對上面具體步驟的後三步做了封裝,簡化了動態代理物件的獲取過程。

清單9:動態代理實現示例

/** 
 * 動態代理類對應的呼叫處理程式類 
 */  
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物件作為方法執行結果。  
  //因為示例程式沒有返回值,所以這裡忽略了返回值處理  
  method.invoke(delegate, args);  
  long ftime = System.currentTimeMillis();   
  System.out.println("執行任務耗時"+(ftime - stime)+"毫秒");  

  return null;  
 }  
}   

複製程式碼

動態代理機制特點

首先是動態生成的代理類本身的一些特點。 1)包:如果所代理的介面都是 public 的,那麼它將被定義在頂層包(即包路徑為空),如果所代理的介面中有非 public 的介面(因為介面不能被定義為 protect 或 private,所以除 public 之外就是預設的 package 訪問級別),那麼它將被定義在該介面所在包(假設代理了 com.ibm.developerworks 包中的某非 public 介面 A,那麼新生成的代理類所在的包就是 com.ibm.developerworks),這樣設計的目的是為了最大程度的保證動態代理類不會因為包管理的問題而無法被成功定義並訪問;

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

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

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

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

接下來讓我們瞭解一下代理類例項的一些特點。每個例項都會關聯一個呼叫處理器物件,可以通過 Proxy 提供的靜態方法 getInvocationHandler 去獲得代理類例項的呼叫處理器物件。在代理類例項上呼叫其代理的介面中所宣告的方法時,這些方法最終都會由呼叫處理器的 invoke 方法執行,此外,值得注意的是,代理類的根類 java.lang.Object 中有三個方法也同樣會被分派到呼叫處理器的 invoke 方法執行,它們是 hashCode,equals 和 toString,可能的原因有:一是因為這些方法為 public 且非 final 型別,能夠被代理類覆蓋;二是因為這些方法往往呈現出一個類的某種特徵屬性,具有一定的區分度,所以為了保證代理類與委託類對外的一致性,這三個方法也應該被分派到委託類執行。當代理的一組介面有重複宣告的方法且該方法被呼叫時,代理類總是從排在最前面的介面中獲取方法物件並分派給呼叫處理器,而無論代理類例項是否正在以該介面(或繼承於該介面的某子介面)的形式被外部引用,因為在代理類內部無法區分其當前的被引用型別。

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

最後再來了解一下異常處理方面的特點。從呼叫處理器介面宣告的方法中可以看到理論上它能夠丟擲任何型別的異常,因為所有的異常都繼承於 Throwable 介面,但事實是否如此呢?答案是否定的,原因是我們必須遵守一個繼承原則:即子類覆蓋父類或實現父介面的方法時,丟擲的異常必須在原方法支援的異常列表之內。所以雖然呼叫處理器理論上講能夠,但實際上往往受限制,除非父介面中的方法支援拋 Throwable 異常。那麼如果在 invoke 方法中的確產生了介面方法宣告中不支援的異常,那將如何呢?放心,Java 動態代理類已經為我們設計好了解決方法:它將會丟擲 UndeclaredThrowableException 異常。這個異常是一個 RuntimeException 型別,所以不會引起編譯錯誤。通過該異常的 getCause 方法,還可以獲得原來那個不受支援的異常物件,以便於錯誤診斷。

5、動態代理的優點和美中不足

優點:

動態代理與靜態代理相比較,最大的好處是介面中宣告的所有方法都被轉移到呼叫處理器一個集中的方法中處理(InvocationHandler.invoke)。這樣,在介面方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣每一個方法進行中轉。在本示例中看不出來,因為invoke方法體內嵌入了具體的外圍業務(記錄任務處理前後時間並計算時間差),實際中可以類似

美中不足:

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

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

更多閱讀

跳槽可以,但請保持正確姿勢!

react-native技術的優劣

金三銀四跳槽季,阿里面試剛回來的總結—乾貨!

相信自己,沒有做不到的,只有想不到的

微信公眾號:終端研發部

技術

相關文章