1. 程式人生 > >Java中的靜態代理和動態代理

Java中的靜態代理和動態代理

[toc] 最近在學習MyBatis原始碼,瞭解到MyBatis裡之所以只需要開發者編寫Mapper介面即可執行SQL,就是因為JDK的動態代理在背後默默為我們做了很多事情。但是我自己對動態代理還只是一知半解,於是手機整理資料學習,整理了這篇筆記。
說到動態代理,首先要講的就是設計模式中的代理模式,而對於代理,根據建立代理類的時間點,又可以分為**靜態代理**和**動態代理**。 # 1. 代理模式 **代理模式(Proxy)**,為其他物件提供一種代理以控制對這個物件的訪問。他的特徵是代理類與委託類實現相同的介面,代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類以及事後處理訊息等。代理類與委託類質檢通常會存在關聯關係,一個代理類的物件與一個委託類的物件關聯,代理類的物件本身並不真正實現服務,而是通過呼叫委託類的物件的相關方法,來提供特定的服務。簡單來說就是,我們**訪問實際物件時,是通過代理物件來訪問的,代理模式就是在訪問實際物件時引入一定程度的間接性**,因為這種間接性,使我們可以附加多種用途。
Tips: - 委託類:指的是代理模式中的被代理物件 - 代理類:指的是生成的代表委託類的一個角色 Java代理模式實現方式,主要有以下五種方法 1. 靜態代理,由開發者編輯代理類程式碼,實現代理模式。在編譯器就生成了代理類。 2. 基於JDK實現動態代理,通過JDK提供的工具方法Proxy.newProxyInstance()動態構建全新的代理類位元組碼檔案並例項化物件返回,這個代理類繼承Proxy類,並持有InvocationHandler介面引用。JDK動態代理是由Java內部的**反射**機制來例項化代理物件,並代理的呼叫委託類方法。 3. 基於CGLib動態代理模式,原理是繼承被代理類生成字代理類,不用實現介面,只需要被代理類是非final類即可。CGLib動態代理底層是藉助asm位元組碼技術。 4. 基於AspectJ實現動態代理。修改目標類的位元組,織入代理的位元組,在程式編譯的時候插入動態代理的位元組碼,不會生成全新的Class檔案。 5. 基於instrumentation實現動態代理。修改目標類的位元組碼、類載入的時候動態攔截去修改,基於javaagent實現`-javaagent:spring-instrument-4.3.8.RELEASE.jar`,類載入的時候插入動態代理的位元組碼,不會生成全新的Class檔案。 # 2. 靜態代理 靜態代理是代理類在編譯器就建立好了,不是編譯器生成的代理類,而是我們手動建立的類。在編譯時就已經將介面、本代理類和代理類確定下來。軟體設計模式中所指的代理一般就是說的靜態代理。 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33fe22305c5249cebdce2770a0086dc8~tplv-k3u1fbpfcp-zoom-1.image) Subject類,定義了RealSubject和Proxy的共用介面,這樣就在任何使用RealSubject的地方都可以使用Proxy。 ```Java public interface Subject { /** * doSomething() */ void doSomething(); } ``` RealSubject類,定義Proxy所代表的真實實體。 ```Java public class RealSubject implements Subject { @Override public void doSomething() { // 委託類執行操作 System.out.println("RealSubject.doSomething()"); } } ``` ProxySubject類,儲存一個引用使得代理可以訪問實體,並提供一個與Subject的介面相同的介面,這樣代理就可以用來替代實體。 ```Java public class ProxySubject implements Subject { private RealSubject realSubject; /** * 向代理類中注入委託類物件 * * @param realSubject 委託類物件 */ public ProxySubject(RealSubject realSubject){ this.realSubject = realSubject; } /** * 代理類執行操作 */ @Override public void doSomething() { System.out.println("代理類呼叫委託類方法之前"); realSubject.doSomething(); System.out.println("代理類呼叫委託類方法之後"); } } ``` 測試第一種方式,不使用代理類,直接使用簡單委託類執行。 ```Java public static void main(String[] args) { RealSubject realSubject = new RealSubject(); realSubject.doSomething(); } ``` 輸出: ```Java RealSubject.doSomething() ``` 測試第二種方式,使用代理類,執行增強邏輯。 ```Java public static void main(String[] args) { RealSubject realSubject = new RealSubject(); ProxySubject proxySubject = new ProxySubject(realSubject); proxySubject.doSomething(); } ``` 輸出: ```Java 代理類呼叫委託類方法之前 RealSubject.doSomething() 代理類呼叫委託類方法之後 ``` 我們在建立代理物件時,通過構造器塞入一個目標物件,然後在代理物件的方法內部呼叫目標物件同名方法,並在呼叫前後做增強邏輯。也就是說,**代理物件 = 增強程式碼 + 目標物件**。有了代理物件後,就不用原物件了。
**靜態代理的缺陷** 開發者需要手動為目標類編寫對應的代理類,而且要對類中的每個方法都編寫增強邏輯的程式碼,如果當前系統中已經存在成百上千個類,工作量太大了,且重複程式碼過多。所以,有沒有什麼方法能讓我們少寫或者不寫代理類,卻能完成代理功能? # 3. 動態代理 靜態代理是代理類在程式碼執行前已經建立好,並生成class檔案;動態代理類是代理類在程式執行時建立的代理模式。動態代理類的代理類並不是在Java程式碼中定義的,而是在執行時根據我們在Java程式碼中的“指示”動態生成的。相比於靜態代理,動態代理的優勢在於可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類中的方法。 ## 3.1 JDK動態代理 **基於介面實現**。Java的`java.lang.reflect`包下提供了Proxy類和一個 InvocationHandler 介面,這個類Proxy定義了生成JDK動態代理類的方法`getProxyClass(ClassLoader loader,Class... interfaces)`生成動態代理類,返回class例項代表一個class檔案。可以儲存該 class 檔案檢視jdk生成的代理類檔案長什麼樣。該生成的動態代理類繼承Proxy類,(重要特性) ,並實現公共介面。InvocationHandler這個介面,是被動態代理類回撥的介面,我們所有需要增加的針對委託類的統一增強邏輯都增加到invoke()方法裡面,在呼叫委託類介面方法之前或之後。
例子。任然使用上面靜態代理裡的類,只不過這次我們不會再用到代理類ProxySubject,而是讓JDK去幫我們生成代理類。方法如下: ```Java public static void main(String[] args) { // 例項化目標物件 Subject subject = new RealSubject(); // 獲取代理物件 Subject proxyInstance = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 增強邏輯 System.out.println("動態代理呼叫委託類方法之前"); Object invoke = method.invoke(subject, args); System.out.println("動態代理呼叫委託類方法之後"); return invoke; } }); // 執行目標方法 proxyInstance.doSomething(); } ``` 輸出: ```Java 動態代理呼叫委託類方法之前 RealSubject.doSomething() 動態代理呼叫委託類方法之後 ``` Jdk為我們的生成了一個叫$Proxy0(這個名字後面的0是編號,有多個代理類會一次遞增)的代理類,這個類檔案時預設不會儲存在檔案,放在記憶體中的,我們在建立代理物件時,就是通過反射獲得這個類的構造方法,然後建立代理物件例項。通過對這個生成的代理類原始碼的檢視,我們很容易能看出,動態代理實現的具體過程。
我們可以對 InvocationHandler看做一箇中介類,中介類持有一個被代理物件,被Proxy類回撥。在invoke方法中呼叫了被代理物件的相應方法。通過聚合方式持有被代理物件的引用,把客戶端對invoke的呼叫最終都轉為對被代理物件的呼叫。
客戶端程式碼通過代理類引用呼叫介面方法時,通過代理類關聯的中介類物件引用來呼叫中介類物件的invoke方法,從而達到代理執行被代理物件的方法。也就是說,動態代理Proxy類提供了模板實現,對外提供擴充套件點,外部通過實現InvocationHandler介面將被代理類納入JDK代理類Proxy。
**JDK動態代理特點總結** 1. 生成的代理類:$Proxy0 extends Proxy implements Subject,我們看到代理類繼承了Proxy類,Java的繼承機制決定了JDK動態代理類們無法實現對 類 的動態代理。所以也就決定了**JDK動態代理只能對介面進行代理**。 2. 每個生成的動態代理例項都會關聯一個呼叫處理器物件,可以通過 Proxy 提供的靜態方法 getInvocationHandler去獲得代理類例項的呼叫處理器物件。在代理類例項上呼叫其代理的介面中所宣告的方法時,這些方法最終都會由呼叫處理器的 invoke 方法執行。 3. 代理類的根類 java.lang.Object 中有三個方法也同樣會被分派到呼叫處理器的 invoke 方法執行,它們是 hashCode,equals 和 toString,可能的原因有:一是因為這些方法為 public 且非 final 型別,能夠被代理類覆蓋;二是因為這些方法往往呈現出一個類的某種特徵屬性,具有一定的區分度,所以為了保證代理類與委託類對外的一致性,這三個方法也應該被呼叫處理器分派到委託類執行。 **JDK動態代理的不足** JDK動態代理的代理類位元組碼在建立時,需要實現業務實現類所實現的介面作為引數。如果業務實現類是沒有實現介面而是直接定義業務方法的話,就無法使用JDK動態代理了。(JDK動態代理重要特點是代理介面)並且,如果業務實現類中新增了介面中沒有的方法,這些方法是無法被代理的(因為無法被呼叫)。動態代理只能對介面產生代理,不能對類產生代理。 ## 3.2 CGLib動態代理 **基於繼承**。CGlib是針對類來實現代理的,他的原理是對代理的目標類生成一個子類,並覆蓋其中方法實現增強,因為底層是基於建立被代理類的一個子類,所以它避免了JDK動態代理類的缺陷。**但因為採用的是繼承,所以不能對final修飾的類進行代理**。final修飾的類不可繼承。 例子。 ```Java public class CGlibSubject implements MethodInterceptor { private Object target; public Object getInstance(Object target) { this.target = target; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); // 設定回撥方法 enhancer.setCallback(this); // 建立代理物件 return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("CGlib動態代理呼叫委託類方法之前"); Object result = methodProxy.invokeSuper(o, objects); System.out.println("CGlib動態代理呼叫委託類方法之後"); return result; } } ``` 測試: ```Java public static void main (String[] args) { CGlibSubject cglibSubject = new CGlibSubject(); RealSubject instance = (RealSubject) cglibSubject.getInstance(new RealSubject()); instance.doSomething(); } ``` 輸出: ```Java CGlib動態代理呼叫委託類方法之前 RealSubject.doSomething() CGlib動態代理呼叫委託類方法之後 ``` **CGlib動態代理特點總結** 1. CGlib可以傳入介面也可以傳入普通的類,介面使用實現的方式,普通類使用會使用繼承的方式生成代理類; 2. 由於是繼承方式,如果是static方法,private方法,final方法等描述的方法是不能被代理的; 3. 做了方法訪問優化,使用建立方法索引的方式避免了傳統JDK動態代理需要通過Method方法反射呼叫; 4. 提供callback 和filter設計,可以靈活地給不同的方法繫結不同的callback。編碼更方便靈活; 5. CGLIB會預設代理Object中equals,toString,hashCode,clone等方法。比JDK代理多了clone。 # 4. 總結 1. 靜態代理是通過在程式碼中**顯式編碼**定義一個業務實現類的代理類,在代理類中對同名的業務方法進行包裝,使用者**通過代理類呼叫委託類的業務方法**; 2. JDK動態代理是通過介面中的方法名,在動態生成的代理類中呼叫業務實現類的同名方法; 3. CGlib動態代理是通過**繼承**業務類,生成的動態代理類是業務類的子類,通過重寫業務方法進行代理; 4. 靜態代理在編譯時產生class位元組碼檔案,可以直接使用,效率高。動態代理必須實現InvocationHandler介面,通過invoke呼叫被委託類介面方法是通過**反射方式**,比較消耗系統性能,但可以減少代理類的數量,使用更靈活。cglib代理無需實現介面,通過生成類位元組碼實現代理,比反射稍快,不存在效能問題,但cglib會繼承目標物件,需要重寫方法,所以目標物件不能為final類。 **參考文章**: 1. [太好了!總算有人把動態代理、CGlib、AOP都說清楚了!](https://cloud.tencent.com/developer/article/1461796) 2. [Java 動態代理作用是什麼?](https://www.zhihu.com/question/20794107) # 5. 程式碼倉庫 https://github.com/goSilver/daydayup/tree/master/java/src/designpattern/proxy