Spring5框架之JDK動態代理與CGLIB代理兩種代理方式性能比較(七)

1.前言

在Spring中有兩種類型的代理:一個是使用JDK Proxy類生成的代理,另一種就是使用CGLIB Enhancer類創建的CGLIB代理,下面將簡單介紹兩種代理方式。

JDK代理

JDK是Spring中最基本的代理類型,與CGLIB不同的是JDK代理只能生成基於接口的代理,無法生成類的代理。所以Spring中使用JDK的代理以實現接口的代理,下面簡單演示一下JDK動態代理使用如下所示:

  1. 新增Calculator接口以其實現如下所示:
public interface Calculator {

    Double divide(Double a,Double b);
}


@Service(value = "calculator")
public class CalculatorImpl implements Calculator {
    @Override
    public Double divide(Double a, Double b) {
        return a / b;
    }
}

  1. 新增CalculatorProxy類實現InvocationHandler接口如下所示:
@NoArgsConstructor
public class CalculatorJDKProxy implements InvocationHandler {

    private Object proxyTarget;

    public CalculatorJDKProxy(Object proxyTarget) {
        this.proxyTarget = proxyTarget;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("\n"+"JDK代理方法執行之前執行的程序邏輯");
        List<Object> objects = Arrays.asList(args);
        double sum = objects.stream().mapToDouble(e -> Double.parseDouble(e.toString().trim())).sum();
        if (sum > 50.0) {
            System.out.println("計算的值大於50了");
        }
        Object invoke = method.invoke(proxyTarget, args);
        System.out.println("JDK代理執行之後的程序邏輯");
        // 代理之和的執行程序
        return invoke;
    }
}
  1. 測試類方法使用如下所示:
    @Test
    public void testJdkProxy() {
        Calculator calculator = new CalculatorImpl();
        Calculator o = (Calculator) Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new CalculatorJDKProxy(calculator));
        Double divide = o.divide(250.0, 5.0);
        System.out.println("輸出的值爲:" + divide);
    }

輸出結果如下所示:

JDK代理方法執行之前執行的程序邏輯
計算的值大於50了
JDK代理執行之後的程序邏輯
輸出的值爲:50.0

當使用JDK代理時,所有的方法的調用都會被JVM攔截到代理的Invoke方法,然後Spring由這個方法以及切入點方法確定是否執行相關通知。

CGLIB代理

CGLIB會爲每個代理動態生成新的字節碼並儘可能重複使用已經生成的字節碼對象。而且也完美解決了JDK動態代理只能代理接口對象的侷限性,從Spring 3.2 開始後不需要在單獨將CGLIB添加到項目路徑中,因爲CGLIB包含在Spring-core jar中。


import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class CalculatorCGLIBProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB代理之前執行的程序邏輯");
        // 執行被代理對象的方法
        Object invokeSuper = methodProxy.invokeSuper(o, objects);
        List<Object> list = Arrays.asList(objects);
        double sum = list.stream().mapToDouble(e -> Double.parseDouble(e.toString().trim())).sum();
        if (sum > 50.0) {
            System.out.println("計算的值大於50了");
        }
        System.out.println("CGLIB代理之後執行的程序邏輯");
        return invokeSuper;
    }
}

測試類方法代碼如下所示;

    @Test
    public void testCGLIB() {
        Enhancer enhancer = new Enhancer();
        System.out.println();
        enhancer.setSuperclass(CalculatorImpl.class);
        enhancer.setCallback(new CalculatorCGLIBProxy());
        Calculator o = (Calculator) enhancer.create();
        Double divide = o.divide(500.0, 3.0);
        System.out.println(divide);
    }

運行結果如下所示:

CGLIB代理之前執行的程序邏輯
計算的值大於50了
CGLIB代理之後執行的程序邏輯
166.66666666666666

需要說明的是CGLIB生成代理是通過字節碼生成的子類作爲代理類,因此不能對private final方法代理;

