Java Proxy 和 CGLIB 動態代理原理

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

Leave all your unhappiness to yesterday, give all your hopes to tomorrow, and do all your hard work today.

把所有的不快給昨天,把所有的希望給明天,把所有的努力給今天。


每日掏心話

所謂的幸福,就是在平淡生活裡的那一份執著和堅守。

來自:CarpenterLee | 責編:樂樂

鏈接:cnblogs.com/CarpenterLee/p/8241042.html

Java Proxy 和 CGLIB 動態代理原理

程序員小樂(ID:study_tech)第 840 次推文 圖片來自百度


往日回顧:又一個程序員“倒”下,Pandownload涼了...


正文


動態代理在Java中有著廣泛的應用,比如Spring AOP,Hibernate數據查詢、測試框架的後端mock、RPC,Java註解對象獲取等。靜態代理的代理關係在編譯時就確定了,而動態代理的代理關係是在編譯期確定的。靜態代理實現簡單,適合於代理類較少且確定的情況,而動態代理則給我們提供了更大的靈活性。今天我們來探討Java中兩種常見的動態代理方式:JDK原生動態代理和CGLIB動態代理。


JDK原生動態代理


先從直觀的示例說起,假設我們有一個接口Hello和一個簡單實現HelloImp:


// 接口

interface Hello{

String sayHello(String str);

}

// 實現

class HelloImp implements Hello{

@Override

public String sayHello(String str) {

return "HelloImp: " + str;

}

}


這是Java種再常見不過的場景,使用接口制定協議,然後用不同的實現來實現具體行為。假設你已經拿到上述類庫,如果我們想通過日誌記錄對sayHello()的調用,使用靜態代理可以這樣做:


// 靜態代理方式

class StaticProxiedHello implements Hello{

...

private Hello hello = new HelloImp();

@Override

public String sayHello(String str) {

logger.info("You said: " + str);

return hello.sayHello(str);

}

}


上例中靜態代理類StaticProxiedHello作為HelloImp的代理,實現了相同的Hello接口。用Java動態代理可以這樣做:


  • 首先實現一個InvocationHandler,方法調用會被轉發到該類的invoke()方法。

  • 然後在需要使用Hello的時候,通過JDK動態代理獲取Hello的代理對象。


// Java Proxy

// 1. 首先實現一個InvocationHandler,方法調用會被轉發到該類的invoke()方法。

class LogInvocationHandler implements InvocationHandler{

...

private Hello hello;

public LogInvocationHandler(Hello hello) {

this.hello = hello;

}

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if("sayHello".equals(method.getName())) {

logger.info("You said: " + Arrays.toString(args));

}

return method.invoke(hello, args);

}

}

// 2. 然後在需要使用Hello的時候,通過JDK動態代理獲取Hello的代理對象。

Hello hello = (Hello)Proxy.newProxyInstance(

getClass().getClassLoader(), // 1. 類加載器

new Class>[] {Hello.class}, // 2. 代理需要實現的接口,可以有多個

new LogInvocationHandler(new HelloImp()));// 3. 方法調用的實際處理者

System.out.println(hello.sayHello("I love you!"));


運行上述代碼輸出結果:


日誌信息: You said: [I love you!]

HelloImp: I love you!


上述代碼的關鍵是Proxy.newProxyInstance(ClassLoader loader, Class>[] interfaces, InvocationHandler handler)方法,該方法會根據指定的參數動態創建代理對象。三個參數的意義如下:


  • loader,指定代理對象的類加載器;

  • interfaces,代理對象需要實現的接口,可以同時指定多個接口;

  • handler,方法調用的實際處理者,代理對象的方法調用都會轉發到這裡(*注意1)。


newProxyInstance()會返回一個實現了指定接口的代理對象,對該對象的所有方法調用都會轉發給InvocationHandler.invoke()方法。理解上述代碼需要對Java反射機制有一定了解。動態代理神奇的地方就是:


  • 代理對象是在程序運行時產生的,而不是編譯期;

  • 對代理對象的所有接口方法調用都會轉發到InvocationHandler.invoke()方法,在invoke()方法裡我們可以加入任何邏輯,比如修改方法參數,加入日誌功能、安全檢查功能等;之後我們通過某種方式執行真正的方法體,示例中通過反射調用了Hello對象的相應方法,還可以通過RPC調用遠程方法。


注意1:對於從Object中繼承的方法,JDK Proxy會把hashCode()、equals()、toString()這三個非接口方法轉發給InvocationHandler,其餘的Object方法則不會轉發。詳見JDK Proxy官方文檔。


如果對JDK代理後的對象類型進行深挖,可以看到如下信息:


# Hello代理對象的類型信息

class=class jdkproxy.$Proxy0

superClass=class java.lang.reflect.Proxy

interfaces:

interface jdkproxy.Hello

invocationHandler=jdkproxy.LogInvocationHandler@a09ee92


代理對象的類型是jdkproxy.$Proxy0,這是個動態生成的類型,類名是形如$ProxyN的形式;父類是java.lang.reflect.Proxy,所有的JDK動態代理都會繼承這個類;同時實現了Hello接口,也就是我們接口列表中指定的那些接口。


如果你還對jdkproxy.$Proxy0具體實現感興趣,它大致長這個樣子:


// JDK代理類具體實現

public final class $Proxy0 extends Proxy implements Hello

{

...

public $Proxy0(InvocationHandler invocationhandler)

{

super(invocationhandler);

}

...

@Override

public final String sayHello(String str){

...

return super.h.invoke(this, m3, new Object[] {str});// 將方法調用轉發給invocationhandler

...

}

...

}

