JDK動態代理和CGLIB動態代理

一、什麼是代理模式

代理模式(Proxy Pattern)給某一個物件提供一個代理,並由代理物件控制原物件的引用。代理物件在客戶端和目標物件之間起到中介作用。

代理模式是常用的結構型設計模式之一,當直接訪問某些物件存在問題時可以透過一個代理物件來間接訪問,為了保證客戶端使用的透明性,所訪問的真實物件與代理物件需要實現相同的介面。代理模式屬於結構型設計模式,屬於GOF23種設計模式之一。

代理模式可以分為靜態代理和動態代理兩種型別,而動態代理中又分為JDK動態代理和CGLIB代理兩種。

JDK動態代理和CGLIB動態代理

代理模式包含如下角色:

Subject (抽象主題角色)

抽象主題角色聲明瞭真實主題和代理主題的共同介面,這樣一來在任何使用真實主題 的地方都可以使用代理主題。客戶端需要針對抽象主題角色進行程式設計。

Proxy (代理主題角色)

代理主題角色內部包含對真實主題的引用,從而可以在任何時候操作真實主題物件。 在代理主題角色中提供一個與真實主題角色相同的介面,以便在任何時候都可以替代真實 主體。代理主題角色還可以控制對真實主題的使用,負責在需要的時候建立和刪除真實主 題物件,並對真實主題物件的使用加以約束。代理角色通常在客戶端呼叫所引用的真實主 題操作之前或之後還需要執行其他操作,而不僅僅是單純的呼叫真實主題物件中的操作。

RealSubject (真實主題 角色)

真實主題角色定義了代理角色所代表的真實物件,在真實主題角色中實現了真實的業 務操作,客戶端可以透過代理主題角色間接呼叫真實主題角色中定義的方法。

代理模式的優點

代理模式能將代理物件與真實被呼叫的目標物件分離。

一定程度上降低了系統的耦合度,擴充套件性好。

可以起到保護目標物件的作用。

可以對目標物件的功能增強。

代理模式的缺點

代理模式會造成系統設計中類的數量增加。

在客戶端和目標物件增加一個代理物件,會造成請求處理速度變慢。

二、JDK動態代理

在java的動態代理機制中,有兩個重要的類或介面,一個是 InvocationHandler(Interface)、另一個則是 Proxy(Class),這一個類和介面是實現我們動態代理所必須用到的。

InvocationHandler

每一個動態代理類都必須要實現InvocationHandler這個介面,並且每個代理類的例項都關聯了一個handler,當我們透過代理物件呼叫一個方法的時候,這個方法的呼叫就會被轉發為由InvocationHandler這個介面的 invoke 方法來進行呼叫。

InvocationHandler這個介面的唯一一個方法 invoke 方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable複製程式碼

這個方法一共接受三個引數,那麼這三個引數分別代表如下:

proxy

:指代JDK動態生成的最終代理物件

method

:指代的是我們所要呼叫真實物件的某個方法的Method物件

args

:指代的是呼叫真實物件某個方法時接受的引數

Proxy

Proxy這個類的作用就是用來動態建立一個代理物件的類,它提供了許多的方法,但是我們用的最多的就是newProxyInstance 這個方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler) throws IllegalArgumentException複製程式碼

這個方法的作用就是得到一個動態的代理物件,其接收三個引數,我們來看看這三個引數所代表的含義:

loader

:ClassLoader物件,定義了由哪個ClassLoader來對生成的代理物件進行載入,即代理類的類載入器。

interfaces

:Interface物件的陣列,表示的是我將要給我需要代理的物件提供一組什麼介面,如果我提供了一組介面給它,那麼這個代理物件就宣稱實現了該介面(多型),這樣我就能呼叫這組介面中的方法了。

Handler

:InvocationHandler物件,表示的是當我這個動態代理物件在呼叫方法的時候,會關聯到哪一個InvocationHandler物件上。

所以我們所說的DynamicProxy(動態代理類)是這樣一種class:它是在執行時生成的class,在生成它時你必須提供一組interface給它,然後該class就宣稱它實現了這些 interface。這個DynamicProxy其實就是一個Proxy,它不會做實質性的工作,在生成它的例項時你必須提供一個handler,由它接管實際的工作。

JDK動態代理例項

建立介面類

public interface HelloInterface { void sayHello();}複製程式碼

建立被代理類,實現介面

/** * 被代理類 */public class HelloImpl implements HelloInterface{ @Override public void sayHello() { System。out。println(“hello”); }}複製程式碼

建立InvocationHandler實現類

