JDK 和 CGLib 實現動態代理和區別

Yin發表於2021-07-29

JDK 和 CGLib 實現動態代理和區別

在日常的開發中,Spring AOP 是一個非常常用的功能。談到 AOP,自然離不開動態代理。

那麼,基於 JDK 和 CGLib 如何實現動態代理,他們之間的區別和適用場景是什麼呢?接下來,我們一起來探討一下這個問題。

JDK 如何實現動態代理?

話不多說,我們直接對照著程式碼來檢視。

程式碼示例

Hello 介面

public interface HelloInterface {

    /**
     * 代理的目標方法
     */
    void sayHello();

    /**
     * 未被代理處理的方法
     */
    void noProxyMethod();
}

Hello 實現類

public class HelloImpl implements HelloInterface {

    @Override
    public void sayHello() {
        System.out.println("proxyMethod:sayHello");
    }

    @Override
    public void noProxyMethod() {
        System.out.println("noProxyMethod");
    }
}

MyInvocationHandler 實現 InvocationHandler 介面類

public class MyInvocationHandler implements InvocationHandler {

    /**
     * 目標物件
     */
    private Object target;

    /**
     * 構造方法
     *
     * @param target
     */
    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if ("sayHello".equals(methodName)) {
            // 比方說,mybaitis 中的 PooledConnection 利用 jdk 動態代理重新實現了 close 方法
            System.out.println("change method");
            return null;
        }
        System.out.println("invoke method");
        Object result = method.invoke(target, args);
        return result;
    }

}

動態代理神奇的地方就是:

  1. 代理物件是在程式執行時產生的,而不是編譯期;
  2. 對代理物件的所有介面方法呼叫都會轉發到InvocationHandler.invoke()方法,在invoke()方法裡我們可以加入任何邏輯,比如修改方法引數,加入日誌功能、安全檢查功能等。

⚠️注意:從 Object 中繼承的方法,JDK Proxy 會把hashCode()、equals()、toString()這三個非介面方法轉發給 InvocationHandler,其餘的 Object 方法則不會轉發。詳見 JDK Proxy官方文件

程式碼測試

public class MyDynamicProxyTest {

    public static void main(String[] args) {
        HelloInterface hello = new HelloImpl();
        MyInvocationHandler handler = new MyInvocationHandler(hello);
        // 構造程式碼例項
        HelloInterface proxyInstance = (HelloInterface) Proxy.newProxyInstance(
                HelloImpl.class.getClassLoader(),
                HelloImpl.class.getInterfaces(),
                handler);
        // 代理呼叫方法
        proxyInstance.sayHello();
        proxyInstance.noProxyMethod();
    }
}

列印的日誌資訊如下:

image

關鍵要點

結合上面的演示,我們小結一下 JDK 動態代理的實現,包括三個步驟:

  • 1.定義一個介面

    比如上面的 HelloInterface,Jdk 的動態代理是基於介面,這就是代理介面。

  • 2.編寫介面實現類

    比如上面的 HelloImpl,這個就是目標物件,也就是被代理的物件類。

  • 3.編寫一個實現 InvocationHandler 介面的類,代理類的方法呼叫會被轉發到該類的 invoke() 方法。

    比如上面的 MyInvocationHandler。

CGLib 如何實現動態代理?

程式碼示例

Hello 類

無需定義和實現介面。

public class Hello {

    public String sayHello(String name) {
        System.out.println("Hello," + name);
        return "Hello," + name;
    }

}

CglibMethodInterceptor 實現 MethodInterceptor

/**
 * 實現一個MethodInterceptor,方法呼叫會被轉發到該類的intercept()方法。
 */
public class CglibMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("intercept param is " + Arrays.toString(args));
        System.out.println("before===============" + method);
        // 這裡可以實現增強的邏輯處理s
        Object result = methodProxy.invokeSuper(obj, args);
        // 這裡可以實現增強的邏輯處理
        System.out.println("after===============" + method);
        return result;
    }

}

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

pom 依賴

	<dependencies>
		<dependency>
			<groupId>cglib</groupId>
			<artifactId>cglib</artifactId>
			<version>2.1_3</version>
		</dependency>
	</dependencies>

