目錄
代理模式
代理模式簡介
代理模式的分類
代理模式組成
代理模式的優缺點
靜態代理
背景前置
編寫代碼
JDK動態代理
編寫代碼
使用Arthas分析JDK動態代理底層原理
CGLIB動態代理
編寫代碼
三種代理的對比
?代理模式使用場景
代理模式
代理模式簡介
????????代理模式屬于結構型模式。指一個對象本身不做實際的操作,而是通過其他對象來獲取自己想要的結果。? ? ? ?
????????產生背景:由于某些原因需要給某對象提供一個代理以控制對該對象的訪問。這時,訪問對象不適合或者不能直接引用目標對象,代理對象作為訪問對象和目標對象之間的中介。
? ? ? ? 意義:目標對象只需要關注自己的實現細節,通過代理來實現功能的增強,可以擴展目標對象的功能。同時體現了非常重要的變成模式,不能隨便修改目標對象的源碼,如果需要修改目標對象的源碼對已有功能進行增強,此時可以通過修改代理的方式實現功能的擴展。
? ? ? ? 例子:如果某人需要租房,此時可以借助于房屋中介或租賃公司,此時房屋中介或租賃公司相當于代理,可以讓租房人找到適合自己的房屋。
代理模式的分類
? ? ? ? 靜態代理:靜態代理就是在程序運行之前,代理類字節碼.class
就已編譯好,通常一個靜態代理類也只代理一個目標類,代理類和目標類都實現相同的接口。
? ? ? ? 動態代理:動態代理類與靜態代理類最主要不同的是,代理類的字節碼不是在程序運行前生成的,而是在程序運行時在虛擬機中程序自動創建的。
代理模式組成
? ? ? ? 代理(Proxy)模式可以分為三種角色:
-
抽象主題(Subject)類: 通過接口或抽象類聲明真實主題和代理對象實現的業務方法。
-
真實主題(Real Subject)類: 實現了抽象主題中的具體業務,是代理對象所代表的真實對象,是最終要引用的對象。
-
代理(Proxy)類 : 提供了與真實主題相同的接口,其內部含有對真實主題的引用,它可以訪問、控制或擴展真實主題的功能。
代理模式的優缺點
? ? ? ? 優點:保護、擴展、降低耦合度。
-
代理模式在客戶端與目標對象之間起到一個中介作用和保護目標對象的作用;
-
代理對象可以擴展目標對象的功能;
-
代理模式能將客戶端與目標對象分離,在一定程度上降低了系統的耦合度;
? ? ? ? 缺點:增加系統的復雜性。
靜態代理
背景前置
????????火車站賣票:如果要買火車票的話,需要去火車站買票,坐車到火車站,排隊等一系列的操作,顯然比較麻煩。而火車站在多個地方都有代售點,我們去代售點買票就方便很多了。這個例子其實就是典型的代理模式,火車站是目標對象,代售點是代理對象。類圖如下:
編寫代碼
? ? ? ? 如上面背景所示,我們需要定義①抽象主題類:買票接口類。②真實主題類:代理的目標類(火車站)。③代理類:代售點,以實現對真實主題類功能的增強。
/*** 聲明抽象類、定義公共方法*/
public interface SallTicket {void sale();
}/**
* 火車站類,實現買票接口
*/
public class TrainStation implements SallTicket{@Overridepublic void sale() {System.out.println("火車站正在賣票...");}
}/**
* 代理類:與目標類實現相同的接口,以達到對目標類方法的增強
*/public class SallProxy implements SallTicket{private TrainStation trainStation;public SallProxy(TrainStation trainStation){this.trainStation = trainStation;}@Overridepublic void sale() {System.out.println("代售點收取少量服務費...");trainStation.sale();}
}/*** 編寫客戶端測試類*/
public class Client {public static void main(String[] args) {SallProxy sallProxy = new SallProxy(new TrainStation());sallProxy.sale();}
}
接口、目標類、代理類之間的關系圖:
- 接口類SallTicket:定義了賣票的抽象公共方法sale()。
- 目標類TrainStation:實現了接口類SallTicket,并重寫方法sale()。
- 代理類SallProxy:實現接口類SallTicket,且在方法內部傳遞了接口類的實現類也即目標類TrainStation。在代理類內部實現了原有賣票方法sale()的功能的增強,并且調用目標類TrainStation中的sale()方法達到了賣票的功能。
- 測試類Client:構建代理類,并實現功能的驗證。
JDK動態代理
編寫代碼
????????JDK動態代理和靜態代理不同的地方在于代理類的編寫不同,因此接口類、目標類仍然沿用靜態代理中創建的,編寫JDK動態代理程序結構如下:
????????其中代理類(ProxyFactory)和測試類(Client)代碼如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/*** 獲取代理對象的工廠類* 代理類也實現了對應的接口*/
public class ProxyFactory {// 聲明目標對象private TrainStation station = new TrainStation();// 獲取代理對象的方法public SallTicket getProxyObject(){// 返回代理對象、代理對象也實現了目標接口/*** ClassLoader loader, 類加載器,用于加載代理類,可以通過目標對象獲取類加載器* Class<?>[] interfaces, 代理類實現的接口的字節碼對象* InvocationHandler h, 代理對象的調用處理程序*/SallTicket proxyObject = (SallTicket) Proxy.newProxyInstance(station.getClass().getClassLoader(),station.getClass().getInterfaces(), // 目標對象和代理類實現了相同的接口,可以通過目標對象獲取代理類實現的接口的字節碼對象new InvocationHandler() { // 匿名內部類/*** @param proxy 代理對象,就是proxyObject* @param method 對接口中的方法進行封裝的method對象* @param args 調用方法的實際參數* @return 返回值就是方法的返回值* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 對目標對象進行方法的增強System.out.println("代售點收取一定的服務費用(JDK)...");// 執行目標對象的方法Object obj = method.invoke(station, args);return obj;}});return proxyObject;}
}/*** 客戶端,測試JDK動態代理對方法的增強*/
public class Client {public static void main(String[] args) {// 創建代理對象工廠ProxyFactory proxyFactory = new ProxyFactory();// 使用proxyFactory對象的方法獲取代理對象SallTicket proxyObject = proxyFactory.getProxyObject();// 調用目標對象的方法proxyObject.sale();}
}
???????接口、目標類、代理類之間的關系圖:?
? ? ? ? 上圖中的ProxyFactory類并不是真正的JDK動態代理類,其本質上是一個代理工廠,通過Proxy類的靜態方法newProxyInstance(...),傳入目標類的字節碼,代理類實現的接口,invocationHandler接口的實現內部類等參數在程序運行的時候動態的創建代理類,并且借助反射獲取代理對象的方法,并且實現對目標方法的增強。
使用Arthas分析JDK動態代理底層原理
? ? ? ? 在測試類Client中添加以下代碼用于獲取代理類的Class對象。
? ? ? ? 隨后借助于阿里開源工具Arthas,使用反編譯命令jad?com.sun.proxy.$Proxy0獲取已加載到 JVM 中的類的源代碼如下:
package com.sun.proxy;import com.itheima.proxy.dynamic.jdk.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy0 extends Proxy implements SellTickets {private static Method m1;private static Method m2;private static Method m3;private static Method m0;public $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);return;}catch (NoSuchMethodException noSuchMethodException) {throw new NoSuchMethodError(noSuchMethodException.getMessage());}catch (ClassNotFoundException classNotFoundException) {throw new NoClassDefFoundError(classNotFoundException.getMessage());}}public final boolean equals(Object object) {try {return (Boolean)this.h.invoke(this, m1, new Object[]{object});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final String toString() {try {return (String)this.h.invoke(this, m2, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final int hashCode() {try {return (Integer)this.h.invoke(this, m0, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final void sell() {try {this.h.invoke(this, m3, null);return;}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}
}
從上面的類中,我們可以看到以下幾個信息:
-
代理類($Proxy0)實現了SellTickets。
-
代理類($Proxy0)將我們提供了的匿名內部類對象傳遞給了父類。
? ? ? ? 可以分析代理類執行流程如下:
- 在測試類中通過代理對象調用sell()方法。
- 根據多態的特性,執行的是代理類($Proxy0)中的sall()方法。
- 代理類($Proxy0)中的sell()方法中又調用了InvocationHandler接口的子實現類對象的invoke方法。
- invoke方法通過反射執行了真實對象所屬類(TrainStation)中的sall()方法。
CGLIB動態代理
編寫代碼
????????CGLIB動態代理和靜態代理不同的地方在于代理類的編寫不同,并且不需要接口類,因此目標類仍然沿用靜態代理中創建的,編寫CGLIB動態代理程序結構如下:
? ? ? ? CGLIB是第三方提供的包,編寫代碼之前需要先引入jar包。
<dependency>
? ? <groupId>cglib</groupId>
? ? <artifactId>cglib</artifactId>
? ? <version>2.2.2</version>
</dependency>
/*** 代理工廠*/
public class ProxyFactory implements MethodInterceptor {private TrainStation station = new TrainStation();// 獲取代理對象public TrainStation getProxyObject(){// 創建Enhancer對象,類似于JDK動態代理的Proxy類Enhancer enhancer = new Enhancer();// 設置父類的字節碼對象enhancer.setSuperclass(station.getClass());// 設置回調函數,傳遞的對象,是MethodInterceptor的子實現類對象enhancer.setCallback(this);// 創建代理對象TrainStation proxyObject = (TrainStation) enhancer.create();return proxyObject;}/*** @param o 代理對象* @param method 真實對象中的方法的Method實例* @param objects 實際參數* @param methodProxy 代理對象中的方法的method實例* @return 代理對象方法的返回值* @throws Throwable*/@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("代理點收取一些服務費用(CGLIB動態代理方式)");TrainStation result = (TrainStation) method.invoke(station, objects);return result;}
}/*** 客戶端*/
public class Client {public static void main(String[] args) {// 創建代理對象工廠ProxyFactory proxyFactory = new ProxyFactory();// 獲取代理對象TrainStation proxyObject = proxyFactory.getProxyObject();proxyObject.sale();}
}
?????????接口、目標類、代理類之間的關系圖:
????????簡單原理:通過自定義實現攔截器接口(MethodInterceptor)的類【也就是目標類】,并重寫intercept()用于攔截增強被代理類的方法【類似于JDK動態代理中的invoke()方法】。通過Enhancer 類的 create()創建簡單的代理類。
????????CGLIB采用非常底層的字節碼技術,通過字節碼技術為一個類創建子類,并在子類中采用方法攔截的技術的攔截所有父類的方法調用,順勢織入橫切邏輯。(CGLIB在字節碼的基礎上,利用ASM開源包,將代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理)
三種代理的對比
動態代理與靜態代理相比較,最大的好處是接口中聲明的所有方法都被轉移到調用處理器一個集中的方法中處理(InvocationHandler.invoke)。這樣,在接口方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣每一個方法進行中轉。如果接口增加一個方法,靜態代理模式除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了代碼維護的復雜度。而動態代理不會出現該問題。
- JDK動態代理只能代理實現了接口的類,而CGLIB可以代理未實現任何接口的類。
- JDK動態代理是實現了被代理對象所實現的接口;CGLIB是繼承了被代理對象。
- JDK和CGLIB都是在運行期生成字節碼,JDK是直接寫Class字節碼,CGLIB是使用ASM框架寫Class字節碼。
- CGLib不能對聲明為final的類或者方法進行代理,因為CGLib原理是動態生成被代理類的子類。
- 在JDK1.6、JDK1.7、JDK1.8逐步對JDK動態代理優化之后,在調用次數較少的情況下,JDK代理效率高于CGLib代理效率,只有當進行大量調用的時候,JDK1.6和JDK1.7比CGLib代理效率低一點,但是到JDK1.8的時候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK動態代理,如果沒有接口使用CGLIB代理。
?代理模式使用場景
? ? ? ? 此部分內容暫時不是很熟悉,等以后學習到再慢慢補充。
-
遠程(Remote)代理
本地服務通過網絡請求遠程服務。為了實現本地到遠程的通信,我們需要實現網絡通信,處理其中可能的異常。為良好的代碼設計和可維護性,我們將網絡通信部分隱藏起來,只暴露給本地服務一個接口,通過該接口即可訪問遠程服務提供的功能,而不必過多關心通信部分的細節。
-
防火墻(Firewall)代理
當你將瀏覽器配置成使用代理功能時,防火墻就將你的瀏覽器的請求轉給互聯網;當互聯網返回響應時,代理服務器再把它轉給你的瀏覽器。
-
保護(Protect or Access)代理
控制對一個對象的訪問,如果需要,可以給不同的用戶提供不同級別的使用權限。