动态代理(JDK 和 Cglib)

动态代理的出镜率非常高,不论是在框架中的应用,还是在面试中,都频繁出现。
因此,弄懂动态代理的来龙去脉,是理解框架的基础,也是进阶路上绕不过去的垫脚石。
一、静态代理 先聊下静态代理,也就是代理模式的出现解决了什么问题?
现实生活中,保姆是家庭事务的代理,经纪人是明星的代理,代理服务于被代理人,一般是在某类事物上更专业的人。
在代码中,来模拟下雇佣保洁来打扫房子的场景,CleanProxyPerson 是保洁,Person 代表业主,CleanThing 代表协商好的清洁范围,如下所示,运行后,cleanHouse() 方法会被增强,不在接口中的 cleanSafeBox() 是无法被代理的。
接口的一个作用,是作为协议,定义职责,例如把生孩子这种事情也放到接口里面代理,明显是不合适的。

public class MainOfUndynamicProxy { public static void main(String[] args) { CleanThing person = new Person(); CleanThing proxyPerson = new CleanProxyPerson(person); proxyPerson.cleanHouse(); } } // 业主 public class Person implements CleanThing{@Override public void cleanHouse() { System.out.println("自己打扫下房间的核心区域"); }public void cleanSafeBox() { System.out.println("打扫下保险箱"); }public Person getChild(){ return new Person(); } }public class CleanProxyPerson implements CleanThing {private CleanThing cleanThing; public CleanProxyPerson(CleanThing cleanThing) { this.cleanThing = cleanThing; }@Override public void cleanHouse() { System.out.println("----- 整体清洁下(专业人士) -----"); cleanThing.cleanHouse(); } }public interface CleanThing { void cleanHouse(); }

代理的应用,以接口为纽带,与目标类解耦的同时,达到了增强目标类的目的。
实际业务场景中,接口与实现各式各样,如果都有增强需求,例如做调用统计,耗时统计等,用这种方式需要一个个写,显然是不现实的。
二、动态代理 动态代理解决的就是工作量的问题。
一般来说,要省掉编写代码的工作,需要在编译时或运行时运用点黑科技。
先看下对象实例化的过程。
如下所示,要实例化 Person 对象,首先 Person.java 被编译成 Person.class 文件,接着被 ClassLoader 加载到 JVM 中,生成了 Class ,放在方法区中,再根据 Class 实例化成 person 对象,放在了堆中。
动态代理(JDK 和 Cglib)
文章图片

Classjava.lang 中的类,描述的是类的原始信息,例如类定义了哪些成员变量,方法,字段等。
所以,要实例化一个对象,需要拿到它的 Class<>,一般情况下,是从 .class 文件中加载的。
是否可以凭空创造出来呢?
答案是可以,因为目标类与代理类的信息基本一致,直接从接口的 Class<> 中复制一份便可。
动态代理(JDK 和 Cglib)
文章图片

