都2024年了,还有人不懂动态代理么?

2024-06-23 14:44
文章标签 动态 代理 2024

本文主要是介绍都2024年了,还有人不懂动态代理么?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 一、定义
  • 二、静态代理
  • 三、动态代理
    • 1. JDK代理
      • 1.1 JDK代理实现流程
      • 1.2 动态生成的类字节码
    • 2. Cglib代理
      • 2.1 Cglib实现流程
  • 四、总结

一、定义

静态代理和动态代理都反映了一个代理模式,代理模式是一种经典的设计模式,常用于为其他对象提供一种代理或占位符,以限制对它的访问,充当访问对象的中介
就好比平时我们租房,会和中介进行沟通,中介带我们去看房,中介又与房东保持联系,但房东不会带我们看房。


二、静态代理

静态代理是一种代理模式的实现,静态代理在编译时就确定了代理类和目标类之间的关系。
在静态代理中,代理类和目标类通常实现相同的接口,或者代理类继承目标类。
代理类作为目标类的包装,持有目标类的引用,并且调用目标对象的方法前后进行增强操作。
下面举个例子

public interface Landlord {/*** 出租房间*/void rentingHouse();}
public class ShenZhenLandlord implements  Landlord {@Overridepublic void rentingHouse() {System.out.println("深圳房东出租房间");}
}
public class Intermediary implements Landlord{/*** 代理类*/private final Landlord landlord;public Intermediary(Landlord landlord) {this.landlord = landlord;}@Overridepublic void rentingHouse() {System.out.println("中介带租客看房");landlord.rentingHouse();System.out.println("中介收取出租非");}
}
public static void main(String[] args) {// 目标类Landlord landlord = new ShenZhenLandlord();//代理类Intermediary intermediary = new Intermediary(landlord);// 运行代理方法intermediary.rentingHouse();
}

image-20240623131158278

这是一个经典的代理模式的实现,可以在不修改原对象的情况下对目标类进行增强处理,但是若接口新增方法,所有代理类都都需要新增对应的实现,不好维护。


三、动态代理

动态代理是一个在运行期间动态创建代理对象的技术,它允许开发者为一个或多个接口创建一个代理对象,且无需事先知道具体实现类。

静态代理和动态代理的区别在于class文件是编译期间确定还是运行期间确定
静态代理在编写代码的时候就明确了代理类和目标类之间的关系,在编译阶段会生成一个Class对象。
动态代理则是在运行阶段动态生成代理对象,在运行的时候动态生成字节码,并加载到JVM中去


1. JDK代理

JDK代理技术是Java提供的一种动态代理技术,使用方式如下

public class ProxyFactory {/*** 目标类*/private Object target;public ProxyFactory(Object target) {this.target = target;}public Object getProxyInstance() {return Proxy.newProxyInstance(// 目标类加载器target.getClass().getClassLoader(),// 目标对象的接口类型target.getClass().getInterfaces(),// 事件处理new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前置增强");method.invoke(target, args);System.out.println("后置增强");return null;}});}
}
public static void main(String[] args) {ShenZhenLandlord landlord = new ShenZhenLandlord();System.out.println(landlord.getClass());Landlord proxyInstance = (Landlord) new ProxyFactory(landlord).getProxyInstance();proxyInstance.rentingHouse();System.out.println(proxyInstance.getClass());
}

image-20240623131414977

代理类打印出的Class为com.sun.proxy.$Proxy0, 从ShenZhenLandlord到 com.sun.proxy.$Proxy0 ,其中经历了什么呢?


1.1 JDK代理实现流程

需要知道的是JVM虚拟机的类加载过程分为加载、验证、准备、解析、初始化这五个阶段, 在加载阶段需要进行下面这几个步骤:

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态结构转化为方法区的运行时数据结构
  3. 内存生成一个代表这类的Class对象,作为方法区这个类的各种数据访问入口

而获取类的二进制字节流,JVM提供了三种途径:

  1. 本地获取字节码,比如前面的ShenZhenLandlord类,编译后就属于本地的字节码
  2. 从网络中获取,可以使用URLClassLoader来加载类
  3. 运行时计算生成,在程序运行的过程中动态生成类字节码

而代理就是运行时动态生成字节码,然后交给JVM进行类加载过程使用。
所以经过JDK代理后,从ShenZhenLandlord变成了com.sun.proxy.$Proxy0,也就是程序在运行中通过计算,生成了$Proxy0代理类的类字节码。


1.2 动态生成的类字节码

由于动态生成的类字节码是动态计算出来并加载到JVM内存中去,因此无法通过查看编译后的文件去查看这个代理类的 Class文件。
不过,可以通过arthas工具来查看动态生成的类字节码。
下载官网:https://arthas.aliyun.com/doc/download.html#%E7%94%A8-arthas-boot-%E5%90%AF%E5%8A%A8
输入命令java -jar arthas-boot.jar 运行arthas

image-20240623131648385

proxy.jdk.Main 就是我们要查看的目标类,按3进入监控界面,如下图所示就代表进入成功了

img

输入命令jad com.sun.proxy.$Proxy0命令,就能解析出$Proxy0的源码了

img

整理一下代码,如下:

public final class $Proxy0
extends Proxy
implements Landlord {private static Method m1;private static Method m3;private static Method m2;private static Method m0;// 构造参数为InvocationHandlerpublic $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {try {m3 = Class.forName("proxy.static_proxy.Landlord").getMethod("rentingHouse", new Class[0]);return;}catch (NoSuchMethodException noSuchMethodException) {throw new NoSuchMethodError(noSuchMethodException.getMessage());}catch (ClassNotFoundException classNotFoundException) {throw new NoClassDefFoundError(classNotFoundException.getMessage());}}// Landlord接口的`rentingHouse`方法public final void rentingHouse() {try {this.h.invoke(this, m3, null);return;}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}
}

看一个类,首先先看构造方法,KaTeX parse error: Expected 'EOF', got '#' at position 70: …就是`ProxyFactory#̲getProxyInstanc…Proxy0`的构造方法传进去的,也就是是同一个!!

    public Object getProxyInstance() {return Proxy.newProxyInstance(// 目标类加载器target.getClass().getClassLoader(),// 目标对象的接口类型target.getClass().getInterfaces(),// 事件处理new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前置增强");method.invoke(target, args);System.out.println("后置增强");return null;}});}

我们稍微看一下Proxy的源码

private static final Class<?>[] constructorParams ={ InvocationHandler.class };public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
throws IllegalArgumentException
{final Class<?>[] intfs = interfaces.clone();/** 获取动态生成的字节码*/Class<?> cl = getProxyClass0(loader, intfs);/** 获取这个代理类的构造参数为InvocationHandler的构造方法*/final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);return null;}});}// 将h作为构造参数创建代理类return cons.newInstance(new Object[]{h});
}

那么在$Proxy0#rentingHouse方法调用了构造参数传进来的InvocationHandlerinvoke方法,也就是调用了下面这个方法(增强后的方法)

new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前置增强");method.invoke(target, args);System.out.println("后置增强");return null;}
}

通过这,就可以得出下面的结论:

  1. 代理类实际上就是实现了Landlord接口,重写了rentingHouse方法
  2. 当外界调用代理类的rentingHouse方法的时候,实际上就是调用了new InvocationHandler的invoke方法

2. Cglib代理

Cglib(Code Generation Library)是一个强大的高性能代码生成库,用于在运行时扩展Java类和实现接口。它通过字节码技术动态生成新的类,从而实现对目标类的代理。Cglib代理是AOP(面向切面编程)中常用的一种代理方式,尤其是在需要代理那些没有实现接口的类时。
举个例子:
目标类如下

public class UserServiceImpl {public String selectList(){System.out.println("正在查询");return "小明, 小红, 小黄";}
}
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();}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("开始查询");// 执行原始方法Object result = methodProxy.invokeSuper(o, objects);System.out.println("查询完毕");return result;}
}public class MethodProxy{public Object invokeSuper(Object obj, Object[] args) throws Throwable {try {init();FastClassInfo fci = fastClassInfo;return fci.f2.invoke(fci.i2, obj, args);} catch (InvocationTargetException e) {throw e.getTargetException();}}
}
public class Main {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());String userList = proxy.selectList();System.out.println(userList);while (true) {}}
}

image.png


2.1 Cglib实现流程

使用arthas工具,将proxy.cglib.UserServiceImpl$$EnhancerByCGLIB$$b96d19a3类的代码获取出来。

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

在JVM编译阶段,Enhancer会根据目标类信息去动态生成代理类并设置回调。
当用户通过Cglib动态代理执行selectList方法的时候,会直接调用methodInterceptor.intercept方法,在intercept方法中通过invikeSuper调用父类的selectList方法。
如果没有设置回调,则直接调用父类的selectList方法。


四、总结