/** * 每次生成動態代理類物件時都需要指定一個實現了InvocationHandler介面的呼叫處理器物件 */public class ProxyHandler implements InvocationHandler{ private Object subject; // 這個就是我們要代理的真實物件,也就是真正執行業務邏輯的類 public ProxyHandler(Object subject) {// 透過構造方法傳入這個被代理物件 this。subject = subject; } /** *當代理物件呼叫真實物件的方法時,其會自動的跳轉到代理物件關聯的handler物件的invoke方法來進行呼叫 */ @Override public Object invoke(Object obj, Method method, Object[] objs) throws Throwable { Object result = null; System。out。println(“可以在呼叫實際方法前做一些事情”); System。out。println(“當前呼叫的方法是” + method。getName()); result = method。invoke(subject, objs);// 需要指定被代理物件和傳入引數 System。out。println(method。getName() + “方法的返回值是” + result); System。out。println(“可以在呼叫實際方法後做一些事情”); System。out。println(“————————————”); return result;// 返回method方法執行後的返回值 }}複製程式碼

測試

public class Mytest { public static void main(String[] args) { //第一步:建立被代理物件 HelloImpl hello = new HelloImpl(); //第二步:建立handler,傳入真實物件 ProxyHandler handler = new ProxyHandler(hello); //第三步:建立代理物件,傳入類載入器、介面、handler HelloInterface helloProxy = (HelloInterface) Proxy。newProxyInstance( HelloInterface。class。getClassLoader(), new Class[]{HelloInterface。class}, handler); //第四步:呼叫方法 helloProxy。sayHello(); }}複製程式碼

結果

可以在呼叫實際方法前做一些事情當前呼叫的方法是sayHellohellosayHello方法的返回值是null可以在呼叫實際方法後做一些事情————————————複製程式碼

JDK動態代理步驟

JDK動態代理分為以下幾步:

拿到被代理物件的引用,並且透過反射獲取到它的所有的介面。

透過JDK Proxy類重新生成一個新的類,同時新的類要實現被代理類所實現的所有的介面。

動態生成 Java 程式碼,把新加的業務邏輯方法由一定的邏輯程式碼去呼叫。

編譯新生成的 Java 程式碼。class。

將新生成的Class檔案重新載入到 JVM 中執行。

所以說JDK動態代理的核心是透過重寫被代理物件所實現的介面中的方法來重新生成代理類來實現的,那麼假如被代理物件沒有實現介面呢?那麼這時候就需要CGLIB動態代理了。

三、CGLIB動態代理

JDK動態代理是透過重寫被代理物件實現的介面中的方法來實現,而CGLIB是透過繼承被代理物件來實現,和JDK動態代理需要實現指定介面一樣,CGLIB也要求代理物件必須要實現MethodInterceptor介面,並重寫其唯一的方法intercept。

CGLib採用了非常底層的位元組碼技術,其原理是透過位元組碼技術為一個類建立子類,並在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。(利用ASM開源包,對代理物件類的class檔案載入進來,透過修改其位元組碼生成子類來處理)

注意

:因為CGLIB是透過繼承目標類來重寫其方法來實現的,故而如果是final和private方法則無法被重寫,也就是無法被代理。

cglib cglib-nodep 2。2複製程式碼

CGLib核心類

1、 net.sf.cglib.proxy.Enhancer

:主要增強類,透過位元組碼技術動態建立委託類的子類例項;

Enhancer可能是CGLIB中最常用的一個類,和Java1。3動態代理中引入的Proxy類差不多。和Proxy不同的是,Enhancer既能夠代理普通的class,也能夠代理介面。Enhancer建立一個被代理物件的子類並且攔截所有的方法呼叫(包括從Object中繼承的toString和hashCode方法)。Enhancer不能夠攔截final方法,例如Object。getClass()方法,這是由於Java final方法語義決定的。基於同樣的道理,Enhancer也不能對fianl類進行代理操作。這也是Hibernate為什麼不能持久化final class的原因。

2、net.sf.cglib.proxy.MethodInterceptor

:常用的方法攔截器介面,需要實現intercept方法,實現具體攔截處理;

public java。lang。Object intercept(java。lang。Object obj, java。lang。reflect。Method method, java。lang。Object[] args, MethodProxy proxy) throws java。lang。Throwable{}複製程式碼

obj

:動態生成的代理物件

method

:實際呼叫的方法

args

:呼叫方法入參

net.sf.cglib.proxy.MethodProxy

:java Method類的代理類,可以實現委託類物件的方法的呼叫;常用方法:methodProxy。invokeSuper(proxy, args);在攔截方法內可以呼叫多次。

CGLib代理例項

建立被代理類

public class SayHello { public void say(){ System。out。println(“hello”); }}複製程式碼

建立代理類

/** *代理類 */public class ProxyCglib implements MethodInterceptor{ private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz){ //設定需要建立子類的類 enhancer。setSuperclass(clazz); enhancer。setCallback(this); //透過位元組碼技術動態建立子類例項 return enhancer。create(); } //實現MethodInterceptor介面方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System。out。println(“可以在呼叫實際方法前做一些事情”); //透過代理類呼叫父類中的方法 Object result = proxy。invokeSuper(obj, args); System。out。println(“可以在呼叫實際方法後做一些事情”); return result; } }複製程式碼

測試

public class Mytest { public static void main(String[] args) { ProxyCglib proxy = new ProxyCglib(); //透過生成子類的方式建立代理類 SayHello proxyImp = (SayHello)proxy。getProxy(SayHello。class); proxyImp。say(); }}複製程式碼

結果

可以在呼叫實際方法前做一些事情hello可以在呼叫實際方法後做一些事情複製程式碼

CGLIB動態代理實現分析

CGLib動態代理採用了FastClass機制,其分別為代理類和被代理類各生成一個FastClass,這個FastClass類會為代理類或被代理類的方法分配一個 index(int型別)。這個index當做一個入參,FastClass 就可以直接定位要呼叫的方法直接進行呼叫,這樣省去了反射呼叫,所以呼叫效率比 JDK 動態代理透過反射呼叫更高。

但是我們看上面的原始碼也可以明顯看到,JDK動態代理只生成一個檔案,而CGLIB生成了三個檔案,所以生成代理物件的過程會更復雜。

四、JDK和CGLib動態代理對比

JDK 動態代理是實現了被代理物件所實現的介面,CGLib是繼承了被代理物件。 JDK和CGLib 都是在執行期生成位元組碼,JDK是直接寫Class位元組碼,CGLib 使用 ASM 框架寫Class位元組碼,Cglib代理實現更復雜,生成代理類的效率比JDK代理低。

JDK 呼叫代理方法,是透過反射機制呼叫,CGLib 是透過FastClass機制直接呼叫方法,CGLib 執行效率更高。

原理區別:

java動態代理是利用反射機制生成一個實現代理介面的匿名類,在呼叫具體方法前呼叫InvokeHandler來處理。核心是實現InvocationHandler介面,使用invoke()方法進行面向切面的處理,呼叫相應的通知。

而cglib動態代理是利用asm開源包,對代理物件類的class檔案載入進來,透過修改其位元組碼生成子類來處理。核心是實現MethodInterceptor介面,使用intercept()方法進行面向切面的處理,呼叫相應的通知。

1、如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP

2、如果目標物件實現了介面,可以強制使用CGLIB實現AOP

3、如果目標物件沒有實現了介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換

效能區別:

1、CGLib底層採用ASM位元組碼生成框架,使用位元組碼技術生成代理類,在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能對宣告為final的方法進行代理,因為CGLib原理是動態生成被代理類的子類。

2、在jdk6、jdk7、jdk8逐步對JDK動態代理最佳化之後,在呼叫次數較少的情況下,JDK代理效率高於CGLIB代理效率,只有當進行大量呼叫的時候,jdk6和jdk7比CGLIB代理效率低一點,但是到jdk8的時候,jdk代理效率高於CGLIB代理。

各自侷限:

1、JDK的動態代理機制只能代理實現了介面的類,而不能實現介面的類就不能實現JDK的動態代理。

2、cglib是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因為採用的是繼承,所以不能對final修飾的類進行代理。

型別

機制

回撥方式

適用場景

效率

JDK動態代理

委託機制,代理類和目標類都實現了同樣的介面,InvocationHandler持有目標類,代理類委託InvocationHandler去呼叫目標類的原始方法

反射

目標類是介面類

效率瓶頸在反射呼叫稍慢

CGLIB動態代理

繼承機制,代理類繼承了目標類並重寫了目標方法,透過回撥函式MethodInterceptor呼叫父類方法執行原始邏輯

透過FastClass方法索引呼叫

非介面類、非final類,非final方法

第一次呼叫因為要生成多個Class物件,比JDK方式慢。多次呼叫因為有方法索引比反射快,如果方法過多,switch case過多其效率還需測試

五、靜態代理和動態的本質區別

靜態代理只能透過手動完成代理操作,如果被代理類增加新的方法,代理類需要同步新增,違背開閉原則。

動態代理採用在執行時動態生成程式碼的方式,取消了對被代理類的擴充套件限制,遵循開閉原則。

若動態代理要對目標類的增強邏輯擴充套件,結合策略模式,只需要新增策略類便可完成,無需修改代理類的程式碼。

作者:Dynasty

連結:https://juejin。cn/post/7011357346018361375

著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。