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的动态代理实现的。学习设计模式,也是想让自己可以更快速的看懂源码,并应用在自己的项目中。


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值