JDK代理与Cglib代理对比

  1. Cglib实现的动态代理是基于ASM字节码生成框架实现的,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高一点。但是需要注意,Cglib代理技术不可对final类进行类或方法的代理,因为Cglib的原理是动态生成代理类的子类实现的。
  2. 在JDK1.6以后的版本已经开始对JDK代理进行优化(具体优化了什么 以后有机会再写篇文章讲),在调用次数较少的情况,JDK代理的效率要比Cglib代理效率要高,只有进行大量调用的时候,Cglib的优势才会出现。而到了JDK1.8,JDK代理的效率已经高于Cglib代理,因此在有接口的情况下推荐优先使用JDK代理,没接口优先推荐使用Cglib代理技术。

动态代理与静态代理的比较

  1. 动态代理的最大优势就是在于可以把接口中声明的所有方法转移到调用处理器的一个集中的方法进行处理(Invocation.invoke),在接口数量比较多的情况下,可以进行灵活处理。
  2. 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

这篇关于都2024年了,还有人不懂动态代理么?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1087454

相关文章

springboot如何通过http动态操作xxl-job任务

《springboot如何通过http动态操作xxl-job任务》:本文主要介绍springboot如何通过http动态操作xxl-job任务的问题,具有很好的参考价值,希望对大家有所帮助,如有错... 目录springboot通过http动态操作xxl-job任务一、maven依赖二、配置文件三、xxl-

Java调用C#动态库的三种方法详解

《Java调用C#动态库的三种方法详解》在这个多语言编程的时代,Java和C#就像两位才华横溢的舞者,各自在不同的舞台上展现着独特的魅力,然而,当它们携手合作时,又会碰撞出怎样绚丽的火花呢?今天,我们... 目录方法1:C++/CLI搭建桥梁——Java ↔ C# 的“翻译官”步骤1:创建C#类库(.NET

MyBatis编写嵌套子查询的动态SQL实践详解

《MyBatis编写嵌套子查询的动态SQL实践详解》在Java生态中,MyBatis作为一款优秀的ORM框架,广泛应用于数据库操作,本文将深入探讨如何在MyBatis中编写嵌套子查询的动态SQL,并结... 目录一、Myhttp://www.chinasem.cnBATis动态SQL的核心优势1. 灵活性与可

Mybatis嵌套子查询动态SQL编写实践

《Mybatis嵌套子查询动态SQL编写实践》:本文主要介绍Mybatis嵌套子查询动态SQL编写方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、实体类1、主类2、子类二、Mapper三、XML四、详解总结前言MyBATis的xml文件编写动态SQL

SpringBoot实现Kafka动态反序列化的完整代码

《SpringBoot实现Kafka动态反序列化的完整代码》在分布式系统中,Kafka作为高吞吐量的消息队列,常常需要处理来自不同主题(Topic)的异构数据,不同的业务场景可能要求对同一消费者组内的... 目录引言一、问题背景1.1 动态反序列化的需求1.2 常见问题二、动态反序列化的核心方案2.1 ht

golang实现动态路由的项目实践

《golang实现动态路由的项目实践》本文主要介绍了golang实现动态路由项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习... 目录一、动态路由1.结构体(数据库的定义)2.预加载preload3.添加关联的方法一、动态路由1

Python Selenium动态渲染页面和抓取的使用指南

《PythonSelenium动态渲染页面和抓取的使用指南》在Web数据采集领域,动态渲染页面已成为现代网站的主流形式,本文将从技术原理,环境配置,核心功能系统讲解Selenium在Python动态... 目录一、Selenium技术架构解析二、环境搭建与基础配置1. 组件安装2. 驱动配置3. 基础操作模

慢sql提前分析预警和动态sql替换-Mybatis-SQL

《慢sql提前分析预警和动态sql替换-Mybatis-SQL》为防止慢SQL问题而开发的MyBatis组件,该组件能够在开发、测试阶段自动分析SQL语句,并在出现慢SQL问题时通过Ducc配置实现动... 目录背景解决思路开源方案调研设计方案详细设计使用方法1、引入依赖jar包2、配置组件XML3、核心配

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

SpringBoot基于配置实现短信服务策略的动态切换

《SpringBoot基于配置实现短信服务策略的动态切换》这篇文章主要为大家详细介绍了SpringBoot在接入多个短信服务商(如阿里云、腾讯云、华为云)后,如何根据配置或环境切换使用不同的服务商,需... 目录目标功能示例配置(application.yml)配置类绑定短信发送策略接口示例:阿里云 & 腾