博客 Spring从成神到升仙系列 一】2023年再不会动态代理,就要被淘汰了

Spring从成神到升仙系列 一】2023年再不会动态代理,就要被淘汰了

   数栈君   发表于 2023-06-20 15:42  194  0

代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

举个生活中常见的例子:客户想买房,房东有很多房,提供卖房服务,但房东不会带客户看房,于是客户通过中介买房。
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/5a6b49e49ad9735673496e0cf5009434..png

这时候对于房东来说,不直接和客户沟通,而是交于中介进行代理

对于中介来说,她也会在原有的基础上收取一定的中介费

三、静态代理
我们创建 Landlord 接口如下:

public interface Landlord {
// 出租房子
void apartmentToRent();
}
创建其实现类 HangZhouLandlord 代表杭州房东出租房子

public class HangZhouLandlord implements Landlord {
@Override
public void apartmentToRent() {
System.out.println("杭州房东出租房子");
}
}
创建代理类 LandlordProxy,代表中介服务

public class LandlordProxy {

public Landlord landlord;

public LandlordProxy(Landlord landlord) {
this.landlord = landlord;
}

public void apartmentToRent() {
apartmentToRentBefore();
landlord.apartmentToRent();
apartmentToRentAfter();
}

public void apartmentToRentBefore() {
System.out.println("出租房前,收取中介费");
}

public void apartmentToRentAfter() {
System.out.println("出租房后,签订合同");
}
}

创建最终测试:

public class JavaMain {
public static void main(String[] args) {
Landlord landlord = new HangZhouLandlord();

LandlordProxy proxy = new LandlordProxy(landlord);
// 从中介进行租房
proxy.apartmentToRent();
}
}

得出最终结果:

出租房前,收取中介费
杭州房东出租房子
出租房后,签订合同

通过上述 demo 我们大概了解代理模式是怎么一回事

优点:
在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展
缺点:
代理对象需要与目标对象实现一样的接口,所以会有很多代理类,一旦接口增加方法,目标对象与代理对象都要维护
四、动态代理
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能,动态代理又被称为JDK代理或接口代理。

静态代理与动态代理的区别:

静态代理在编译时就已经实现了,编译完成后代理类是一个实际的 class 文件
动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时动态生成类字节码,并加载到 JVM 中
1、JDK代理
代码如下:

public class ProxyFactory {
// 目标方法
public Object target;
public ProxyFactory(Object target) {
this.target = target;
}

public Object getProxyInstance() {
return Proxy.newProxyInstance(
// 目标对象的类加载器
target.getClass().getClassLoader(),
// 目标对象的接口类型
target.getClass().getInterfaces(),
// 事件处理器
new InvocationHandler() {
/**
*
* @param proxy 代理对象
* @param method 代理对象调用的方法
* @param args 代理对象调用方法时实际的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是前置增强");
method.invoke(target, args);
System.out.println("我是后置增强");
return null;
}
}
);
}
}

我们测试一下:

public class JavaMain {
public static void main(String[] args) {
Landlord landlord = new HangZhouLandlord();

System.out.println(landlord.getClass());

Landlord proxy = (Landlord) new ProxyFactory(landlord).getProxyInstance();

proxy.apartmentToRent();

System.out.println(proxy.getClass());

while (true){}
}
}

得出结果:

class com.company.proxy.HangZhouLandlord
我是前置增强
杭州房东出租房子
我是后置增强
class com.sun.proxy.$Proxy0

这里可能有小伙伴已经懵了,接着往后看

1.1 JDK类的动态生成
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/1561ce231e9d438cd630c37cd8c32384..png


从本地获取

从网络中获取
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/a1529a0106166511474a003a65dc6dfa..png

运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到 JVM 中使用

1.2 JDK动态代理流程
所以,我们可以得出一个结论:我们上面的 $Proxy0 实际上是 JVM 在编译时期加载出来的类,由于这个类是编译时期加载的,所以我们没办法在 IDEA 里面看到。

可能一般的文章,到这里基本就结束了,让大家知道 $Proxy0是由 JVM 编译时期加载出来的类

但大家都知道,小黄的文章主打的就是一个硬核、源码级。所以,我们直接去看 $Proxy0 的源代码

首先,我们需要下载一个 arthas 的产品,网址:https://arthas.aliyun.com/doc/,跟随流程解压即可。

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

当我们一切准备完成后,启动我们上面动态代理的测试 JavaMain 类

启动完成后,进入我们的 arthas 页面,执行命令:java -jar arthas-boot.jar
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/b7d6af3713b61ca4d42cb812496ea6b7..png



我们可以看到,我们的目标类 com.company.proxy.JavaMain 就出现了,随后我们按下 4,进入到我们的监控页面。
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/5fc64b353ef3bd98c5b52d94a9fce057..png



随后使用 jad com.sun.proxy.$Proxy0 之后,可以看到我们已经解析出来 $Proxy0 的源码了
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/5c447b1c5ab994a6d18e33afc102020f..png



我们将其复制到下面,并删减一些不必要的信息。

public final class $Proxy0 extends Proxy implements Landlord {
private static Method m3;

// $Proxy0 类的构造方法
// 参数为 invocationHandler
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}

static {
m3 = Class.forName("com.company.proxy.Landlord").getMethod("apartmentToRent", new Class[0]);
}

public final void apartmentToRent() {
this.h.invoke(this, m3, null);
return;
}
}

我们先看其有参构造方法,可以看到 $Proxy0 的构造方法入参为 InvocationHandler,有没有感觉似曾相识。

如果你这里忘掉了,不妨去看一下动态代理的 ProxyFactory 的代码,可以发现,我们 Proxy.newProxyInstance() 的第三个自定义的参数,也正是我们的 InvocationHandler。

我们猜测一下,如果这里的传的 InvocationHandler 是我们之前自定义的 InvocationHandler

那么,如果我调用 $Proxy0.apartmentToRent() 是不是就是执行下面的代码:

public final void apartmentToRent() {
this.h.invoke(this, m3, null);
return;
}

// 这里的h.invoke执行的是我们这里自定义的方法,然后进行的前后增强
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是前置增强");
method.invoke(target, args);
System.out.println("我是后置增强");
return null;
}
}
);

如果说我们这个猜测是正确的话,那么会得出这样的几个结论:

我们的代理类实际上是实现了 Landlord 的接口,然后重写了 Landlord 接口中的 apartmentToRent 方法
当外界调用代理类的 apartmentToRent() 方法时,实际上是调用的我们自定义的 new InvocationHandler() 类里面的 invoke 方法
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/2d48b7b65bdb231d05ae62296b8fcfbf..png


还有我们的最后一步,也就是证明 $Proxy0 的构造入参 InvocationHandler 就是我们自定义的 InvocationHandler,废话不多说,直接来看代理的源码。

return Proxy.newProxyInstance(ClassLoader,Interfaces,new InvocationHandler() {});
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h){
// cl = class com.sun.proxy.$Proxy0
Class<?> cl = getProxyClass0(loader, intfs);
// cons = public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler)
final Constructor<?> cons = cl.getConstructor(constructorParams);
// 根据构造参数实例化对象
return cons.newInstance(new Object[]{h});
}
1
2
3
4
5
6
7
8
9
我们通过源码可以看到,一共分为三步(下面为反射的内容,如不熟悉可提前学习下反射):

拿到 $Proxy0 的 Class
根据 Class 拿到其构造方法
根据构造方法传入参数进行实例化
这就确定了我们上述的猜想是正确的。

2、Cglib代理
cglib (Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。cglib 为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。



最底层是字节码
ASM 是操作字节码的工具
cglib 基于 ASM 字节码工具操作字节码(即动态生成代理,对方法进行增强)
SpringAOP 基于 cglib 进行封装,实现 cglib 方式的动态代理
使用 cglib 需要引入 cglib 的jar包,如果你已经有 spring-core 的jar包,则无需引入,因为 spring 中包含了cglib 。

cglib 的Maven坐标

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>

2.1 cglib动态代理实现
还是同样的配方,我们要创建一个需要代理的类(UserServiceImpl),但不需要实现任何的接口,因为我们的 cglib 是根据类来进行创建的。

UserServiceImpl

public class UserServiceImpl {
// 查询功能
List<String> findUserList() {
return Collections.singletonList("小A");
}
}

实现 cglib 的工厂类:UserLogProxy

public class UserLogProxy implements MethodInterceptor {
/**
* 生成 CGLIB 动态代理类方法
*
* @param target
* @return
*/
public Object getLogProxy(Object target) {
// 增强器类,用来创建动态代理类
Enhancer enhancer = new Enhancer();

// 设置代理类的父类字节码对象
enhancer.setSuperclass(target.getClass());

// 设置回调
enhancer.setCallback(this);

// 创建动态代理对象并返回
return enhancer.create();

}

/**
* @param o 代理对象
* @param method 目标对象中的方法的Method实例
* @param objects 实际参数
* @param methodProxy 代理类对象中的方法的Method实例
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置输出");
Object result = methodProxy.invokeSuper(o, objects);
return result;
}
}

测试程序:JavaMainTest

public class JavaMainTest {
public static void main(String[] args) {

// 目标对象
UserServiceImpl userService = new UserServiceImpl();
System.out.println(userService.getClass());

// 代理对象
UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
System.out.println(proxy.getClass());

List<String> list = proxy.findUserList();
System.out.println("用户信息:" + list);

while (true) {

}
}
}

结果:

class com.study.spring.proxy.UserServiceImpl
class com.study.spring.proxy.UserServiceImpl$$EnhancerByCGLIB$$cd9788d
前置输出
用户信息:[小A]
1
2
3
4
2.2 cglib代理流程
按照上述我们分析 $Proxy0 的方法,将 com.study.spring.proxy.UserServiceImpl$$EnhancerByCGLIB$$cd9788d 取出,得到如下:

public class UserServiceImpl$$EnhancerByCGLIB$$cd9788d extends UserServiceImpl implements Factory {
final List findUserList() {
// 是否设置了回调
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
if (methodInterceptor == null) {
UserServiceImpl$$EnhancerByCGLIB$$cd9788d.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_0;
}
// 设置回调,需要调用 intercept 方法
if (methodInterceptor != null) {
return (List) methodInterceptor.intercept(this, CGLIB$findUserList$0$Method, CGLIB$emptyArgs, CGLIB$findUserList$0$Proxy);
}
// 无回调,调用父类的 findUserList 即可
return super.findUserList();
}
final List CGLIB$findUserList$0() {
return super.findUserList();
}
}

博主先把整个流程图放到下面,然后结合流程图来进行讲解:
http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/2d2f69b8e2caecf9ba1864ff8d7fb58f..png



在 JVM 编译期间,我们的 Enhancer 会根据目标类的信息去动态的生成 动态代理类并设置 回调
当用户在通过上述的动态代理类执行 findUserList() 方法时,有两个执行选项
若设置了回调接口,则直接调用UserLogProxy 中的 intercept ,然后通过 FastClass 类调用动态代理类,执行CGLIB$findUserList$0 方法,调用父类的 findUserList() 方法
若没有设置回调接口,则直接调用父类的 findUserList() 方法
五、代理模式总结
1、三种代理模式实现方式的对比
jdk 代理和 CGLIB 代理

使用 CGLib 实现动态代理,CGLib 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类,在JDK1.6 之前比使用 Java 反射效率要高。唯一需要注意的是,CGLib 不能对声明为 final 的类或者方法进行代理,因为 CGLib 原理是动态生成被代理类的子类。

在 JDK1.6、JDK1.7、JDK1.8 逐步对 JDK 动态代理优化之后,在调用次数较少的情况下,JDK 代理效率高于 CGLib 代理效率,只有当进行大量调用的时候,JDK1.6 和 JDK1.7 比 CGLib 代理效率低一点,但是到 JDK1.8 的时候,JDK 代理效率高于 CGLib 代理。所以如果有接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理。

动态代理和静态代理

动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
2、代理模式优缺点
优点:

代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
代理对象可以扩展目标对象的功能;
代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:

增加了系统的复杂度;
3、代理模式使用场景
功能增强

当需要对一个对象的访问提供一些额外操作时,可以使用代理模式
远程(Remote)代理

实际上,RPC 框架也可以看作一种代理模式,GoF 的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用 RPC 服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC 服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节。
防火墙(Firewall)代理

当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
保护(Protect or Access)代理

控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

  • 免责申明:

    本文系转载,版权归原作者所有,如若侵权请联系我们进行删除!

  • 《数据治理行业实践白皮书》下载地址:https://fs80.cn/4w2atu


  • 《数栈V6.0产品白皮书》下载地址:https://fs80.cn/cw0iw1

  • 想了解或咨询更多有关袋鼠云大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=bbs

    同时,欢迎对大数据开源项目有兴趣的同学加入「袋鼠云开源框架钉钉技术群」,交流最新开源技术信息,群号码:30537511,项目地址:
    https://github.com/DTStack

0条评论
社区公告
  • 大数据领域最专业的产品&技术交流社区,专注于探讨与分享大数据领域有趣又火热的信息,专业又专注的数据人园地

最新活动更多
微信扫码获取数字化转型资料
钉钉扫码加入技术交流群