兩種代理執行性能比較

  1. 先分別測試兩種標準代理實現方式的性能差異比較,詳情代碼如下所示:
 @Test
    public void testPerformance() {
        //TODO JDK代理使用
        Map<String,String> map = new HashMap<>();
        Calculator calculator = new CalculatorImpl();
        Random random = new Random();
        // 創建Spring的計算類對象
        StopWatch stopWatch = new StopWatch();
        String taskName="JDK代理";
        stopWatch.start(taskName);
        Calculator o = (Calculator) Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new CalculatorJDKProxy(calculator));
        List<Double> list = new ArrayList<>(1000);
        addNumToList(o, random, list);
        Double aDouble = list.stream().max(Double::compareTo).get();
        System.out.println("JDK計算比較結束,集合最大的值爲:"+aDouble);
        stopWatch.stop();
        map.put(taskName,stopWatch.prettyPrint());
        list.clear();

        //TODO CGLIB代理使用
        taskName="CGLIB代理";
        stopWatch = new StopWatch();
        stopWatch.start(taskName);
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CalculatorImpl.class);
        enhancer.setCallback(new CalculatorCGLIBProxy());
        Calculator o1 = (Calculator) enhancer.create();
        addNumToList(o1, random, list);
        System.out.println("CGLIB計算比較結束,集合最大的值爲:"+aDouble);
        stopWatch.stop();
        map.put(taskName,stopWatch.prettyPrint());
        System.out.println(map);
    }


    private void addNumToList(Calculator o, Random random, List<Double> list) {
        for (int i = 1; i < 10000; i++) {
            double a = (random.nextInt(i) + 1) * 1.0;
            double b = (random.nextInt(i) + 1) * 1.0;
            Double result = o.divide(a, b);
            list.add(result);
        }
    }

輸出map的結果如下所示:

{CGLIB代理=StopWatch '': running time = 134290034 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
134290034  100%  CGLIB代理
, JDK代理=StopWatch '': running time = 114593231 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
114593231  100%  JDK代理
}

無論運行多少次testPerformance方法我們總是發現CGLIB的運行時間比JDK的時間長久一些,一般有接口的情況下可以選擇使用JDK動態代理,這也是Spring對接口默認的代理方式。

  1. 下面測試在Spring中兩種代理方式的性能比較,具體代碼如下所示:
  public void runCGLIBTest(ProxyFactory proxyFactory, Calculator calculator, int num, Map<String, Long> map) {
        // 當結果爲true將使用CGLIB代理否則爲JDK代理,默認爲false
        proxyFactory.setProxyTargetClass(true);
        proxyFactory.setFrozen(true);
        testCode(calculator, num, map);
    }

    public void runJDKTest(ProxyFactory proxyFactory, Calculator calculator, int num, Map<String, Long> map) {
        // 當結果爲true將使用CGLIB代理否則爲JDK代理,默認爲false
        proxyFactory.setProxyTargetClass(false);
        proxyFactory.setInterfaces(calculator.getClass().getInterfaces());
        testCode(calculator, num, map);
    }

    public void testCode(Calculator calculator, int num, Map<String, Long> map) {

        StopWatch stopWatch = new StopWatch();
        Random random = new Random();
        stopWatch.start("divide");
        List<Double> list = new ArrayList<>();
        addNumToList(calculator, random, list);
        Double aDouble = list.stream().max(Double::compareTo).get();
        System.out.println("計算比較結束,集合最大的值爲:" + aDouble);
        stopWatch.stop();
        map.put("divide", stopWatch.getLastTaskTimeMillis());
        stopWatch.start("equals");
        for (int i = 0; i < num; i++) {
            calculator.equals(calculator);
        }
        stopWatch.stop();
        map.put("equals", stopWatch.getLastTaskTimeMillis());

        stopWatch.start("hashCode");
        for (int i = 0; i < num; i++) {
            calculator.hashCode();
        }
        stopWatch.stop();
        map.put("hashCode", stopWatch.getLastTaskTimeMillis());


        Advised advised = (Advised) calculator;
        stopWatch.start("hashCode");
        for (int i = 0; i < num; i++) {
            advised.getTargetClass();
        }
        stopWatch.stop();
        map.put("advised.getTargetClass()", stopWatch.getLastTaskTimeMillis());
    }

    @Test
    public void testProxyPerformance() {
        Map<String, Long> map = new HashMap<>();
        Calculator calculator = applicationContext.getBean("calculator", Calculator.class);
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.addMethodName("divide");
        PointcutAdvisor pointcutAdvisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvisor(pointcutAdvisor);
        proxyFactory.setTarget(calculator);
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        runCGLIBTest(proxyFactory, proxy, 5000000, map);
        System.out.println("CGLIB---map:"+map);
        map.clear();
        runJDKTest(proxyFactory, proxy, 5000000, map);
        System.out.println("JDK---map:"+map);
    }

運行結果輸出map結果如下所示:

CGLIB---map:{hashCode=96, equals=24, divide=455, advised.getTargetClass()=12}

JDK---map:{hashCode=113, equals=45 divide=261, advised.getTargetClass()=0}

正如你上面看到的那樣CGLIB代理與JDK代理兩種性能差異不大,唯一就是CGLIB代理不僅可以代理接口也可以代理類,不過需要注意的是如果開啓CGLIB代理接口,需要調用ProxyFactory 實例中的setOptimize方法將optimize 的值設置爲true。

源碼

以上代碼均在 https://github.com/codegeekgao 可以下載查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章