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


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM