基本介紹代理模式是一種結構型設計模式。爲對象提供一個替身,以控制對這個對象的訪問。即通過代理對象訪問目標對象,並允許在將請求提交給對象前後進行一些處理。

被代理的對象可以是遠程對象、創建開銷大的對象或需要安全控制的對象。

代理模式主要有三種不同的形式:

靜態代理:由程序員創建代理類或特定工具自動生成源代碼再對其編譯。在程序運行前代理類的 .class 文件就已經存在了

動態代理(JDK 代理、接口代理):在程序運行時運用反射機制動態創建而成,動態就是在程序運行時生成的,而不是編譯時。

cglib 代理(可以在內存動態的創建對象,而不是實現接口,屬於動態代理的範疇)

問題爲什麼要控制對於某個對象的訪問呢?舉個例子:有這樣一個消耗大量系統資源的巨型對象, 你只是偶爾需要使用它, 並非總是需要。

圖:refactoringguru.cn

你可以實現延遲初始化:在實際有需要時再創建該對象。對象的所有客戶端都要執行延遲初始代碼。不幸的是, 這很可能會帶來很多重複代碼。

在理想情況下, 我們希望將代碼直接放入對象的類中, 但這並非總是能實現:比如類可能是第三方封閉庫的一部分。

解決方案代理模式建議新建一個與原服務對象接口相同的代理類, 然後更新應用以將代理對象傳遞給所有原始對象客戶端。代理類接收到客戶端請求後會創建實際的服務對象, 並將所有工作委派給它。

圖:refactoringguru.cn

代理將自己僞裝成數據庫對象, 可在客戶端或實際數據庫對象不知情的情況下處理延遲初始化和緩存查詢結果的工作。

這有什麼好處呢?如果需要在類的主要業務邏輯前後執行一些工作, 你無需修改類就能完成這項工作。由於代理實現的接口與原類相同, 因此你可將其傳遞給任何一個使用實際服務對象的客戶端。

代理模式結構

圖:refactoringguru.cn

服務接口 (Service Interface) 聲明瞭服務接口。代理必須遵循該接口才能僞裝成服務對象。

服務 (Service) 類提供了一些實用的業務邏輯。

代理 (Proxy) 類包含一個指向服務對象的引用成員變量。代理完成其任務 (例如延遲初始化、 記錄日誌、 訪問控制和緩存等) 後會將請求傳遞給服務對象。通常情況下, 代理會對其服務對象的整個生命週期進行管理。

客戶端 (Client) 能通過同一接口與服務或代理進行交互, 所以你可在一切需要服務對象的代碼中使用代理。

打遊戲有代練、買賣房子有中介代理、再比如一般公司投互聯網廣告也可以找代理公司,這裏的代練、中介、廣告代理公司扮演的角色都是代理。

這裏舉個更接近程序員的例子,比如有些變態的公司不允許在公司刷微博,看視頻,可以通過一層代理來限制我們訪問這些網站。

廢話不多說,先來個靜態代理。

靜態代理1、定義網絡接口

public interface Internet { void connectTo(String serverHost) throws Exception;}

2、真正的網絡連接

public class RealInternet implements Internet{ @Override public void connectTo(String serverHost) throws Exception { System.out.println("Connecting to "+ serverHost); }}

3、公司的網絡代理

