JDK動態代理與cglib的使用以及對其效率的統計,以及Mybatis中動態代理的使用

一、什麼是動態代理

代理模式是 Java 中的常用設計模式,代理類通過調用被代理類的相關方法,提供預處理、過濾、事後處理等服務,動態代理及通過反射機制動態實現代理機制。如Spring中使用動態代理完成AOP的操作,Mybatis中使用動態代理完成對Mapper Interface到可用的Mapper Class的生成。

簡單的說我們現在有一個計算的方法,我們在寫代碼時沒有加入時間統計的這個業務,我們如果需要對業務提供計算業務時,我們需要修改原有的這些代碼,並重新侵入一些代碼。這樣如果有很多計算方法我們需要都給它們加上統計時間的業務,我們就得找到所有這些有關於計算的方法,並手動添加時間統計。這樣的做法很油膩。

二、JDK動態代理

無論JDK動態代理還是Cglib動態代理都是用反射去實現的。JDK動態代理是基於接口實現的。其大概的原理就是根據當前對象,獲取對象的class文件,獲取到其接口的類加載器 和 原生對象所擁有的接口  還有一個我們自定義的處理器。再通過實現處理器中的invoke方法,封裝原生對象的方法並得到一個代理後的屬於該接口的實現類。並代碼如下:

首先:我們需要擁有一個接口:

public interface CalculationInterface {

    /**
     * 做計算類
     * */
    public void doCalculation();

}
其次:我們需要一個具體的實現類:這個實現類繼承了上面的接口,並實現了計算的方法。

public class MakeCalculation implements CalculationInterface{

    /**
     *  做計算
     * */
    @Override
    public void doCalculation(){
        System.out.println("做點計算");
        try {
            Thread.sleep(2000); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("計算了兩秒鐘....");
    }

}
第三步:我們需要一個處理器,這個處理器的作用是負責創建一個代理對象。我們去創建代理對象時,JDK已經提供給我們一個接口(InvocationHandler),我們只需要自定義一個處理類實現InvocationHandler接口並實現其中的invoke方法,在invoke方法中選擇我們要去包裝的方法, 對其進行包裝。

第四步:我們需要去獲取代理對象的方法,這個時候就需要有一個原生對象。因爲我們的處理器只是通過反射拿到原生對象的方法,再對原生對象的方法增強或重寫。所以我們在處理器中需要保有一個原生對象(你可能在別的博客中看到處理類中並沒有保有原生對象,其實都是一個道理。我將獲得代理對象的方法封裝到處理類中,以便於用戶獲取代理對象時,可以透明無感。)、

第三步、第四步中代碼如下,除了基礎的JDK代理,我還提供了guava reflect包對JDK動態代理封裝後的調用方法, 以簡化書寫

public class ProxyHandler implements InvocationHandler{


    private MakeCalculation target;

    private void setTarget(MakeCalculation target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if("doCalculation".equals(method.getName())){
            long start = System.currentTimeMillis();
            method.invoke(target,args);
            long end = System.currentTimeMillis();
            System.out.println(end - start);
        }else{
            method.invoke(proxy,args);
        }

        return null;
    }

    /**
     *用戶提供一個子類去創建一個子類的代理類
     */
    public CalculationInterface getProxy(MakeCalculation target){
        setTarget(target);
        return (CalculationInterface)Proxy.newProxyInstance(CalculationInterface.class.getClassLoader(),target.getClass().getInterfaces(),this);
    }

    /**
     * 用戶直接獲取代理對象,我們內部獲取要被代理的對象,並對其代理
     * */
    public CalculationInterface getProxyWithOutTarget(){
        setTarget(new MakeCalculation());
        return (CalculationInterface)Proxy.newProxyInstance(CalculationInterface.class.getClassLoader(),target.getClass().getInterfaces(),this);
    }

    /**
     * 用guava簡化原生JDK代理語法
     * 需要導入com.google.common.reflect.Reflection;
     * */
    public CalculationInterface getProxyWithGuava(){
        setTarget(new MakeCalculation());
        return Reflection.newProxy(CalculationInterface.class,this);
    }
}
第五步:測試類

/**
 *  JDK動態代理測試類
 */
public class ProxyTest {

    @Test
    public void proxy(){

        ProxyHandler proxyHandler = new ProxyHandler();
        MakeCalculation calculation = new MakeCalculation();

        System.out.println("代理前的原對象輸出的結果----");
        calculation.doCalculation();

        // 用戶傳入子類獲取代理
        System.out.println();
        System.out.println("用戶自傳原生類生成代理 -- 代理對象輸出的結果----");
        proxyHandler.getProxy(calculation).doCalculation();

        // 用戶直接獲取代理後的對象
        System.out.println();
        System.out.println("封裝原生類  -- 用戶透明版 --  代理對象輸出的結果----");
        proxyHandler.getProxyWithOutTarget().doCalculation();

        // 使用guava簡化JDK動態代理語法
        System.out.println();
        System.out.println("使用guava簡化JDK動態代理語法   --  代理對象輸出的結果----");
        proxyHandler.getProxyWithGuava().doCalculation();

    }
}
輸出結果

代理前的原對象輸出的結果----
做點計算
計算了兩秒鐘....

用戶自傳原生類生成代理 -- 代理對象輸出的結果----
做點計算
計算了兩秒鐘....
2000

封裝原生類  -- 用戶透明版 --  代理對象輸出的結果----
做點計算
計算了兩秒鐘....
2000

使用guava簡化JDK動態代理語法   --  代理對象輸出的結果----
做點計算
計算了兩秒鐘....
2001

Process finished with exit code 0

三、cglib動態代理

Cglib動態代理與JDK動態代理完成的目標一樣,只不過Cglib動態代理不需要一個接口,只需要一個實現類,就可以完成對此對象的代理。具體代碼如下:

首先一個計算類: 因爲Cglib不需要接口,所以我們直接實現一個計算類

public class CglibTarget {
    /**
     *  做計算
     * */
    public void doCalculation(){
        System.out.println("做點計算");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("計算了兩秒鐘....");
    }
}
動態代理處理類以及獲取代理對象:

/**
 * Cglib 動態代理的處理類
 * */
public class TargetInterceptor implements MethodInterceptor {

