【深度思考】聊聊CGLIB動態代理原理

申城異鄉人發表於2023-04-21

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$$398819d0getIndex()方法:

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$$8e91f654CGLIB$work$0()方法:

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

綜上所述,coder.work();的呼叫順序依次是:

代理類--->自定義方法攔截器--->代理類索引類getIndex()方法-->代理類索引類invoke()方法--->代理類--->被代理類。

4. JDK動態代理與CGLIB動態代理區別(面試常問)

關於JDK動態代理,可以檢視上一篇部落格:【深度思考】聊聊JDK動態代理原理

瞭解了JDK動態代理和CGLIB動態代理的原理後,現在來比較下兩者的區別,這也是面試時幾乎必問的一道面試題。

  1. 使用JDK動態代理,被代理類必須要實現介面,使用CGLIB動態代理,被代理類可以不實現介面

    原因分析:

    JDK動態代理生成的代理類繼承了java.lang.reflect.Proxy,因為Java是單繼承的,如果不透過實現介面的形式,

    無法對類進行擴充套件。

    CGLIB動態代理生成的代理類實際上是被代理類的子類,所以被代理類可以不實現介面。

  2. 自動生成類的數量不同

    JDK動態代理只會生成1個代理類,一般情況下名稱為:com.sun.proxy.$Proxy0

    CGLIB動態代理會生成好幾個類,核心的3個分別是:

    1)代理類:被代理類的子類,名稱格式為Coder$$EnhancerByCGLIB$$8e91f654,包名和被代理類包名一致。

    2)代理類的索引類:名稱格式為Coder$$EnhancerByCGLIB$$8e91f654$$FastClassByCGLIB$$4e5eb5aa

    包名和被代理類包名一致。

    3)被代理類的索引類:名稱格式為Coder$$FastClassByCGLIB$$398819d0,包名和被代理類包名一致。

  3. 生成代理類技術不同

    JDK動態代理使用JDK自帶的ProxyGenerator類生成位元組碼檔案。

    CGLIB動態代理使用ASM框架生成位元組碼檔案。

  4. 呼叫方式不同

    JDK動態代理:代理類--->InvocationHandler.invoke()--->被代理類方法(用到了反射)。

    CGLIB動態代理:代理類--->MethodInterceptor.intercept()--->代理類索引類getIndex()--->

    代理類索引類invoke()--->代理類--->被代理類。(直接呼叫)

文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閱讀!

相關文章