0
点赞
收藏
分享

微信扫一扫

动态代理:JDK 与 Cglib

圣杰 2022-07-28 阅读 56

动态代理的出镜率非常高,不论是在框架中的应用,还是在面试中,都频繁出现。

因此,弄懂动态代理的来龙去脉,是理解框架的基础,也是进阶路上绕不过去的垫脚石。

一、静态代理

先聊下静态代理,也就是代理模式的出现解决了什么问题?

现实生活中,保姆是家庭事务的代理,经纪人是明星的代理,代理服务于被代理人,一般是在某类事物上更专业的人。

在代码中,来模拟下雇佣保洁来打扫房子的场景,​​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<Person>​​​ ,放在方法区中,再根据 ​​Class<Person>​​​ 实例化成 ​​person​​ 对象,放在了堆中。

动态代理:JDK 与 Cglib_cglib

​Class<Order>​​ 是 ​​java.lang​​ 中的类,描述的是类的原始信息,例如类定义了哪些成员变量,方法,字段等。

所以,要实例化一个对象,需要拿到它的 ​​Class<>​​,一般情况下,是从 ​​.class​​ 文件中加载的。

是否可以凭空创造出来呢?

答案是可以,因为目标类与代理类的信息基本一致,直接从接口的 ​​Class<>​​ 中复制一份便可。

动态代理:JDK 与 Cglib_cglib_02

JDK 动态代理

这就是 JDK 动态代理的核心思想。

其中,完成这个过程的核心类为 ​​Proxy​​ 和 ​​InvocationHandler​​。

​Proxy​​ 中的 ​​getProxyClass()​​ 用来获得 ​​Class<>​​。

​InvocationHandler​​ 是一个钩子,代理对象生成后,执行方法时会先回调 ​​InvocationHandler​​ 的 ​​invoke()​​。

来看下具体的写法:

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​​ 用继承目标类的方式,给出了自己的答案。

核心思想是作为子类来增强目标类的方法,而不是通过实现接口的形式。

其中,核心类是 ​​Enhancer​​ 和 ​​MethodInterceptor​​,相当于 ​​Proxy​​ 和 ​​InvocationHandler​​。

如下所示,​​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_cglib_03

如图所示,首先调用的是代理类的 ​​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_cglib_04

可以看到,代理类继承了目标类 ​​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_cglib_05

这里不是直接根据方法名称判断,方法名是可以重复的,所以根据方法签名做了一层映射,用映射后的 Id 来表示。

类的方法个数是有限的,提前记录并给与索引,避免使用过重的反射机制,属于将空间换时间玩出了花来。

总结下,​​cglib​​ 通过继承目标类,成为目标类的子类来扩展功能,实际调用的过程中,还用了 ​​FastClass​​ 来优化性能。

三、JDK vs cglib

接下来,对比下 ​​JDK​​ 方式和 ​​cglib​​ 的方式。

最直观的,对目标类的限制上,​​JDK​​ 的方式要求必须实现接口,无接口不代理,而 ​​cglib​​ 则另辟蹊径,用继承的方式来实现代理,也有一定的限制,例如被 ​​final​​ 修饰的类无法代理。

其次,在实现机制上,​​JDK​​ 用了反射机制运行目标方法,而 ​​cglib​​ 则是通过 ​​FastClass​​ 的方式,优化调用过程。

性能上,​​cglib​​ 理论上讲是更快的,当在一般业务场景中,类的数量有限,一般不会有太大的差距。

使用方式上,​​JDK​​ 的方式是原生的,写法简单,不用引入其它依赖,可以平滑升级,而 ​​cglib​​ 是三方包,需要投入更多的维护成本。

具体选型过程,在可维护性、可靠性、性能、以及工作量上要多加考量。

四、应用场景

对实现原理理解后,再看平时应用的东西,比如 ​​RPC​​ 调用时的接口,比如写 ​​mybatis​​ 为何只写接口就可以,不用写实现?还有 ​​spring AOP​​ 机制,都是基于动态代理实现的。

在这基础上,更高阶的玩法是代理链,例如 ​​mybatis​​ 里面的拦截器,多个的情况下,就是代理套代理,层层代理,跟套娃一样。

五、总结

从代理模式,到动态代理,增强代码的同时,解决了代码侵入,与繁琐工作的问题。​​JDK​​ 和 ​​cglib​​ 从不同视角给出了解决方案,也有着不同的优势和局限性。

其他

文中示例代码都在 github 上,欢迎把玩:

​​https://github.com/JayeGuo/JavaDemo​​

举报

相关推荐

0 条评论