Java設計模式(八)----代理模式
代理模式
1.生活中:
代理就是一個人或者一個組織代表其他人去做一件事的現實生活中的。在一些情況下,一個客戶不想或者不能夠直接引用一個對象,而代理對象可以在用戶端和目標對象之間起到中介的作用。
2.官方:
代理模式是對象的結構模式。代理模式給某一個對象提供一個代理對象,並由代理對象控制對原對象的引用
一、靜態代理
類圖結構如下
在代理模式中的角色:
●抽象主題角色:聲明了目標對象和代理對象的共同介面,這樣一來在任何可以使用目標對象的地方都可以使用代理對象。
●真實主題角色:定義了代理對象所代表的目標對象。
●代理主題角色:代理對象內部含有目標對象的引用,從而可以在任何時候操作目標對象;代理對象提供一個與目標對象相同的介面,以便可以在任何時候替代目標對象。代理對象通常在用戶端調用傳遞給目標對象之前或之後,執行某個操作,而不是單純地將調用傳遞給目標對象。它可以增加一些真實主題裡面沒有的功能。
生活中的例子:過年加班比較忙,沒空去買火車票,這時可以打個電話到附近的票務中心,叫他們幫你買張回家的火車票,當然這會附加額外的勞務費。但要清楚票務中心自己並不賣票,只有火車站才真正賣票,票務中心賣給你的票其實是通過火車站實現的。這點很重要!
上面這個例子,你就是“客戶”,票務中心就是“代理角色”,火車站是“真實角色”,賣票稱為“抽象角色”!
代碼
抽象主題角色
//抽象角色:聲明真實對象和代理對象的共同介面;public interface TicketManager { /** * 售票 */ public void soldTicket(); /** * 改簽 */ public void changeTicket(); /** * 退票 */ public void returnTicket();}
真實主題角色
public class TicketManagerImpl implements TicketManager { @Override public void soldTicket() { //checkIdentity(); System.out.println("售票"); } @Override public void changeTicket(){ //checkIdentity(); System.out.println("改簽"); } @Override public void returnTicket() { //checkIdentity(); System.out.println("退票"); } /** * 身分識別驗證 */ public void checkIdentity(){ System.out.println("身分識別驗證"); }}
代理主題角色(添加了身分識別驗證功能)
public class StaticProxyTicketManager implements TicketManager { TicketManager ticketManager;//目標對象的引用 public StaticProxyTicketManager(TicketManager ticketManager) { this.ticketManager = ticketManager; } @Override public void soldTicket() { checkIdentity(); ticketManager.soldTicket(); } @Override public void changeTicket() { checkIdentity(); ticketManager.changeTicket(); } @Override public void returnTicket() { checkIdentity(); ticketManager.changeTicket(); } /** * 身分識別驗證 */ public void checkIdentity(){ System.out.println("身分識別驗證--------------"); }}
第二個代理主題角色(添加了日誌功能)
//代理類 實現同一個介面public class LogProxy implements TicketManager { TicketManager ticketManager;//目標類的引用 public LogProxy(TicketManager ticketManager){ this.ticketManager=ticketManager; } @Override public void soldTicket() { ticketManager.soldTicket(); log();//後置增強 } @Override public void changeTicket() { ticketManager.changeTicket(); log(); } @Override public void returnTicket() { ticketManager.returnTicket(); log(); } //增強 private void log() { System.out.println("日誌..."); }}
用戶端
public class Test { public static void main(String[] args) { //裝飾模式 new TicketManagerImpl() 真實的目標對象 //TicketManager tm=new StaticProxyTicketManager(new TicketManagerImpl()); TicketManager tm=new LogProxy(new StaticProxyTicketManager(new TicketManagerImpl())); tm.soldTicket(); tm.changeTicket(); tm.returnTicket(); }}
結果:
身分識別驗證————–
售票
日誌…
身分識別驗證————–
改簽
日誌…
身分識別驗證————–
改簽
日誌…
從上面例子可以看出 用戶端通過代理來購票 而代理實際上不能賣票給客戶,他實際上是通過目標對象賣票給客戶的,也就是說他是通過真實主題的目標對象實現給用戶端賣票的功能,他只是一個中介,但我們可以在它裡面增加一些功能,比如身分識別驗證或者宣傳打廣告等其他的功能。
靜態代理類:在程式運行前,代理類的.class檔案就已經存在了,已確定被代理的對象
靜態代理:
優點:對真實對象進行封裝,不會修改目標類的代碼。
缺點:
1.多個不同類型目標對象需要代理時,我就需要建立多個代理類,造成類的膨脹
2.代碼的冗餘
3.編譯期加入,不夠靈活
二、動態代理
描述(這個描述從網上看到的,相對比較容易理解)
動態代理(Dynamic Proxy):相比靜態代理,動態代理具有更強的靈活性,因為它不用在我們設計實現的時候就指定某一個代理類來代理哪一個被代理對象,我們可以把這種指定延遲到程式運行時由JVM來實現。
所謂代理,就是需要代理類和被代理類有相同的對外介面或者說成服務,所以代理類一般都必須實現了所有被代理類已實現的介面,因為介面就是制定了一系列對外服務的標準。
1.JDK實現動態代理
正因為動態代理有這樣靈活的特性,所以我們在設計動態代理類(DynamicProxy)時不用顯式地讓它實現與真實主題類(RealSubject)相同的介面(interface),而是把這種實現延遲到運行時。
為了能讓DynamicProxy類能夠在運行時才去實現RealSubject類已實現的一系列介面並執行介面中相關的方法操作,需要讓 DynamicProxy類實現JDK內建的java.lang.reflect.InvocationHandler介面,該介面中的invoke() 方法能夠讓DynamicProxy執行個體在運行時調用被代理類的“對外服務”,即調用被代理類需要對外實現的所有介面中的方法,也就是完成對真實方法的調 用,Java協助文檔中稱這些真實方法為處理常式。
按照上面所述,我們肯定必須先把被代理類RealSubject已實現的所有interface都載入到JVM中,不然JVM怎麼能夠找到這些方法呢?明白了這個道理,那麼我們就可以建立一個被代理類的執行個體,獲得該執行個體的類載入器ClassLoader。
所謂的類載入器ClassLoader,就是具有某個類的類定義,即類的內部相關結構(包括繼承樹、方法區等等)。
更重要的是,動態代理模式可以使得我們在不改變原來已有的代碼結構的情況下,對原來的“真實方法”進行擴充、增強其功能,並且可以達到控制被代理對 象的行為的目的。請詳看下面代碼中的DynamicProxy類,其中必須實現的invoke()方法在調用被代理類的真實方法的前後都可進行一定的特殊 操作。這是動態代理最明顯的優點
類圖
代碼
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class DynamicProxyTicketManager implements InvocationHandler { private Object targetObject; /** * 目標的初始化方法,根據目標組建代理程式類 * * @param targetObject * @return */ public Object newProxyInstance(Object targetObject) { this.targetObject = targetObject; // 第一個參數,目標對象 的裝載器 // 第二個參數,目標介面已實現的所有介面,而這些是動態代理類要實現的介面列表 // 第三個參數, 調用實現了InvocationHandler的對象產生動態代理執行個體,當你一調用代理,代理就會調用InvocationHandler的invoke方法 return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this); } /** * 反射,這樣你可以在不知道具體的類的情況下,根據配置的參數去調用一個類的方法。在靈活編程的時候非常有用。 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 檢查 checkIdentity(); Object ret = null; try { // 調用目標方法 ret = method.invoke(targetObject, args); // 執行成功,列印成功資訊 log(); } catch (Exception e) { e.printStackTrace(); // 失敗時,列印失敗資訊 System.out.println("error-->>" + method.getName()); throw e; } return ret; } /** * 身分識別驗證 */ public void checkIdentity(){ System.out.println("身分識別驗證--------------"); } public void log(){ System.out.println("日誌..." ); }}
用戶端
public class Test { public static void main(String[] args) { DynamicProxyTicketManager dynamicProxyTicketManager=new DynamicProxyTicketManager(); TicketManager tm=(TicketManager) dynamicProxyTicketManager.newProxyInstance(new TicketManagerImpl()); tm.soldTicket(); tm.changeTicket(); tm.returnTicket(); }}
結果同上
優缺點
優點:
1、一個動態代理類更加簡單了,可以解決建立多個靜態代理的麻煩,避免不斷的重複多餘的代碼
2、調用目標代碼時,會在方法“運行時”動態加入,決定你是什麼類型,才調誰,靈活
缺點:
1、系統靈活了,但是相比而言,效率降低了,比靜態代理慢一點
2、動態代理比靜態代理在代碼的可讀性上差了一點,不太容易理解
3、JDK動態代理只能對實現了介面的類進行代理
總結
各有各的好,具體情況具體討論
2.Cglib實現動態代理
描述(網上整理)
AOP的源碼中用到了兩種動態代理來實現攔截切入功能:jdk動態代理和cglib動態代理。
兩種方法同時存在,各有優劣。jdk動態代理是由java內部的反射機制來實現的 ,cglib動態代理底層則是藉助asm來實現的。總的來說,反射機制在產生類的過程中比較高效,而asm在產生類之後的相關執行過程中比較高效(可以通 過將asm產生的類進行緩衝,這樣解決asm產生類過程低效問題)。還有一點必須注意:jdk動態代理的應用前提,必須是目標類基於統一的介面。如果沒有 上述前提,jdk動態代理不能應用。由此可以看出,jdk動態代理有一定的局限性,cglib這種第三方類庫實現的動態代理應用更加廣泛, 且在效率上更有優勢。
JDK的動態代理機制只能代理實現了介面的類,否則不能實現JDK的動態代理,cglib是針對類來實現代理的,他的原理是對指定的目標類產生一個子類,並覆蓋其中方法實現增強,但因為採用的是繼承,所以不能對final修飾的類進行代理。
介紹:
CGLIB的核心類:
net.sf.cglib.proxy.Enhancer – 主要的增強類
net.sf.cglib.proxy.MethodInterceptor – 主要的方法攔截類,它是Callback介面的子介面,需要使用者實現
net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method類的代理類,可以方便的實現對來源物件方法的調用,如使用:
Object o = methodProxy.invokeSuper(proxy, args);//雖然第一個參數是被代理對象,也不會出現死迴圈的問題。
net.sf.cglib.proxy.MethodInterceptor介面是最通用的回調(callback)類型,它經常被基於代理的AOP用來實現攔截(intercept)方法的調用。這個介面只定義了一個方法
public Object intercept(Object object, java.lang.reflect.Method method,
Object[] args, MethodProxy proxy) throws Throwable;
第一個參數是代理對像,第二和第三個參數分別是攔截的方法和方法的參數。原來的方法可能通過使用java.lang.reflect.Method 對象的一般反射調用,或者使用 net.sf.cglib.proxy.MethodProxy對象調用。net.sf.cglib.proxy.MethodProxy通常被首選使 用,因為它更快
代碼
public class CglibDynamicProxyTicketManager implements MethodInterceptor { private Object targetObject;//目標對象 /** * 建立代理對象 * * @param targetObject * @return */ public Object getInstance(Object targetObject) { this.targetObject = targetObject; Enhancer enhancer = new Enhancer(); // 用這個類來建立代理對象(被代理類的子類): 並設定父類;設定回調; enhancer.setSuperclass(this.targetObject.getClass()); // 設定被代理類作為其父類的代理目標 // 回調方法 enhancer.setCallback(this); // 設定回調--當這個代理對象的方法被調用時 回調方法intercept()會被執行 // 建立代理對象 return enhancer.create(); } @Override //回調方法 // methodProxy 代理的類的方法 /** * methodProxy 會調用父類(目標對象)的被代理的方法,比如soldTicket方法等 */ public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object result = null; checkIdentity();//前置增強 result=methodProxy.invokeSuper(obj, args); //調用新產生的cglib的代理對象 所屬的父類的被代理的方法 log();//後置增強 return result; } /** * 身分識別驗證 */ public void checkIdentity(){ System.out.println("身分識別驗證--------------"); } public void log(){ System.out.println("日誌..." ); }}
用戶端
public class Test { public static void main(String[] args) { CglibDynamicProxyTicketManager cglibdynamicProxyTicketManager=new CglibDynamicProxyTicketManager(); //組建代理程式對象 TicketManager tm=(TicketManager) cglibdynamicProxyTicketManager.getInstance(new TicketManagerImpl()); tm.soldTicket();//當調用代理對象的被代理對象的方法時 會自動回調 代理類中的Intercept()方法 tm.changeTicket(); tm.returnTicket(); }}
結果同上