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