【深度思考】聊聊CGLIB動態代理原理
2023-04-21 13:19 申城異鄉人 閱讀(3946) 評論(4) 收藏 舉報1. 簡介
CGLIB的全稱是:Code Generation Library。
CGLIB是一個強大的、高性能、高質量的代碼生成類庫,它可以在運行期擴展Java類與實現Java接口,
底層使用的是字節碼處理框架ASM。
Github地址:https://github.com/cglib/cglib。
CGLIB的Maven坐標如下所示:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
2. 示例
首先,新增一個類:
public class Coder {
public void work() {
System.out.println("認真寫bug……");
}
}
然后,自定義一個方法攔截器,實現net.sf.cglib.proxy.MethodInterceptor接口并重寫intercept方法:
public class AttendanceMethodInterceptor implements MethodInterceptor {
@Override
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;
}
}
重點看下Object result = proxy.invokeSuper(obj, args);,該行代碼最終會執行真正的目標方法,在這前后,我們可以添加一些增強邏輯。
然后,新建個測試類,看下CGLIB動態代理如何使用:
public class CglibProxyTest {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Coder.class);
enhancer.setCallback(new AttendanceMethodInterceptor());
// 創建代理對象
Object object = enhancer.create();
Coder coder = (Coder) object;
coder.work();
}
}
運行以上代碼,效果如下圖所示:

從運行結果可以看出,在目標方法的前后,執行了自定義的操作。
3. 原理
看下上面的測試類代碼,首先是創建了一個net.sf.cglib.proxy.Enhancer對象,然后調用了setSuperclass()方法
將enhancer對象的父類設置為Coder類:

緊接著調用了setCallback()方法將enhancer對象的方法攔截器設置為自定義的AttendanceMethodInterceptor:


然后是調用enhancer對象的create()方法來生成一個代理對象。
先打印下,簡單看下這個代理類的信息:

圖中的com.zwwhnly.mybatisplusdemo.cglibproxy.Coder$$EnhancerByCGLIB$$8e91f654就是CGLIB生成的代理類的名稱。
那么這個代理類具體是什么樣子呢?
在上面的測試類代碼中(Object object = enhancer.create();代碼之前)添加以下一行代碼:
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./cglib");
然后再次運行,會看到項目根目錄下生成了一個cglib文件夾,自動生成的代理類就包含在其中:

可以看到一共生成了5個類,這里重點關注下紅色標記的3個類。
先看下Coder$$EnhancerByCGLIB$$8e91f654.class,這個類就是自動生成的代理類:

可以看出Coder$$EnhancerByCGLIB$$8e91f654.class繼承了Coder類(也就是說自動生成的代理類其實是被代理類的一個子類),
并且重寫了Coder類的work()方法,重寫后的work()方法會調用自定義的方法攔截器AttendanceMethodInterceptor里的intercept()
方法。
然后看下Coder$$EnhancerByCGLIB$$8e91f654$$FastClassByCGLIB$$4e5eb5aa,從名稱上可以看出這個類的前半段和上面的類的名稱
是一樣的,后半段拼接上了$$FastClassByCGLIB$$4e5eb5aa,從功能上說,這個類是上面的代理類的索引類,重點關注下里面的
getIndex()方法和invoke()方法:

最后看下Coder$$FastClassByCGLIB$$398819d0,這個類是被代理類Coder的索引類,重點也是關注下里面的
getIndex()方法和invoke()方法:

知道了這3個類的作用后,再一步一步看下示例代碼中coder.work();的調用過程,因為coder是生成的代理類的實例,所以
coder.work();首先調用的是Coder$$EnhancerByCGLIB$$8e91f654的work()方法:

這里的var10000是自定義的方法攔截器AttendanceMethodInterceptor,所以執行的是紅色截圖里的intercept()方法,也就是:

然后看下invokeSuper()方法:

首先執行的是init()方法,在該方法內部對fastClassInfo字段進行了賦值:

從上圖可以看出,fci.f1是自動生成的Coder類的索引類Coder$$FastClassByCGLIB$$398819d0,所以fci.i1 = fci.f1.getIndex(sig1);
其實執行的是的Coder$$FastClassByCGLIB$$398819d0的getIndex()方法:

fci.f2是自動生成的代理類的索引類Coder$$EnhancerByCGLIB$$8e91f654$$FastClassByCGLIB$$4e5eb5aa,
所以fci.i2 = fci.f2.getIndex(sig2);其實執行的是的Coder$$EnhancerByCGLIB$$8e91f654$$FastClassByCGLIB$$4e5eb5aa的
getIndex()方法:

看完init()方法后再回到invokeSuper()方法:

上圖中的FastClassInfo fci = fastClassInfo;使用到的字段fastClassInfo在init()方法內部已經賦過值,
fci.f2其實是自動生成的代理類的索引類Coder$$EnhancerByCGLIB$$8e91f654$$FastClassByCGLIB$$4e5eb5aa,
fci.i2值是1,
所以fci.f2.invoke(fci.i2, obj, args);實際執行的是:

這里的var10000其實是自動生成的代理類Coder$$EnhancerByCGLIB$$8e91f654的實例,所以接著調用的是
代理類Coder$$EnhancerByCGLIB$$8e91f654的CGLIB$work$0()方法:

這里的super指的是Coder類,所以super.work();實際執行的是Coder類的work()方法:

綜上所述,coder.work();的調用順序依次是:
代理類--->自定義方法攔截器--->代理類索引類getIndex()方法-->代理類索引類invoke()方法--->代理類--->被代理類。
4. JDK動態代理與CGLIB動態代理區別(面試常問)
關于JDK動態代理,可以查看上一篇博客:【深度思考】聊聊JDK動態代理原理。
了解了JDK動態代理和CGLIB動態代理的原理后,現在來比較下兩者的區別,這也是面試時幾乎必問的一道面試題。
-
使用JDK動態代理,被代理類必須要實現接口,使用CGLIB動態代理,被代理類可以不實現接口
原因分析:
JDK動態代理生成的代理類繼承了
java.lang.reflect.Proxy,因為Java是單繼承的,如果不通過實現接口的形式,無法對類進行擴展。
CGLIB動態代理生成的代理類實際上是被代理類的子類,所以被代理類可以不實現接口。
-
自動生成類的數量不同
JDK動態代理只會生成1個代理類,一般情況下名稱為:
com.sun.proxy.$Proxy0。CGLIB動態代理會生成好幾個類,核心的3個分別是:
1)代理類:被代理類的子類,名稱格式為
Coder$$EnhancerByCGLIB$$8e91f654,包名和被代理類包名一致。2)代理類的索引類:名稱格式為
Coder$$EnhancerByCGLIB$$8e91f654$$FastClassByCGLIB$$4e5eb5aa,包名和被代理類包名一致。
3)被代理類的索引類:名稱格式為
Coder$$FastClassByCGLIB$$398819d0,包名和被代理類包名一致。 -
生成代理類技術不同
JDK動態代理使用JDK自帶的ProxyGenerator類生成字節碼文件。
CGLIB動態代理使用ASM框架生成字節碼文件。
-
調用方式不同
JDK動態代理:代理類--->InvocationHandler.invoke()--->被代理類方法(用到了反射)。
CGLIB動態代理:代理類--->MethodInterceptor.intercept()--->代理類索引類getIndex()--->
代理類索引類invoke()--->代理類--->被代理類。(直接調用)
文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閱讀!
浙公網安備 33010602011771號