    /**
     * Cglib 動態代理的處理方法
     * */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        // 方法調用返回值
        Object result = null;

        if("doCalculation".equals(method.getName())){
            System.out.println("計算開始");
            result = methodProxy.invokeSuper(o,objects);
            System.out.println("計算結束");
        }else{
            result = methodProxy.invokeSuper(o,objects);
        }

        return result;
    }

    public CglibTarget getProxy(){
        // Enhancer 爲 Cglib中的字節碼增強器,將目標類和動態處理類傳入此增強器,調用增強器的create方法返回代理對象
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CglibTarget.class);
        enhancer.setCallback(this);
        return (CglibTarget)enhancer.create();
    }
測試類:

public class CglibProxyTest {

    @Test
    public void testProxy(){
        new TargetInterceptor().getProxy().doCalculation();
    }
    
}

四、JDK動態代理及Cglib的效率比較

在面試現在工作的這家公司時,面試官問了我一個問題。就是JDK動態代理和Cglib誰更快。當時我聽到這個問題就尷尬了,因爲的確沒有關注過這類問題。既然我們上面講述了JDK動態代理和Cglib動態代理,那我們就做一個小測試,看看他們的效率誰更快。

改寫JDK動態代理測試類:

  @Test
    public void speed(){
        Long startTime = System.currentTimeMillis();
        new ProxyHandler().getProxyWithOutTarget().doCalculation();
        Long endTime  = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
運行五次 結果分別爲:

2004 、2003、2004、2004、2003

可以得知,JDK動態代理完成整個代理過程大概4毫秒左右

再來測試Cglib:

首先改寫測試類

  @Test
    public void speed(){
        Long startTime = System.currentTimeMillis();
        new TargetInterceptor().getProxy().doCalculation();
        Long endTime  = System.currentTimeMillis();
        System.out.println("cglib -- :"+ (endTime - startTime));
    }
運行五次結果如下:

2172、2208、2154、2135、2169

可以看出,Cglib完成整個代理大概要 150毫秒左右。明顯JDK的速度要快很多。本次測試JDK動態代理要比Cglib快近40倍,這個性能差距也是蠻大的。

本次測試Cglib使用版本 2.2.2

JDK使用版本 1.8

感興趣的同學可以使用別的版本試一試他們的效率差距,留言給我呦!

五、MyBatis中使用JDK動態代理動態生成Mapper實現類

在MyBatis的使用中,我們只定義了Mapper接口,在Mapper接口中去定義了一些CRUD的方法,但是我們卻從來沒有去實現過Mapper。問題來了,誰去幫我們做了這件事呢?那必須就是MyBatis啊。在MyBatis中有一個MapperProxy類去完成這件事。

讓我們看一下源碼:

首先去獲取代理類:

 //org.apache.ibatis.binding.MapperProxyFactory
 protected T newInstance(MapperProxy<T> mapperProxy) {
   return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
 }
讓我們看看MapperProxy

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

  @UsesJava7
  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  /**
   * Backport of java.lang.reflect.Method#isDefault()
   */
  private boolean isDefaultMethod(Method method) {
    return ((method.getModifiers()
        & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
        && method.getDeclaringClass().isInterface();
  }
}
看源碼需要定焦,你看這端代碼主要想從代碼中獲得什麼,只要想知道代碼中的哪部分是如何實現的。從上面的源碼中,我們可以到看到,MyBatis也是使用了JDK的動態代理。

MyBatis中判斷是否是一個類,如果是一個類,那麼就直接傳遞方法和參數調用即可。但我們知道此時是一個接口(也可以自己實現接口,舊版本通常這樣做)。如果不是一個類的話,就會創建一個MapperMethod方法。見名思意:好像就是這個類在執行我們所調用的每一個接口方法。最後返回的是MapperMethod.execute方法。暫時不予理會MapperProxy類中的cachedMapperMethod方法。

具體MyBatis源碼解讀請參見http://www.cnblogs.com/yulinfeng/p/6063974.html

這個系列他寫的很好,我就不重複造輪子了。第五部分只想告訴大家在MyBatis中對於Dao層的Mapper接口是使用JDK的動態代理實現的。學習設計模式,也是想讓自己可以更快速的看懂源碼,並應用在自己的項目中。


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