這些邏輯沒什麼複雜之處,但是他們是在運行時動態產生的,無需我們手動編寫。更多詳情,可參考 BrightLoong的Java靜態代理&動態代理筆記


https://www.jianshu.com/p/e2917b0b9614


Java動態代理為我們提供了非常靈活的代理機制,但Java動態代理是基於接口的,如果對象沒有實現接口我們該如何代理呢?CGLIB登場。


CGLIB動態代理


CGLIB(Code Generation Library)是一個基於ASM的字節碼生成庫,它允許我們在運行時對字節碼進行修改和動態生成。CGLIB通過繼承方式實現代理。


來看示例,假設我們有一個沒有實現任何接口的類HelloConcrete:


public class HelloConcrete {

public String sayHello(String str) {

return "HelloConcrete: " + str;

}

}


因為沒有實現接口該類無法使用JDK代理,通過CGLIB代理實現如下:


  • 首先實現一個MethodInterceptor,方法調用會被轉發到該類的intercept()方法。

  • 然後在需要使用HelloConcrete的時候,通過CGLIB動態代理獲取代理對象。


// CGLIB動態代理

// 1. 首先實現一個MethodInterceptor,方法調用會被轉發到該類的intercept()方法。

class MyMethodInterceptor implements MethodInterceptor{

...

@Override

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

logger.info("You said: " + Arrays.toString(args));

return proxy.invokeSuper(obj, args);

}

}

// 2. 然後在需要使用HelloConcrete的時候,通過CGLIB動態代理獲取代理對象。

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(HelloConcrete.class);

enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();

System.out.println(hello.sayHello("I love you!"));


運行上述代碼輸出結果:


日誌信息: You said: [I love you!]

HelloConcrete: I love you!


上述代碼中,我們通過CGLIB的Enhancer來指定要代理的目標對象、實際處理代理邏輯的對象,最終通過調用create()方法得到代理對象,對這個對象所有非final方法的調用都會轉發給MethodInterceptor.intercept()方法,在intercept()方法裡我們可以加入任何邏輯,比如修改方法參數,加入日誌功能、安全檢查功能等;通過調用MethodProxy.invokeSuper()方法,我們將調用轉發給原始對象,具體到本例,就是HelloConcrete的具體方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很類似,都是方法調用的中轉站。


注意:對於從Object中繼承的方法,CGLIB代理也會進行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不會,因為它是final方法,CGLIB無法代理。


如果對CGLIB代理之後的對象類型進行深挖,可以看到如下信息:


# HelloConcrete代理對象的類型信息

class=class cglib.HelloConcrete$EnhancerByCGLIB$e3734e52

superClass=class lh.HelloConcrete

interfaces:

interface net.sf.cglib.proxy.Factory

invocationHandler=not java proxy class


我們看到使用CGLIB代理之後的對象類型是cglib.HelloConcrete$EnhancerByCGLIB$e3734e52,這是CGLIB動態生成的類型;父類是HelloConcrete,印證了CGLIB是通過繼承實現代理;同時實現了net.sf.cglib.proxy.Factory接口,這個接口是CGLIB自己加入的,包含一些工具方法。


注意,既然是繼承就不得不考慮final的問題。我們知道final類型不能有子類,所以CGLIB不能代理final類型,遇到這種情況會拋出類似如下異常:


java.lang.IllegalArgumentException: Cannot subclass final class cglib.HelloConcrete


同樣的,final方法是不能重載的,所以也不能通過CGLIB代理,遇到這種情況不會拋異常,而是會跳過final方法只代理其他方法。


如果你還對代理類cglib.HelloConcrete$EnhancerByCGLIB$e3734e52具體實現感興趣,它大致長這個樣子:


// CGLIB代理類具體實現

public class HelloConcrete$EnhancerByCGLIB$e3734e52

extends HelloConcrete

implements Factory

{

...

private MethodInterceptor CGLIB$CALLBACK_0; // ~~

...

public final String sayHello(String paramString)

{

...

MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;

if (tmp17_14 != null) {

// 將請求轉發給MethodInterceptor.intercept()方法。

return (String)tmp17_14.intercept(this,

CGLIB$sayHello$0$Method,

new Object[] { paramString },

CGLIB$sayHello$0$Proxy);

}

return super.sayHello(paramString);

}

...

}


上述代碼我們看到,當調用代理對象的sayHello()方法時,首先會嘗試轉發給MethodInterceptor.intercept()方法,如果沒有MethodInterceptor就執行父類的sayHello()。這些邏輯沒什麼複雜之處,但是他們是在運行時動態產生的,無需我們手動編寫。如何獲取CGLIB代理類字節碼可參考Access the generated byte[] array directly。


更多關於CGLIB的介紹可以參考Rafael Winterhalter的cglib: The missing manual,一篇很深入的文章。


https://dzone.com/articles/cglib-missing-manual


結語


本文介紹了Java兩種常見動態代理機制的用法和原理,JDK原生動態代理是Java原生支持的,不需要任何外部依賴,但是它只能基於接口進行代理;CGLIB通過繼承的方式進行代理,無論目標對象有沒有實現接口都可以代理,但是無法處理final的情況。


動態代理是Spring AOP(Aspect Orient Programming, 面向切面編程)的實現方式,瞭解動態代理原理,對理解Spring AOP大有幫助。


歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


猜你還想看


阿里、騰訊、百度、華為、京東最新面試題彙集

面試熱身:5 億整數的大文件,排個序?

線上服務 CPU 100%?一鍵定位 So Easy!

基於 token 的多平臺身份認證架構設計

關注訂閱號「程序員小樂」,收看更多精彩內容
嘿,你在看嗎?


分享到:


相關文章: