05. 动态代理
05. 动态代理
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。
实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)、Javassist 等。
静态代理
静态代理就是设计模式中的代理模式:主要为其他对象提供一种代理,并控制对这个对象的访问。
示例:
定义Subject抽象类,包含RealSubject和Proxy的公共接口,这样就在任何使用RealSubject的地方都可以使用Proxy。
1 | public abstract class Subject { |
RealSubject定义Proxy,代理的真正实体:
1 | public class RealSubject extends Subject{ |
Proxy代理类保存了一个引用,使得代理可以访问真实的实体,并提供一个和Subjcet接口相同的接口,这样代理就可以用来代替实体:
1 | public class Proxy extends Subject{ |
最后,实现:
1 | public class Test { |
结果:
1 | 真实的请求 |
静态代理的问题
静态代理模式固然在访问无法访问的资源,能够增强现有的接口业务功能方面有很大有点,但是大量使用这种静态代理,会使我们的系统内的类规模增大,并且不易维护;
从本质上看,示例中Proxy和RealSubject功能一样,Proxy只是中介的作用,这种代理在系统中的存在,导致系统结构比较臃肿和松散。
JDK动态代理
为了解决静态代理的问题,动态代理思想提了出来。
以示例为例子,动态代理在运行状态中,需要代理的地方,会根据 Subject 和 RealSubject,动态地创建一个 Proxy,用完之后,就会销毁,这样就可以避免了 Proxy 角色的 class 在系统中冗杂的问题了。
动态代理属于经典的代理模式,需要引入一个InvocationHandler,它负责统一管理所有的方法调用。(其实就是一个接口,这个接口就一个invoke方法,参数固定的三个参数,下面会讲)。
动态代理步骤
- 获取RealSubject上的所有接口列表(执行invoke()方法的时候要用);
- 确定要生成的代理类的类名,默认为:com.sum.proxy.$ProxyXXXX;
- 根据需要实现的接口信息,在代码中动态创建该Proxy类的字节码;
- 将对应的字节码转换成对应的class对象;
- 创建InvocationHandler实例handler,用来处理Proxy的所有方法调用
- Proxy的class对象以创建handler对象为参数,实例化一个Proxy对象。
从上面可以看出,JDK 动态代理的实现是基于实现接口的方式,使得 Proxy 和 RealSubject 具有相同的功能。
但其实还有一种思路:通过继承。即:让 Proxy 继承 RealSubject,这样二者同样具有相同的功能,Proxy 还可以通过重写 RealSubject 中的方法,来实现多态。CGLIB 就是基于这种思路设计的。
在上述步骤中,最重要的有两个类,一个是InvocationHandler接口,另外一个是Proxy类。
InvocationHandler接口
1 | public interface InvocationHandler { |
只有这一个方法。每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个Handler,当我们通过代理对象调用一个方法的时候,这个方法调用会被转发有InvocationHandler这个接口的invoke方法来调用。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
参数说明:
- proxy - 代理的真实对象。
- method - 所要调用真实对象的某个方法的
Method
对象 - args - 所要调用真实对象某个方法时接受的参数
Proxy类
Proxy
这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance
这个方法:
1 | public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException |
这个方法的作用就是得到一个动态的代理对象。
参数说明:
- loader - 一个
ClassLoader
对象,定义了由哪个ClassLoader
对象来对生成的代理对象进行加载。 - interfaces - 一个
Class<?>
对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了 - h - 一个
InvocationHandler
对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler
对象上
示例
首先,定义Subject接口,声明两个方法:
1 | public interface Subject { |
定义真实对象RealSubject类:
1 | public class RealSubject implements Subject{ |
定义动态代理类,实现 InvocationHandler 接口:
1 | public class InvocationHandlerDemo implements InvocationHandler { |
最后,实现Client类:
1 | public class Client { |
输出:
1 | com.sun.proxy.$Proxy0 |
输出说明:
com.sun.proxy.$Proxy0?
为什么会返回代理对象的类名会是这样的?
1 | Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject |
可能我以为返回的这个代理对象会是 Subject 类型的对象,或者是 InvocationHandler 的对象,结果却不是,首先我们解释一下为什么我们这里可以将其转化为 Subject 类型的对象?
原因就是:在 newProxyInstance 这个方法的第二个参数上,我们给这个代理对象提供了一组什么接口,那么我这个代理对象就会实现了这组接口,这个时候我们当然可以将这个代理对象强制类型转化为这组接口中的任意一个,因为这里的接口是 Subject 类型,所以就可以将其转化为 Subject 类型了。
同时我们一定要记住,通过 Proxy.newProxyInstance
创建的代理对象是在 jvm 运行时动态生成的一个对象,它并不是我们的 InvocationHandler 类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy 为中,最后一个数字表示对象的标号。
接着我们来看看这两句
1 | subject.hello("world"); |
这里是通过代理对象来调用实现的那种接口中的方法,这个时候程序就会跳转到由这个代理对象关联到的 handler 中的 invoke 方法去执行,而我们的这个 handler 对象又接受了一个 RealSubject 类型的参数,表示我要代理的就是这个真实对象,所以此时就会调用 handler 中的 invoke 方法去执行。
在真正通过代理对象来调用真实对象的方法的时候,我们可以在该方法前后添加自己的一些操作(与AOP切面的环绕通知类似),同时我们看到我们的这个 method 对象是这样的:
1 | public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String) |
正好就是我们的 Subject 接口中的两个方法,这也就证明了当我通过代理对象来调用方法的时候,起实际就是委托由其关联到的 handler 对象的 invoke 方法中来调用,并不是自己来真实调用,而是通过代理的方式来调用的。
动态代理总结
代理类与委托类实现同一接口,主要是通过代理类实现 InvocationHandler
并重写 invoke
方法来进行动态代理的,在 invoke
方法中将对方法进行处理。
JDK 动态代理特点:
- 优点:相对于静态代理模式,不需要硬编码接口,代码复用率高。
- 缺点:强制要求代理类实现
InvocationHandler
接口。
CGLIB动态代理
这里只是简单介绍CGlib的动态代理
CGLIB 提供了与 JDK 动态代理不同的方案。很多框架,例如 Spring AOP 中,就使用了 CGLIB 动态代理。
CGLIB 底层,其实是借助了 ASM 这个强大的 Java 字节码框架去进行字节码增强操作。
CGLIB 动态代理的工作步骤:
- 生成代理类的二进制字节码文件;
- 加载二进制字节码,生成
Class
对象( 例如使用Class.forName()
方法 ); - 通过反射机制获得实例构造,并创建代理类对象。
CGLIB 动态代理特点:
优点:使用字节码增强,比 JDK 动态代理方式性能高。可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口。
缺点:不能对 final
类以及 final
方法进行代理。