設計模式——代理模式
在現實生活中,一個物件不能直接訪問另一個物件,這時需要找中介來訪問目標物件,此時的中介就是代理物件。例如:租房子時,我們無法與房東取得聯絡,只能通過某網站與中介進行交易,獲取自己心儀的房間等等。在軟體設計中,使用代理模式的例子也很多,例如:訪問阿里的 maven 倉庫,其就是海外 maven 倉庫的代理。還有因為安全原因需要遮蔽客戶端直接訪問真是物件,如某單位的內部資料等。
一、代理模式基本介紹
【1】代理模式:為一個物件提供一個替身,以控制對目標物件的訪問。即通過代理物件訪問目標物件。這樣做的好處是:可以在目標物件實現的基礎上,增強額外的功能操作,及擴充套件目標物件的功能。
【2】被代理的物件可以是遠端物件,建立開銷大的物件或需要安全控制的物件。
【3】代理模式有不同的形式,主要有三種:靜態代理、動態代理(又稱JDK代理、介面代理)和 Cglib 代理(可以在記憶體動態的建立物件,目標物件也不需要實現介面,它也屬於動態代理的範疇,但比較特殊)
【4】代理模式的主要優點:
【5】代理模式的主要缺點:①、在客戶端和目標物件之間增加一個代理物件,會造成請求處理速度變慢。②、增加了系統的複雜度。
二、靜態代理
靜態代理在使用時,需要定義介面或父類,被代理物件與代理物件一起實現相同的介面或者繼承相同的父類。
靜態代理 類圖 如下:
【1】抽象主題(Subject)類:通過介面或抽象類宣告真實主題和代理物件實現的業務方法。
【2】真實主題(Real Subject)類:實現了出現主題中的具體業務,是代理物件所代表的真實物件,是最終要引用的物件。
【3】代理(Proxy)類:
靜態代理 程式碼 例項:
【1】抽象主題類:代理類與被代理類都需要繼承的介面
1 //購票介面 2 public interface Ticketing { 3 //購票 4 public String buy(); 5 }
【2】真實主題類:目標類
1 //火車售票官方系統 2 public class RailwaySite implements Ticketing{ 3 @Override 4 public String buy() { 5 String ticket = " 呼叫官方系統購票,票價=120 ";6 System.out.println(); 7 return ticket; 8 } 9 }
【3】代理類:需要實現被代理類的介面,使用代理方法呼叫目標物件的方法,同時實現對目標方法的擴充套件。
1 //實現與目標系統一致的介面 2 public class ProxyTicketSystem implements Ticketing{ 3 //組合 被代理物件 4 private Ticketing ticket; 5 //構造器 6 public ProxyTicketSystem(Ticketing ticket) { 7 this.ticket = ticket; 8 } 9 10 @Override 11 public String buy() { 12 System.out.println("代理(智行火車票 系統啟動"); 13 String ticketInfo = ticket.buy(); 14 ticketInfo+="第三方系統服務費 20 總計:140元"; 15 System.out.println("代理(智行火車票 系統結束"); 16 return ticketInfo; 17 } 18 19 }
【4】客戶端:需要建立被代理物件和代理物件,並進行組合呼叫。
1 public class Client { 2 public static void main(String[] args) { 3 //被代理類 4 RailwaySite railwaySite = new RailwaySite(); 5 //獲取代理類 6 ProxyTicketSystem proxy = new ProxyTicketSystem(railwaySite); 7 //呼叫代理方法 8 String buy = proxy.buy(); 9 /** 10 * 代理(智行火車票 系統啟動 11 * 代理(智行火車票 系統結束 12 * 呼叫官方系統購票,票價=120 第三方系統服務費 20 總計:140元 13 */ 14 System.out.println(buy); 15 } 16 }
靜態代理的 優缺點:
【1】優點:在不修改目標物件的功能前提下,能通過代理物件對目標功能擴充套件。
【2】缺點:因為代理物件需要與目標物件實現一樣的介面,所以會有很多代理類。
【3】一旦介面增加方法,代理物件與目標物件都要維護。
三、動態代理
動態代理基本介紹:1)、代理物件,不需要實現介面,但是目標物件要實現介面,否則不能使用動態代理。
2)、代理物件的生成,是利用 JDK 的 API,動態的在記憶體中構建代理物件。
3)、動態代理又叫:JDK 代理、介面代理。
JDK 中生成代理物件的 API:代理類所在包:java.lang.reflect.Proxy JDK 實現代理只需要使用 newProxyInstance方法,但是該方法需要接收三個引數,完整的寫法是:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
動態代理類圖如下:與動態代理 程式碼例項相互參考實現。與靜態最大的不同在於代理類的不同。代理類無需實現目標類的介面,同時組合的是 Object 通用物件,無需組合目標介面物件,適合所有類(實現了介面)的代理方式,非常通用。
動態代理程式碼例項:【1】抽象主題類:被代理類(目標類)都需要繼承的介面
1 public interface ITicket { 2 //購票 3 public String buy(); 4 }
【2】真實主題類:目標類
1 public class RailTicketImpl implements ITicket{ 2 3 @Override 4 public String buy() { 5 String ticket = " 呼叫官方系統購票,票價=120 "; 6 System.out.println(); 7 return ticket; 8 } 9 }
【3】代理類:也是動態代理與靜態的區別之處,動態代理主要通過 JDK的 Proxy.newProxyInstance方法返回代理物件,且呼叫 method的 invoke內建方法,並將其結果返回。代理類實現了與目標類的解耦,適合為實現任意介面的所有類做代理。
1 //代理類 能夠實現所有類(必須實現介面)的代理 2 public class ProxyTicket { 3 //注入目標類的介面 4 private Object target; 5 //構造器 6 public ProxyTicket(Object target) { 7 super(); 8 this.target = target; 9 } 10 11 public Object getInstance() { 12 // ClassLoader loader =指定當前目標物件使用的類載入器,獲取載入器的方法固定 13 // Class<?>[] interfaces = 目標類實現的介面 使用泛型方法確認型別 14 //InvocationHandler h = 事件處理,執行目標物件的方法,會觸發事件處理器方法,會把當前執行的目標物件方法作為引數傳入 15 return Proxy.newProxyInstance(target.getClass().getClassLoader(), 16 target.getClass().getInterfaces(), new InvocationHandler() { 17 //Object proxy 傳入代理物件 18 //method 代理的方法 19 // args 代理引數 20 @Override 21 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 22 System.out.println("代理物件 方法入口"); 23 //呼叫代理方法時 傳入目標物件和引數 24 Object invoke = method.invoke(target, args); 25 return invoke; 26 } 27 }); 28 } 29 }
【4】客戶端類:目標物件和代理物件的返回值都必須使用介面接收,否則會出現轉換異常。這也是為什麼目標類必須實現介面的原因。
1 public class Client { 2 public static void main(String[] args) { 3 //定義目標類(被代理類) 4 ITicket railTicketImpl = new RailTicketImpl(); 5 //建立代理類 6 ProxyTicket proxyTicket = new ProxyTicket(railTicketImpl); 7 //獲取代理物件 需要強轉物件型別 8 ITicket ticket = (ITicket)proxyTicket.getInstance(); 9 //呼叫目標方法,使用 debugger 除錯時會發現其呼叫了 invoke 方法 10 ticket.buy(); 11 } 12 }
【JDK代理為什麼不能直接對非介面的類進行代理】 :由於 Java的單繼承,動態生成的代理類已經繼承了 Proxy類的,就不能再繼承其他的類,所以只能靠實現被代理類的介面的形式,故 JDK的動態代理必須有介面。
四、Cglib 代理
1)、靜態代理和動態代理都要求目標物件是實現一個介面,但是有時候目標物件只是一個單獨的物件,並沒有實現任何的介面,這個時候可使用目標物件子類來實現代理——這就是Cglib 代理。
2)、Cglib 代理也叫子類代理,它是在記憶體中構建一個子類物件從而實現對目標物件功能擴充套件,有些書也將 Cglib 代理歸屬於動態代理。
3)、Cglib 是一個非常強大的高效能的程式碼生成包,它可以在執行期擴充套件 java 類與實現java 介面。它廣泛的被許多 AOP 框架使用,例如:Spring AOP,實現方法的攔截。
4)、在 AOP程式設計中如何選擇代理:目標物件需要實現介面,用 JDK 代理。目標物件不需要實現介面,用 Cglib 代理。
5)、Cglib 包在底層是通過使用位元組碼處理框架 ASM 來轉換位元組碼並生成新的類。
【1】Cglib 依賴的 jar 包:
【2】在記憶體中動態構建子類,需要注意代理的類不能為 final ,否則會出現:java.lang.IllegalArgumentException 錯誤。
【3】目標方法不能使用 final/static 修飾,否則不會被攔截,即不會執行。
【動態代理 類圖 如下】:
【動態代理程式碼例項如下】:
【1】被代理類:無需實現介面,很平常的一個類
1 public class RailTicketImpl{ 2 public String buy() { 3 String ticket = " 呼叫官方系統購票,票價=120 "; 4 System.out.println(ticket); 5 return ticket; 6 } 7 }
【2】代理類:需要實現 jar 包中的 MethodInterceptor 介面,重寫 intercept 方法,此方法用於攔截代理物件的方法呼叫。同時需要建立一個代理物件返回方法:getProxyInstall() 可自行定義,其內部通過工具類 enhancer.create() 建立並返回代理物件,代理物件中需要傳入父類即目標類等等引數。
1 public class ProxyFactory implements MethodInterceptor{ 2 //組合目標物件 3 private Object target; 4 //構造器 5 public ProxyFactory(Object target) { 6 this.target = target; 7 } 8 //寫一個返回代理物件的方法:target 的代理類 9 public Object getProxyInstall() { 10 //1.建立一個工具類 11 Enhancer enhancer = new Enhancer(); 12 //2.將目標類設定為 父類 , 因為我們建立的是子類 13 enhancer.setSuperclass(target.getClass()); 14 //3. 設定回撥函式 15 enhancer.setCallback(this); 16 //4. 建立子類物件,即代理物件 17 return enhancer.create(); 18 } 19 20 //重寫 intercept 方法,會呼叫目標物件的方法 21 @Override 22 public Object intercept(Object obj, Method method, Object[] args, MethodProxy arg3) throws Throwable { 23 System.out.println("Cglib代理模式 ~~ 開始"); 24 Object returnVal = method.invoke(target, args); 25 System.out.println("Cglib代理模式 ~~ 提交"); 26 return returnVal; 27 } 28 }
【3】客戶端類:建立目標類和代理類,並呼叫目標方法,跟蹤程式碼會發現呼叫了 intercept方法,也就是方法會被 intercept 攔截。此時代理物件的返回值,就可以為目標類。
1 public class Client { 2 public static void main(String[] args) { 3 //目標類 4 RailTicketImpl ticket = new RailTicketImpl(); 5 //代理工廠類 6 ProxyFactory proxyFactory = new ProxyFactory(ticket); 7 //獲取代理類 需要強轉型別 8 RailTicketImpl proxyInstall = (RailTicketImpl)proxyFactory.getProxyInstall(); 9 //呼叫目標方法 10 proxyInstall.buy(); 11 /** 12 * 輸入如下: 13 * Cglib代理模式 ~~ 開始 14 * 呼叫官方系統購票,票價=120 15 * Cglib代理模式 ~~ 提交 16 */ 17 } 18 }
五、代理模式的變體(瞭解)
【1】防火牆(Firewall)代理:內網通過代理穿透防火牆,實現對公網的訪問。
【2】快取(Cache)代理:例如,當請求圖片或檔案等資源時,先到快取代理取,如果取到資源則返回,如果取不到資源,再到公網或者資料庫中取,然後快取。
【3】遠端(Remote)代理:可以把遠端物件在本地 cope 一份來呼叫。遠端代理通過網路和真正的遠端物件同步。
【4】同步(Synchronization)代理:主要使用在多執行緒程式設計中,完成多執行緒間同步工作。
【5】智慧引用(Smart Reference)代理 等等