JDK 动态代理
这就是 JDK 动态代理的核心思想。
其中,完成这个过程的核心类为 ProxyInvocationHandler
Proxy 中的 getProxyClass() 用来获得 Class<>
InvocationHandler 是一个钩子,代理对象生成后,执行方法时会先回调 InvocationHandlerinvoke()
来看下具体的写法:
public class MainOfJDKProxy {public static void main(String[] args) { IOrder order = new Order(); // 目标类 LogInvocationHandler handler = new LogInvocationHandler(order); // 回调函数 Class proxyClass = Proxy.getProxyClass(order.getClass().getClassLoader(), order.getClass().getInterfaces()); // 这里就是 copy Class<> Constructor constructor = proxyClass.getConstructor(InvocationHandler.class); IOrder proxyOrder = (IOrder) constructor.newInstance(handler); proxyOrder.run(); } }public interface IOrder { void run(); }public class Order implements IOrder {@Override public void run() { System.out.println("Order run"); } }public class LogInvocationHandler implements InvocationHandler {private Object targetObject; public LogInvocationHandler(Object targetObject){ this.targetObject = targetObject; }@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("----- LogInvocationHandler begin -----"); method.invoke(targetObject, args); System.out.println("----- LogInvocationHandler end -----"); return null; } }

其实还有个更简便的方法,用 Proxy.newProxyInstance() 可以直接返回代理对象,底层原理类似。
public class MainOfJDKProxy {public static void main(String[] args) { IOrder order = new Order(); // 目标类 LogInvocationHandler handler = new LogInvocationHandler(order); // 回调函数 IOrder proxyOrder = (IOrder) Proxy.newProxyInstance(order.getClass().getClassLoader(), order.getClass().getInterfaces(), handler); proxyOrder.run(); } }

所以,代理模式是为了增强业务代码,JDK 用了反射机制,复制类的元信息来实例化代理类,减少了手动编写的问题。
但是,目标类需要实现接口这个限制在使用上还是有很大的局限性,是否有其它解法呢?
Cglib:Code Generation Library
cglib 用继承目标类的方式,给出了自己的答案。
核心思想是作为子类来增强目标类的方法,而不是通过实现接口的形式。
其中,核心类是 EnhancerMethodInterceptor,相当于 ProxyInvocationHandler
如下所示,Enhancer 设置父类和回调函数,创建出代理对象。
Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Item.class); // 设置父类 enhancer.setCallback(new LogMethodInterceptor(new Item())); // 回调函数 Item proxyItem = (Item) enhancer.create(); proxyItem.run();

回调函数的实现是这样的:
public class LogMethodInterceptor implements MethodInterceptor {private Object targetObject; public LogMethodInterceptor(Object targetObject){ this.targetObject = targetObject; }@Override public Object intercept(Object proxyObject, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("----- LogMethodInterceptor begin -----"); Object result = method.invoke(targetObject, args); System.out.println("----- LogMethodInterceptor end -----"); return result; } }

这个写法跟 JDK 的类似,在创建的时候,将目标对象存为成员变量,真正回调的时候,通过反射 invoke 对应的方法。
intercept() 有4个入参,proxyObject 是代理,method 是目标类的方法,args 是方法入参,methodProxy 是代理方法。
其它的都好理解,基本跟 JDK 的回调方法一致,但多了个 methodProxy ,为什么要有它呢?
如果这样写,其实就是调用的代理类的代理方法,而不是直接调用目标类,invokeSuper()invoke() 的区别在于是否继续走 intercept() ,所以,invoke() 会造成一个死循环,相当于递归调用了。
public class LogSuperMethodInterceptor implements MethodInterceptor {@Override public Object intercept(Object proxyObject, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("----- LogMethodInterceptor begin -----"); Object result = methodProxy.invokeSuper(proxyObject, args); //Object result = methodProxy.invoke(obj, args); // 死循环 System.out.println("----- LogMethodInterceptor end -----"); return result; } }

动态代理(JDK 和 Cglib)
文章图片

如图所示,首先调用的是代理类的 run() 方法,然后回调到拦截器的 intercept() 方法,这步是 cglib 自动实现的。
所以在 intercept() 中如果继续调用代理方法,就会走图中的③,然后自动又走②,导致死循环。
③ 不让用,那 ④ 和 ⑤ 的区别是啥呢?看起来都是调用目标类的方法。
这的本质区别,就是通过子类去调用父类方法与直接调用父类方法的区别,体现在了关键字 this 上。
如下所示,如果 run() 调用 runElse() 是子类调用过来的,这里的 this 是指的子类,而不是父类,这一点会比较反直觉。
public class Item {public void run(){ System.out.println("item run"); this.runElse(); }public void runElse(){ System.out.println("item run else"); } }

所以最终体现的就是嵌套调用的时,还会不会走到代理的方法上,进而又走到回调方法里面,这样就会让整个调用链路上的方法都被增强了。
弄懂这个后,来继续看看底层实现,在首行加下面这行代码,可以看到编译后的代理类文件。
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "target/temp");

动态代理(JDK 和 Cglib)
文章图片

可以看到,代理类继承了目标类 Order ,成员变量中可以看到传入的拦截器。
图一发现两个带有 FastClass 文件是用来干啥的?
FastClass 机制,用另外一种思路来达到反射调用的效果。
通过反射调用对象的具体方法时,一般这么写:
public class MainOfReflection { public static void main(String[] args) throws Throwable { Person person = new Person(); invokeByName(person, "cleanHouse"); invokeByName(person, "cleanSafeBox"); Order order = new Order(); invokeByName(order, "run"); }public static void invokeByName(Object object, String methodName) throws Throwable{ Class aClass = Class.forName(object.getClass().getName()); Method method = aClass.getMethod(methodName, new Class[0]); method.invoke(object, null); } }

但是,反射的操作是比较重的,一般要经过鉴权,native 等的调用。
FastClass 用空间换时间的思路,将要调用的方法存下来,放到文件中。
生成的文件,记录了一份类的方法列表,可以想象成数据库记录,方法名就是索引,这样要运行指定方法名的时候,根据方法名去调用对应的方法。
public static void invokeByName(Person object, String methodName){ if ("cleanHouse".equals(methodName)) { object.cleanHouse(); } else if ("cleanSafeBox".equals(methodName)) { object.cleanSafeBox(); } }

动态代理(JDK 和 Cglib)
文章图片

这里不是直接根据方法名称判断,方法名是可以重复的,所以根据方法签名做了一层映射,用映射后的 Id 来表示。
类的方法个数是有限的,提前记录并给与索引,避免使用过重的反射机制,属于将空间换时间玩出了花来。
总结下,cglib 通过继承目标类,成为目标类的子类来扩展功能,实际调用的过程中,还用了 FastClass 来优化性能。
三、JDK vs cglib 接下来,对比下 JDK 方式和 cglib 的方式。
【动态代理(JDK 和 Cglib)】最直观的,对目标类的限制上,JDK 的方式要求必须实现接口,无接口不代理,而 cglib 则另辟蹊径,用继承的方式来实现代理,也有一定的限制,例如被 final 修饰的类无法代理。
其次,在实现机制上,JDK 用了反射机制运行目标方法,而 cglib 则是通过 FastClass 的方式,优化调用过程。
性能上,cglib 理论上讲是更快的,当在一般业务场景中,类的数量有限,一般不会有太大的差距。
使用方式上,JDK 的方式是原生的,写法简单,不用引入其它依赖,可以平滑升级,而 cglib 是三方包,需要投入更多的维护成本。
具体选型过程,在可维护性、可靠性、性能、以及工作量上要多加考量。
四、应用场景 对实现原理理解后,再看平时应用的东西,比如 RPC 调用时的接口,比如写 mybatis 为何只写接口就可以,不用写实现?还有 spring AOP 机制,都是基于动态代理实现的。
在这基础上,更高阶的玩法是代理链,例如 mybatis 里面的拦截器,多个的情况下,就是代理套代理,层层代理,跟套娃一样。
五、总结 从代理模式,到动态代理,增强代码的同时,解决了代码侵入,与繁琐工作的问题。JDKcglib 从不同视角给出了解决方案,也有着不同的优势和局限性。
其他
文中示例代码都在 github 上,欢迎把玩:
https://github.com/JayeGuo/Ja...

    推荐阅读