Java中的原生動態(tài)代理和CGLIB動態(tài)代理的原理,我不信你全知道!
作者:CarpenterLee
cnblogs.com/CarpenterLee/p/8241042.html
動態(tài)代理在Java中有著廣泛的應用,比如Spring AOP,Hibernate數(shù)據(jù)查詢、測試框架的后端mock、RPC,Java注解對象獲取等。靜態(tài)代理的代理關系在編譯時就確定了,而動態(tài)代理的代理關系是在編譯期確定的。靜態(tài)代理實現(xiàn)簡單,適合于代理類較少且確定的情況,而動態(tài)代理則給我們提供了更大的靈活性。
今天我們來探討Java中兩種常見的動態(tài)代理方式:JDK原生動態(tài)代理和CGLIB動態(tài)代理。
JDK原生動態(tài)代理
先從直觀的示例說起,假設我們有一個接口Hello和一個簡單實現(xiàn)HelloImp:
//?接口
interface?Hello{
????String?sayHello(String?str);
}
//?實現(xiàn)
class?HelloImp?implements?Hello{
????@Override
????public?String?sayHello(String?str)?{
????????return?"HelloImp:?"?+?str;
????}
}
這是Java種再常見不過的場景,使用接口制定協(xié)議,然后用不同的實現(xiàn)來實現(xiàn)具體行為。假設你已經(jīng)拿到上述類庫,如果我們想通過日志記錄對sayHello()的調(diào)用,使用靜態(tài)代理可以這樣做:
//?靜態(tài)代理方式
class?StaticProxiedHello?implements?Hello{
????...
????private?Hello?hello?=?new?HelloImp();
????@Override
????public?String?sayHello(String?str)?{
????????logger.info("You?said:?"?+?str);
????????return?hello.sayHello(str);
????}
}
上例中靜態(tài)代理類StaticProxiedHello作為HelloImp的代理,實現(xiàn)了相同的Hello接口。
用Java動態(tài)代理可以這樣做:
首先實現(xiàn)一個InvocationHandler,方法調(diào)用會被轉發(fā)到該類的invoke()方法。
然后在需要使用Hello的時候,通過JDK動態(tài)代理獲取Hello的代理對象。
//?Java?Proxy
// 1. 首先實現(xiàn)一個InvocationHandler,方法調(diào)用會被轉發(fā)到該類的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動態(tài)代理獲取Hello的代理對象。
Hello?hello?=?(Hello)Proxy.newProxyInstance(
????getClass().getClassLoader(),?//?1.?類加載器
????new?Class>[]?{Hello.class},?//?2.?代理需要實現(xiàn)的接口,可以有多個
????new?LogInvocationHandler(new?HelloImp()));//?3.?方法調(diào)用的實際處理者
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)方法,該方法會根據(jù)指定的參數(shù)動態(tài)創(chuàng)建代理對象。三個參數(shù)的意義如下:
loader,指定代理對象的類加載器;
interfaces,代理對象需要實現(xiàn)的接口,可以同時指定多個接口;
handler,方法調(diào)用的實際處理者,代理對象的方法調(diào)用都會轉發(fā)到這里(*注意1)。
newProxyInstance()會返回一個實現(xiàn)了指定接口的代理對象,對該對象的所有方法調(diào)用都會轉發(fā)給InvocationHandler.invoke()方法。理解上述代碼需要對Java反射機制有一定了解。動態(tài)代理神奇的地方就是:
代理對象是在程序運行時產(chǎn)生的,而不是編譯期;
對代理對象的所有接口方法調(diào)用都會轉發(fā)到InvocationHandler.invoke()方法,在invoke()方法里我們可以加入任何邏輯,比如修改方法參數(shù),加入日志功能、安全檢查功能等;之后我們通過某種方式執(zhí)行真正的方法體,示例中通過反射調(diào)用了Hello對象的相應方法,還可以通過RPC調(diào)用遠程方法。
注意1:對于從Object中繼承的方法,JDK Proxy會把hashCode()、equals()、toString()這三個非接口方法轉發(fā)給InvocationHandler,其余的Object方法則不會轉發(fā)。詳見JDK Proxy官方文檔。
如果對JDK代理后的對象類型進行深挖,可以看到如下信息:
#?Hello代理對象的類型信息
class=class?jdkproxy.$Proxy0
superClass=class?java.lang.reflect.Proxy
interfaces:?
interface?jdkproxy.Hello
invocationHandler=jdkproxy.LogInvocationHandler@a09ee92
代理對象的類型是jdkproxy.$Proxy0,這是個動態(tài)生成的類型,類名是形如$ProxyN的形式;父類是java.lang.reflect.Proxy,所有的JDK動態(tài)代理都會繼承這個類;同時實現(xiàn)了Hello接口,也就是我們接口列表中指定的那些接口。擴展:Java面試題內(nèi)容聚合
如果你還對jdkproxy.$Proxy0具體實現(xiàn)感興趣,它大致長這個樣子:
//?JDK代理類具體實現(xiàn)
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});//?將方法調(diào)用轉發(fā)給invocationhandler
????...
??}
??...
}
這些邏輯沒什么復雜之處,但是他們是在運行時動態(tài)產(chǎn)生的,無需我們手動編寫。更多詳情,可參考:
https://www.jianshu.com/p/e2917b0b9614
Java動態(tài)代理為我們提供了非常靈活的代理機制,但Java動態(tài)代理是基于接口的,如果對象沒有實現(xiàn)接口我們該如何代理呢?CGLIB登場。
CGLIB動態(tài)代理
CGLIB(Code Generation Library)是一個基于ASM的字節(jié)碼生成庫,它允許我們在運行時對字節(jié)碼進行修改和動態(tài)生成。CGLIB通過繼承方式實現(xiàn)代理。
來看示例,假設我們有一個沒有實現(xiàn)任何接口的類HelloConcrete:
public?class?HelloConcrete?{
????public?String?sayHello(String?str)?{
????????return?"HelloConcrete:?"?+?str;
????}
}
因為沒有實現(xiàn)接口該類無法使用JDK代理,通過CGLIB代理實現(xiàn)如下:
首先實現(xiàn)一個MethodInterceptor,方法調(diào)用會被轉發(fā)到該類的intercept()方法。
然后在需要使用HelloConcrete的時候,通過CGLIB動態(tài)代理獲取代理對象。
//?CGLIB動態(tài)代理
// 1. 首先實現(xiàn)一個MethodInterceptor,方法調(diào)用會被轉發(fā)到該類的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動態(tài)代理獲取代理對象。
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來指定要代理的目標對象、實際處理代理邏輯的對象,最終通過調(diào)用create()方法得到代理對象,對這個對象所有非final方法的調(diào)用都會轉發(fā)給MethodInterceptor.intercept()方法,在intercept()方法里我們可以加入任何邏輯,比如修改方法參數(shù),加入日志功能、安全檢查功能等;
通過調(diào)用MethodProxy.invokeSuper()方法,我們將調(diào)用轉發(fā)給原始對象,具體到本例,就是HelloConcrete的具體方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很類似,都是方法調(diào)用的中轉站。
注意:對于從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動態(tài)生成的類型;父類是HelloConcrete,印證了CGLIB是通過繼承實現(xiàn)代理;同時實現(xiàn)了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具體實現(xiàn)感興趣,它大致長這個樣子:
//?CGLIB代理類具體實現(xiàn)
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)?{
??????//?將請求轉發(fā)給MethodInterceptor.intercept()方法。
??????return?(String)tmp17_14.intercept(this,?
??????????????CGLIB$sayHello$0$Method,?
??????????????new?Object[]?{?paramString?},?
??????????????CGLIB$sayHello$0$Proxy);
????}
????return?super.sayHello(paramString);
??}
??...
}
上述代碼我們看到,當調(diào)用代理對象的sayHello()方法時,首先會嘗試轉發(fā)給MethodInterceptor.intercept()方法,如果沒有MethodInterceptor就執(zhí)行父類的sayHello()。這些邏輯沒什么復雜之處,但是他們是在運行時動態(tài)產(chǎn)生的,無需我們手動編寫。
更多關于CGLIB的介紹可以參考:
https://dzone.com/articles/cglib-missing-manual
結語
本文介紹了Java兩種常見動態(tài)代理機制的用法和原理,JDK原生動態(tài)代理是Java原生支持的,不需要任何外部依賴,但是它只能基于接口進行代理;CGLIB通過繼承的方式進行代理,無論目標對象有沒有實現(xiàn)接口都可以代理,但是無法處理final的情況。
動態(tài)代理是Spring AOP(Aspect Orient Programming, 面向切面編程)的實現(xiàn)方式,了解動態(tài)代理原理,對理解Spring AOP大有幫助。

最近我一直在面試高級工程師,不管初級,高級,程序員,我想面試前,大家刷題一定是是少不了吧。
我也一樣,我在網(wǎng)上找了很多面試題來看,最近又趕上跳槽的高峰期,好多粉絲,都問我要有沒有最新面試題,索性,我就把我看過的和我面試中的真題,及答案都整理好,整理了《第2版:互聯(lián)網(wǎng)大廠面試題》并分類?65份?PDF,累計 2340頁!我會持續(xù)更新中,馬上就出第三版,涵蓋大廠算法會更多!
第2版:題庫非常全面包括 Java 集合、JVM、多線程、并發(fā)編程、設計模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、Python、HTML、CSS、Vue、React、JavaScript、Android 大數(shù)據(jù)、阿里巴巴等大廠面試題等、等技術棧!
第2版:都是親自整理,看看縮略圖吧

我放在我的Java開發(fā)寶典里了,掃碼下方二維碼
回復:111,即可下載
點贊是最大的支持?![]()