程式碼測試

public class CglibTest {

    /**
     * 在需要使用 Hello 的時候,通過CGLIB動態代理獲取代理物件
     */
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Hello.class);
        enhancer.setCallback(new CglibMethodInterceptor());
        // 給目標物件建立一個代理物件
        Hello hello = (Hello) enhancer.create();
        hello.sayHello("Alan");
}

列印的日誌如下:

image

關鍵要點

結合上面的演示,我們小結一下 CGLIB 動態代理的實現:

  • 1.實現一個MethodInterceptor,方法呼叫被轉發到該類的 intercept() 方法。
  • 2.使用 Enhancer 獲取代理物件,並呼叫對應的方法。

JDK Vs CgLib

Java 1.3 後,提供了動態代理技術,允許我們開發者在執行期建立介面的代理例項,後來這項技術被用到了很多地方(比如 Spring AOP)。

JDK 動態代理主要對應到 java.lang.reflect 包下邊的兩個類:ProxyInvocationHandler

其中 InvocationHandler 是一個介面,可以通過實現該介面定義橫切邏輯。

舉個例子,在方法執行前後列印的日誌(這裡只是為了說明,實際應用一般不會只是簡單的列印日誌,一般用於日誌、安全、事務等場景),並通過「反射機制」呼叫目標類的程式碼,動態地將橫切邏輯和業務邏輯編織在一起。

  • JDK 動態代理有一個限制:它只能為介面建立代理例項

    對於沒有通過介面定義業務方法的類,如何建立動態代理例項呢?答案就是 CGLib。

  • CGLIB(Code Generation Library))是一個底層基於 ASM 的位元組碼生成庫,它允許我們在「執行時」修改和動態生成位元組碼。

    CGLIB 通過繼承方式實現代理,在子類中採用方法攔截的方式攔截所有父類方法的呼叫並順勢織入橫切邏輯

JDK 和 CGLib 動態代理區別

1. JDK 動態代理實現原理

  • 通過實現 InvocationHandler 介面建立自己的呼叫處理器
  • 通過為 Proxy 類指定 ClassLoader 物件和一組 interface 建立動態代理
  • 通過反射機制獲取動態代理類的建構函式,其唯一引數型別就是呼叫處理器介面型別
  • 通過建構函式建立動態代理類例項,構造時呼叫處理器物件作為引數傳入

JDK 動態代理是面向介面的代理模式,如果被代理目標沒有介面則無能為力。

例如,Spring 通過 Java 的反射機制生產被代理介面的新的匿名實現類,重寫了 AOP 的增強方法。

2. CGLib 動態代理原理

利用 ASM 開源包,對代理物件類的 class 檔案載入進來,通過修改其位元組碼生成子類來處理。

3. 兩者對比

  • JDK 動態代理是面向介面的;
  • CGLib 動態代理是通過位元組碼底層繼承代理類來實現,如果被代理類被 final 關鍵字所修飾,則無法被代理。

4.適用場景

  • 如果被代理的物件是個實現了介面的實現類,那麼可以使用 JDK 動態代理。

    例如,Spring 會使用 JDK 動態代理來完成操作(Spirng 預設採用)

  • 如果被代理的物件沒有實現介面,只有實現類,那麼只能使用 CGLib 實現動態代理(JDK 不支援)。

    例如,被代理物件是沒有介面的實現類,Spring 強制使用 CGLib 實現的動態代理。

效能對比

網上有人對於不通版本的 jdk 進行了測試,經過多次試驗,測試結果大致如下:

  • 在 JDK 1.6 和 1.7 時,JDK 動態代理的速度要比 CGLib 要慢,但是並沒有某些書上寫的10倍差距那麼誇張。
  • 在 JDK 1.8 時,JDK 動態代理的速度比 CGLib 快很多。

[idea] 很多時候,效能差異不一定是我們選擇某種方式的絕對因素,我們更應該去考慮該技術適用的場景

例如,我們應用中絕大多數的效能差異可能主要在集中在磁碟 I/O,網路頻寬等因素,動態代理這點效能差異可能只是佔了非常小的比例。

Reference


END

相關文章