当疯狂慢慢从爱情离开
前言
什么是代理 (模式)?
代理模式 (Proxy Pattern) 也称委托模式 (Deletage Pattern),属于结构型设计模式,也是一项基本的设计技巧。通常,代理模式用于处理两种问题:
- 控制对基础对象的访问
- 在访问基础对象时增加额外功能
代理的基本分类: 静态代理 + 动态代理,分类的标准是 “代理关系是否在编译期确定;
动态代理的实现方式: JDK、CGLIB、Javassist、ASM
静态代理
定义
静态代理是指代理关系在编译期确定的代理模式。使用静态代理时,通常的做法是为每个业务类抽象一个接口,对应地创建一个代理类。
Cilent调用Source的method()方法,实际上是Proxy来调用method()方法,静态代理中Source跟Proxy都要实现接口Sourceable。
看一个例子
父接口 Animal.java
1 | package Proxy; |
Cat类
1 | package Proxy; |
CatProxy类
1 | package Proxy; |
如果我想再创建一个Dog对象,又需要重新为Dog创建一个代理对象,如下:
Dog.java
1 | package Proxy; |
DogProxy类
1 | package Proxy; |
TestStaticProxy.java
1 | package Proxy; |
运行如下:
每次我要新加入一个实现Animal接口的对象的话,都要重新创建一个代理对象,这样会非常的麻烦,这其实是静态代理的缺点。
动态代理
动态代理有什么作用及应用场景?
- 日志集中打印
- 事务
- 权限管理
- AOP
定义
动态代理是指代理关系在运行时确定的代理模式。需要注意,JDK 动态代理并不等价于动态代理,前者只是动态代理的实现之一,其它实现方案还有:CGLIB 动态代理、Javassist 动态代理和 ASM 动态代理等。因为代理类在编译前不存在,代理关系到运行时才能确定,因此称为动态代理。
JDK动态代理基于拦截器和反射来实现。
JDK代理是不需要第三方库支持的,只需要JDK环境就可以进行代理,使用条件:
1)必须实现InvocationHandler接口;
2)使用Proxy.newProxyInstance产生代理对象;
3)被代理的对象必须要实现接口;
JDK的动态代理需要了解两个类
核心 : InvocationHandler 调用处理程序类和 Proxy 代理类
InvocationHandler:调用处理程序
1 | public interface InvocationHandler |
InvocationHandler是由代理实例的调用处理程序实现的接口
每个代理实例都有一个关联的调用处理程序。
1 | Object invoke(Object proxy, 方法 method, Object[] args); |
当在代理实例上调用方法的时候,方法调用将被编码并分派到其调用处理程序的invoke()
方法。
参数:
proxy
– 调用该方法的代理实例method
-所述方法对应于调用代理实例上的接口方法的实例。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。args
-包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数。原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer
或java.lang.Boolean
。
Proxy : 代理
1 | public class Proxy extends Object implements Serializable |
Proxy
提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。
动态代理类 (以下简称为代理类 )是一个实现在类创建时在运行时指定的接口列表的类,具有如下所述的行为。 代理接口是由代理类实现的接口。 代理实例是代理类的一个实例。
1 | public static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException |
返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
参数
loader
– 类加载器来定义代理类interfaces
– 代理类实现的接口列表h
– 调度方法调用的调用处理函数
也看一个例子
还是上一个例子,作者的下一篇文章要vip了,其他动态代理又看不懂,让GPT写一个尝试理解吧。
使用JDK动态代理的五大步骤:
1)通过实现InvocationHandler接口来自定义自己的InvocationHandler;
2)通过Proxy.getProxyClass获得动态代理类;
3)通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class);
4)通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入;
5)通过代理对象调用目标方法;
要使用静态代理中的Animal.java,Cat.java,Dog.java
新建两个类
AnimalProxy类
1 | package Proxy; |
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
:实现 InvocationHandler
接口的方法,负责处理动态代理的调用逻辑。在这个方法中,我们可以在目标方法执行前后插入自定义逻辑。具体逻辑如下:
- 输出代理类执行开始的日志。
- 调用目标对象的相应方法,即
method.invoke(target, args)
。 - 输出代理类执行结束的日志。
- 返回方法的执行结果。
这个代理类的主要作用是在代理的目标对象执行前后输出日志,类似于横切关注点(cross-cutting concerns),这是 AOP(面向切面编程)的一种常见应用。代理类通过实现 InvocationHandler
接口,使得我们可以在运行时动态地为目标对象创建代理,而不需要为每个类都手动创建一个专门的代理类。
在 TestDynamicProxy.java
中,我们通过使用 Proxy.newProxyInstance
方法来动态创建代理对象,并传入 AnimalProxy
作为 InvocationHandler
。这样,无论是 Cat
还是 Dog
都可以使用同一个代理类进行代理,而不需要为每个类都创建一个专门的代理类。
TestDynamicProxy类
1 | package Proxy; |
运行结果:
JDK 动态代理源码分析
我看的是JDK1.8
API概述
Proxy 类主要包括以下 API:
Proxy | 描述 |
---|---|
getProxyClass(ClassLoader, Class…) : Class | 获取实现目标接口的代理类 Class 对象 |
newProxyInstance(ClassLoader,Class<?>[],InvocationHandler) : Object | 获取实现目标接口的代理对象 |
isProxyClass(Class<?>) : boolean | 判断一个 Class 对象是否属于代理类 |
getInvocationHandler(Object) : InvocationHandler | 获取代理对象内部的 InvocationHandler |
核心源码
Proxy.java
获取代理类 Class 对象
1 | public static Class<?> getProxyClass(ClassLoader loader, |
实例化代理类对象
1 | public static Object newProxyInstance(ClassLoader loader, |
实例化代理对象也需要先通过 getProxyClass0(…) 获取代理类 Class 对象,而 newProxyInstance(…) 随后会获取参数为 InvocationHandler 的构造函数实例化一个代理类对象。
先看下代理类 Class 对象是如何获取的:
1 | /** |
看看proxyClassCache实例
1 | private static final WeakCache<ClassLoader, Class<?>[], Class<?>> |
看到WeakCache.java的120行,这个apply的实现类的ProxyClassFactory(代理类的工厂)再次回到Proxy类
注意这个时候在这里加一个断点调试可以看到proxy的值为$Proxy@537为什么呢?
接着看
1 | public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { |
视线放到这里,其实就行进行了一个拼接来获取名字。这个名字就是代理对象的名字!
接下来的这行代码用来生成字节码的文件,然后加载到class文件
到这里代理对象以及生成了。
那我们在TestDynamicProxy.java的main方法中添加一些代码来看到生成的字节码文件
1 | byte[] classFile = sun.misc.ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{Animal.class}); |
可以看到提示成功生成了.class文件
接下来看看IDEA反编译的内容
1 | // |
注意看这个breath方法,可以看出来调用TestDynamicProxy类的xxxProxy.breath()方法实则在调用invoke方法。
invoke执行的这个h是继承于Proxy类里面的
InvocationHandler接口只有一个invoke方法
调用代理方法是就是AnimalProxy类的invoke方法。这样就是根据源码以及运行结果都可以看出来。
在动态代理中InvocationHandler是核心,每个代理实例都具有一个关联的调用处理程序(InvocationHandler)。
对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序(InvocationHandler)的invoke()方法。
所以对代理方法的调用都是通InvocationHadler的invoke来实现中,而invoke方法根据传入的代理对象,
方法和参数来决定调用代理的哪个方法。
AOP与动态代理
Spring框架中有用到,相信后面在学习Spring框架安全的时候还会再遇到,此处作为扩展了解即可。
AOP(Aspect Orient Programming,面向切面编程) 假如有三个代码段中都有相同的代码,如下图所示:
这时为了简化代码,我们会将相同的代码封装到一个方法中,比如这样:
有这样一种情况,某几段代码段,前后的代码是固定一样的,中间的一段代码会调用不同方法,比如这样:
这时简化代码就需要用到动态代理。在动态代理中增加通用方法,中间变化的代码就进行动态调用。
JDK动态代理和CGLIB动态代理区别
在于JDK自带的动态代理,必须要有接口,而CGLIB动态代理有没有接口都可以。
- JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib动态代理:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。(CGLIB 通过动态生成一个需要被代理类的子类(即被代理类作为父类),该子类重写被代理类的所有不是 final 修饰的方法,并在子类中采用方法拦截的技术拦截父类所有的方法调用,进而织入横切逻辑。)
没有实现接口或者不需要实现接口的类,我们可以通过cglib动态代理对它进行代理。
基于cglib实现的动态代理需要引入第三方cglib库,之后我们可以实现基于子类的动态代理。
使用cglib实现的动态代理也有一个约束条件,就是被代理类不能被final修饰,
使用cglib实现的动态代理核心是Enhancer类,仔细观察我们会发现cglib动态代理的实现过程和JDK实现动态代理的过程极其类似。
在反序列化中动态代理的作用
虽然Java安全还没怎么开始,但是还是先了解一下吧,毕竟学这些开发都是为了安全。
据说在LazyMap CC1链和JDK7U21这两条链子会涉及到动态代理。
说要利用反序列化的漏洞,我们是需要一个入口类的。
我们先假设存在一个能够漏洞利用的类为B.f
,比如Runtime.exec
这种。
我们将入口类定义为A
,我们最理想的情况是 A[O] -> O.f,那么我们将传进去的参数O
替换为B
即可。但是在实战的情况下这种情况是极少的。
回到实战情况,比如我们的入口类A
存在O.abc
这个方法,也就是 A[O] -> O.abc;而 O 呢,如果是一个动态代理类,O
的invoke
方法里存在.f
的方法,便可以漏洞利用了,我们还是展示一下。
1 | A[O] -> O.abc |
动态代理在反序列化当中的利用和readObject
是异曲同工的。
readObject
方法在反序列化当中会被自动执行。
而invoke
方法在动态代理当中会自动执行。
参考:
【Spring基础】JDK动态代理实现原理(jdk8)_jdk8 动态方法调用-CSDN博客
彻底搞懂jdk动态代理并自己动手写一个动态代理-腾讯云开发者社区-腾讯云 (tencent.com)
Java | JDK 动态代理的原理其实很简单 - 掘金 (juejin.cn)