public class ProxyInternet implements Internet { //目標對象,通過接口聚合 private Internet internet; // 通過構造方法傳入目標對象 public ProxyInternet(Internet internet){ this.internet = internet; } //網絡黑名單 private static List bannedSites; static { bannedSites = new ArrayList(); bannedSites.add("bilibili.com"); bannedSites.add("youtube.com"); bannedSites.add("weibo.com"); bannedSites.add("qq.com"); } @Override public void connectTo(String serverhost) throws Exception { // 添加限制功能 if(bannedSites.contains(serverhost.toLowerCase())) { throw new Exception("Access Denied:"+serverhost); } internet.connectTo(serverhost); }}

4、客戶端驗證

public class Client { public static void main(String[] args) { Internet internet = new ProxyInternet(new RealInternet()); try { internet.connectTo("so.com"); internet.connectTo("qq.com"); } catch (Exception e) { System.out.println(e.getMessage()); } }}

5、輸出

Connecting to so.comAccess Denied:qq.com

不能訪問娛樂性網站,但是可以用 360 搜索,SO 靠譜,哈哈

靜態代理類優缺點

優點:

在不修改目標對象的前提下,可以通過代理對象對目標對象功能擴展

代理使客戶端不需要知道實現類是什麼,怎麼做的,而客戶端只需知道代理即可(解耦合),對於如上的客戶端代碼,RealInterner() 可以應用工廠將它隱藏。

缺點:

代理類和委託類實現了相同的接口,代理類通過委託類實現了相同的方法。這樣就出現了大量的代碼重複。如果接口增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了代碼維護的複雜度。

代理對象只服務於一種類型的對象,如果要服務多類型的對象。勢必要爲每一種對象都進行代理,靜態代理在程序規模稍大時就無法勝任了。

動態代理靜態代理會產生很多靜態類,所以我們要想辦法可以通過一個代理類完成全部的代理功能,這就引出了動態代理。

JDK原生動態代理

代理對象,不需要實現接口,但是目標對象要實現接口,否則不能用動態代理

代理對象的生成,是通過 JDK 的 API(反射機制),動態的在內存中構建代理對象

在 Java 中要想實現動態代理機制,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 類的支持

Coding

1、網絡接口不變

public interface Internet { void connectTo(String serverHost) throws Exception;}

2、真正的網絡連接,也不會改變

public class RealInternet implements Internet{ @Override public void connectTo(String serverHost) throws Exception { System.out.println("Connecting to "+ serverHost); }}

3、動態代理,需要實現 InvocationHandler,我們用 Lambda 表達式簡化下

public class ProxyFactory { /** * 維護一個目標對象 **/ private Object target; /** * 構造器,初始化目標對象 **/ public ProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { /** 被代理對象target通過參數傳遞進來, 通過target.getClass().getClassLoader()獲取ClassLoader對象, 然後通過target.getClass().getInterfaces()獲取它實現的所有接口, 再將target包裝到實現了InvocationHandler接口的對象中。 通過newProxyInstance函數我們就獲得了一個動態代理對象。 */ return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(bannedSites.contains(args[0].toString().toLowerCase())) { throw new Exception("Access Denied:"+args[0]); } //反射機制調用目標對象的方法 Object obj = method.invoke(target, args); return obj; } }); } private static List bannedSites; static { bannedSites = new ArrayList(); bannedSites.add("bilibili.com"); bannedSites.add("youtube.com"); bannedSites.add("weibo.com"); bannedSites.add("qq.com"); }}

4、客戶端

public class Client { public static void main(String[] args) { Internet internet = new ProxyInternet(new RealInternet()); try { internet.connectTo("360.cn"); internet.connectTo("qq.com"); } catch (Exception e) { System.out.println(e.getMessage()); } }}

動態代理的方式中,所有的函數調用最終都會經過 invoke 函數的轉發,因此我們就可以在這裏做一些自己想做的操作,比如日誌系統、事務、攔截器、權限控制等。

cglib代理

靜態代理和 JDK 代理模式都要求目標對象實現一個接口,但有時候目標對象只是一個單獨的對象,並沒有實現任何接口,這個時候就可以使用目標對象子類來實現代理,這就是 cglib 代理。

cglib(Code Generation Library)是一個基於ASM的字節碼生成庫,它允許我們在運行時對字節碼進行修改和動態生成。cglib 通過繼承方式實現代理。它廣泛的被許多AOP的框架使用,比如我們的 Spring AOP。

cglib 包的底層是通過使用字節碼處理框架 ASM 來轉換字節碼並生成新的類。

cglib 代理也被叫做子類代理,它是在內存中構建一個子類對象從而實現目標對象功能擴展。

Coding

添加 cglib 依賴

cglib cglib 3.3.0

1、不需要接口

public class RealInternet{ public void connectTo(String serverHost) { System.out.println("Connecting to "+ serverHost); }}

2、代理工廠類

public class ProxyFactory implements MethodInterceptor { private Object target; public ProxyFactory(Object target){ this.target = target; } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("cglib 代理開始,可以添加邏輯"); Object obj = method.invoke(target,objects); System.out.println("cglib 代理結束"); return obj; } public Object getProxyInstance(){ //工具類,類似於JDK動態代理的Proxy類 Enhancer enhancer = new Enhancer(); //設置父類 enhancer.setSuperclass(target.getClass()); //設置回調函數 enhancer.setCallback(this); //創建子類對象,即代理對象 return enhancer.create(); }}

3、客戶端

public class Client { public static void main(String[] args) { //目標對象 RealInternet target = new RealInternet(); //獲取代理對象,並且將目標對象傳遞給代理對象 RealInternet internet = (RealInternet) new ProxyFactory(target).getProxyInstance(); internet.connectTo("so.cn"); }}

4、輸出

cglib 代理開始,可以添加邏輯Connecting to so.cncglib 代理結束

代理模式適合應用場景使用代理模式的方式多種多樣, 我們來看看最常見的幾種。

延遲初始化 (虛擬代理):如果你有一個偶爾使用的重量級服務對象, 一直保持該對象運行會消耗系統資源時, 可使用代理模式。

你無需在程序啓動時就創建該對象, 可將對象的初始化延遲到真正有需要的時候。

訪問控制 (保護代理):如果你只希望特定客戶端使用服務對象, 這裏的對象可以是操作系統中非常重要的部分, 而客戶端則是各種已啓動的程序 (包括惡意程序), 此時可使用代理模式。

代理可僅在客戶端憑據滿足要求時將請求傳遞給服務對象。

本地執行遠程服務 (遠程代理):適用於服務對象位於遠程服務器上的情形。

在這種情形中, 代理通過網絡傳遞客戶端請求, 負責處理所有與網絡相關的複雜細節。

記錄日誌請求 (日誌記錄代理):適用於當你需要保存對於服務對象的請求歷史記錄時。代理可以在向服務傳遞請求前進行記錄。

緩存請求結果 (緩存代理):適用於需要緩存客戶請求結果並對緩存生命週期進行管理時, 特別是當返回結果的體積非常大時。

代理可對重複請求所需的相同結果進行緩存, 還可使用請求參數作爲索引緩存的鍵值。比如請求圖片、文件等資源時,先到代理緩存取,如果沒有就去公網取並緩存到代理服務器

智能引用:可在沒有客戶端使用某個重量級對象時立即銷燬該對象。

代理會將所有獲取了指向服務對象或其結果的客戶端記錄在案。代理會時不時地遍歷各個客戶端, 檢查它們是否仍在運行。如果相應的客戶端列表爲空, 代理就會銷燬該服務對象, 釋放底層系統資源。

代理還可以記錄客戶端是否修改了服務對象。其他客戶端還可以複用未修改的對象。

AOP 中的代理模式AOP(面向切面編程)主要的的實現技術主要有 Spring AOP 和 AspectJ

AspectJ 的底層技術就是靜態代理,用一種 AspectJ 支持的特定語言編寫切面,通過一個命令來編譯,生成一個新的代理類,該代理類增強了業務類,這是在編譯時增強,相對於下面說的運行時增強,編譯時增強的性能更好。(AspectJ 的靜態代理,不像我們前邊介紹的需要爲每一個目標類手動編寫一個代理類,AspectJ 框架可以在編譯時就生成目標類的“代理類”,在這裏加了個冒號,是因爲實際上它並沒有生成一個新的類,而是把代理邏輯直接編譯到目標類裏面了)

Spring AOP 採用的是動態代理,在運行期間對業務方法進行增強,所以不會生成新類,對於動態代理技術,Spring AOP 提供了對 JDK 動態代理的支持以及 CGLib 的支持。

默認情況下,Spring對實現了接口的類使用 JDK Proxy方式,否則的話使用CGLib。不過可以通過配置指定 Spring AOP 都通過 CGLib 來生成代理類。

具體邏輯在 org.springframework.aop.framework.DefaultAopProxyFactory類中,使用哪種方式生成由AopProxy 根據 AdvisedSupport 對象的配置來決定源碼如下:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { public DefaultAopProxyFactory() { } public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) { return new JdkDynamicAopProxy(config); } else { Class targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation."); } else { //如果目標類是接口且是代理類, 使用JDK動態代理類,否則使用Cglib生成代理類 return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config)); } } } private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) { }}

具體內容就不展開了,後邊整理 SpringAOP 的時候再深入。

參考與感謝https://refactoringguru.cn/design-patterns/proxy https://www.geeksforgeeks.org/proxy-design-pattern/

相關文章