Java JDK Proxy和CGLib動態代理範例講解

2023-02-07 12:03:06

簡介

代理模式在Java中有很多應用場景,而代理又分靜態程式碼和動態代理。靜態代理是編寫、編譯或載入時織入程式碼實現,而動態代理則在執行時實現。簡單而言,靜態代理是在執行前就已經存在,而動態代理則在執行時才存在的。而常用的動態代理有兩種實現:

  • JDK Proxy: JDK Proxy是JDK自帶的,不需要引入外部庫,通過實現介面進行代理;
  • CGLib: CGLib是引入第三方庫,通過ASM技術來實現位元組碼的生成;通過繼承的方式來實現。

現在我們來通過程式碼分別展示一下兩種方式。

JDK Proxy

JDK Proxy是通過實現介面來實現代理的,我們先定義一個介面:

public interface Flyable {
    String fly(String route);
}

接著有一個實現類:

public class Bird implements Flyable {
    @Override
    public String fly(String route) {
        System.out.println("Route: " + route);
        return route;
    }
}

然後我們需要定義一個InvocationHandler來改動方法的邏輯,就是目標被代理後有什麼不同:

public class FlyableInvocation  implements InvocationHandler {
    private final Flyable target;

    public FlyableInvocation(Flyable target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        System.out.println(target + ": ===JDK proxy===");
        Object result = method.invoke(this.target, args);
        System.out.println(target + ": ===JDK proxy===");
        long end = System.nanoTime();
        System.out.println("Executing time: " + (end - start) + " ns");
        return result;
    }
}

這裡我們在方法呼叫前後加了紀錄檔,同時也計算了一下方法的執行時間。

最終在呼叫的時候如下:

public class JDKDynamicProxy {
    public static void main(String[] args) {
        ClassLoader classLoader = JDKDynamicProxy.class.getClassLoader();
        Class<?>[] interfaces = Bird.class.getInterfaces();
        Bird bird = new Bird();
        Flyable flyable = (Flyable) Proxy.newProxyInstance(classLoader, interfaces, new FlyableInvocation(bird));
        flyable.fly("Go to pkslow.com");
    }
}

通過Proxy.newProxyInstance方法會生成一個代理的範例,執行這個範例的方法,而原有範例bird被代理了。

執行結果如下:

com.pkslow.basic.jdk.Bird@3551a94: ===JDK proxy===
Route: Go to pkslow.com
com.pkslow.basic.jdk.Bird@3551a94: ===JDK proxy===
Executing time: 18195736 ns

檢視代理類

我們還可以檢視生成的代理類,可以通過新增VM引數:

# JDK 8
-Dsum.misc.ProxyGenerator.saveGeneratedFiles=true
# JDK 11
-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true

當然,也可以在Java程式碼中設定系統屬性來實現。

設定完成,再執行程式,就會生成代理類的.class檔案。

CGLib

CGLib是通過繼承來實現的,我們先來定義一個類:

public class Animal {
    public String talk(String str) {
        System.out.println("Talking: " + str);
        return str;
    }
}

然後定義一個Interceptor,這個類的作用就是生成代理範例,且定義如何改變目標方法的執行:

public class CGLibProxy<T> implements MethodInterceptor {
    private T target;

    public T getInstance(T target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        enhancer.setCallback(this);
        return (T) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        long start = System.nanoTime();
        System.out.println(target + ": ===CGLib proxy===");
        Object result = methodProxy.invoke(this.target, args);
        System.out.println(target + ": ===CGLib proxy===");
        long end = System.nanoTime();
        System.out.println("Executing time: " + (end - start) + " ns");

        return result;
    }
}

這裡同樣是在方法前後加了紀錄檔,同時記錄時長。

呼叫如下:

public class CGLibDynamicProxy {
    public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/larry/IdeaProjects/pkslow-samples/java-basic/jdk-cglib-proxy/target/cglib_proxy_classes");
        CGLibProxy<Animal> cgLibProxy = new CGLibProxy<>();
        Animal animal = cgLibProxy.getInstance(new Animal());
        animal.talk("Hi, pkslow");
    }
}

這裡設定系統屬性是為了把生成的代理類輸出到.class檔案中,方便學習檢視。

執行結果如下:

com.pkslow.basic.cglib.Animal@57855c9a: ===CGLib proxy===
Talking: Hi, pkslow
com.pkslow.basic.cglib.Animal@57855c9a: ===CGLib proxy===
Executing time: 28396871 ns

總結

JDK Proxy本質上使用的是反射的機制,而CGLib使用的是ASM,CGLib速度會更好。但它們都不支援final的類和方法,因為通過介面和繼承都無法改變final方法。

程式碼請看GitHub: https://github.com/LarryDpk/pkslow-samples