JDK动态代理详解

news2024/9/22 3:34:01

1.什么是动态代理

代理

可能很多小伙伴首次接触动态代理这个名词的时候,或者是在面试过程中被问到动态代理的时候,不能很好的描述出来,动态代理到底是个什么高大上的技术。不方,其实动态代理的使用非常广泛,例如我们平常使用的Spring中的@Transactional注解,其依赖于AOP,而AOP的底层实现便是动态代理,看到这里,是不是更有兴趣去了解动态代理了呢?

动态代理:可以分解为“动态”+“代理”。

  • 代理:“代理”一词,在我们的生活中也是随处可见的,例如房屋中介,其是对房主的一种代理,房主需要出租其房屋,但是可能没时间去接待租客,给租客介绍房屋信息,带领租客看房,但是房屋中介可以为租客提供这些服务,所以,代理其是对被代理对象的一个功能增强
  • 动态:“动态”通常与“静态”相比较,“静态”描述的是事物是固定存在的,“动态”则描述的是事物是随着需求而动态生成的。

所以,静态代理存在一定的局限性,不能很好的满足需求的千变万化,动态代理的出现,就是为了解决这些局限性。

我们先来看看静态代理。

2.静态代理

在开发中,通常需要为方法添加日志打印,能够记录程序的执行过程,以便后续出现异常问题的时候,能更好的排查定位。

假设我们现在已经完成了系统用户的增加、删除、修改等功能,这些功能在类UserServiceImpl中已经实现。

代码示例:

public class UserServiceImpl {
    public void add() {
        System.out.println("添加用户");
    }

    public void update() {
        System.out.println("修改用户");
    }

    public void delete() {
        System.out.println("删除用户");
    }
}

现在,我们需要在UserServiceImpl类中的方法添加日志功能,那么怎么才能更好地去实现这个需求呢?

1)直接在目标方法前后添加日志代码

代码示例:

public class UserServiceImpl {
    public void add() {
        System.out.println("====== add方法开始 ======");
        System.out.println("添加用户");
        System.out.println("====== add方法结束 ======");
    }

    public void update() {
        System.out.println("====== update方法开始 ======");
        System.out.println("修改用户");
        System.out.println("====== update方法结束 ======");
    }

    public void delete() {
        System.out.println("====== delete方法开始 ======");
        System.out.println("删除用户");
        System.out.println("====== delete方法结束 ======");
    }
}

观察上述代码,这种方式的缺点在于:

  • 如果UserServiceImpl类中,有很多的方法,修改量大,且存在大量重复代码,不利于后期维护。
  • 直接修改源代码,不符号开闭原则。应该对扩展开放,对修改关闭。

2)静态代理方式实现

静态代理需要我们将目标类的方法抽取到接口中,代理类和目标类实现同一个接口,既然要实现代理,代理类自然需要在其内部维护目标对象的引用,并通过构造函数为其赋值,然后在代理类的方法中调用目标对象的同名方法,并在调用前后完成功能的增强。

实现步骤:

  • 抽取UserService接口
  • 创建目标类UserServiceImpl实现UserService接口
  • 创建代理类UserServiceProxy实现UserService接口
  • 代理类中完成功能的增强

代码实现:

// 目标接口
public interface UserService {
    void add();

    void update();

    void delete();
}
// 目标类
public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        System.out.println("添加用户");
    }

    @Override
    public void update() {
        System.out.println("修改用户");
    }

    @Override
    public void delete() {
        System.out.println("删除用户");
    }
}
// 代理类
public class UserServiceProxy implements UserService {

    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void add() {
        System.out.println("====== add方法开始 ======");
        userService.add();
        System.out.println("====== add方法结束 ======");
    }

    @Override
    public void update() {
        System.out.println("====== update方法开始 ======");
        userService.update();
        System.out.println("====== update方法结束 ======");
    }

    @Override
    public void delete() {
        System.out.println("====== delete方法开始 ======");
        userService.delete();
        System.out.println("====== delete方法结束 ======");
    }
}

观察上述代码,静态代理遵循开闭原则,在不修改目标类的前提下,完成了功能的增强,但是依然存在大量重复的代码,且一个代理类只能代理一个目标类,如果有n个目标类需要被代理,就需要同比增加n个代理类。

那么,有没有办法可以使得我们不需要去定义这么多的代理类,就可以实现对目标类功能的增强?答案是有的:动态代理

2.JDK动态代理

前面,我们提到静态代理的实现方式:代理类和目标类都实现同一个接口,在代理类中维护目标类对象,并完成对目标类对象方法的增强,这种方式虽然遵循开闭原则,但是代理类和目标类至少是“一对一”的绑定关系,如果需要被代理的目标类个数越多,代理类就会越多,会产生大量重复的代码,也不利于后期的维护。

从静态代理中,我们知道代理类也是接口的一个实现类,代理对象的类型也是属于接口类型,我们来验证一下。

public class Test {
    public static void main(String[] args) {
        UserServiceProxy userServiceProxy = new UserServiceProxy(new UserServiceImpl());
        System.out.println(userServiceProxy instanceof UserService);
    }
}
// 打印结果:true

那么,能不能动态生成这些代理对象呢?我们知道类是构造对象的模板,代理类都还不存在,怎么去构造代理对象呢?

除了不存在代理类,还剩下UserService接口和UserServiceImpl目标类,JDK动态代理的目的就是通过接口来生成代理类以及代理类的对象,我们知道接口是不能直接通过new关键字创建对象的。

那么JDK动态代理是怎么创建出代理类以及代理类对象的呢?

我们先来看看通过new关键字创建对象的过程。

UserServiceImpl userService = new UserServiceImpl();
/*
创建对象的过程:
	1.执行new指令,如果类未加载,先执行类加载过程。
		1.加载:JVM通过ClassLoader将UserServiceImpl.class文件加载到方法区(Method Area),在堆内存中创建代表该类的Class对象。
		2.验证
		3.准备:为静态变量分配内存并设置类型初始值。
		4.解析
		5.初始化:为静态变量赋值、执行静态代码块
	2.为对象分配内存,将对象的实例字段初始化类型零值。
	3.执行构造方法,对对象进行初始化
*/

对象创建过程

追踪上述过程,我们得知创建对象,需要先得到该类的Class对象,通过Class对象去创建实例对象。为了验证这一点,我们不妨来看看通过反射的方式创建对象的过程。

public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        // 获取Class对象
        Class<UserServiceImpl> userServiceClass = UserServiceImpl.class;
        // 获取构造器
        Constructor<?>[] constructors = userServiceClass.getConstructors();
        for (Constructor<?> constructor : constructors) {
            // 通过构造器创建实例对象
            System.out.println(constructor.newInstance());
        }
    }
}

现在,问题回归到接口不能直接new,也没有构造方法,并且不存在代理类的class文件,怎么获得Class对象了。

动态代理关键类

我们先来看看JDK动态代理的实战代码:

  • 需要自定义个CustomInvocationHandler实现InvocationHandler接口。
  • 利用Proxy.newProxyInstance构建实例对象。
// UserService接口
public interface UserService {
    void add();

    void update();

    void delete();
}

// 目标类
public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        System.out.println("添加用户");
    }

    @Override
    public void update() {
        System.out.println("修改用户");
    }

    @Override
    public void delete() {
        System.out.println("删除用户");
    }
}

// CustomInvocationHandler
public class CustomInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    public CustomInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("====== 方法开始 ======");
        Object result = method.invoke(target, args);
        System.out.println("====== 方法结束 ======");
        return result;
    }
}

public class Test {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        // 关键代码
        UserService service = (UserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces(), new CustomInvocationHandler(userService));
        service.add();
    }
}

从测试代码可以看出,Proxy类是关键。我们来看看Proxy为我们提供的方法:

Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)虽然被标注为过时方法,但是从名字上可以得知,其目的是为了获得代理类的Class对象。话不多说,我们来调用一下。

public class Test {
    public static void main(String[] args) {
        Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());
        System.out.println(proxyClass.getName());
        for (Method method : proxyClass.getDeclaredMethods()) {
            System.out.println(method.getDeclaringClass() + "." + method.getName() + "()");
        }
        System.out.println(Arrays.toString(proxyClass.getConstructors()));
    }
}

可以看到:

  • 获得的Class对象的名称为$Proxy0
  • 定义了我们需要的add()、update()、delete()方法。
  • 定义了一个有参构造方法$Proxy0(InvocationHandler handler)

虽然没有无参构造方法,我们还是得尝试一下调用一下这个有参的构造方法,需要我们传入一个java.lang.reflect.InvocationHandler对象

public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());
        // 获取$Proxy0(InvocationHandler handler)构造方法
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
        UserService userService = (UserService) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(proxy.getClass());
                System.out.println(method.getDeclaringClass() + "." + method.getName() + "()");
                return null;
            }
        });
        userService.add();
    }
}

看的出来,**当我们获得代理对象之后,通过代理对象来调用接口方法,都会回调构造时传进来的InvocationHandler对象的invoke(Object proxy, Method method, Object[] args)方法,**该方法有3个参数:

  • Object proxy:代表的是代理对象本身。
  • Method method:代表的是被调用的方法的Method对象。
  • Object[] args:代表的是被调用方法的参数。

可以猜测,JDK动态代理生成的代理类中,维护了InvocationHandler类的对象变量,并且在实现接口方法时,通过InvocationHandler对象调用了invoke(Object proxy, Method method, Object[] args)方法。

System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

不知道大家没有看到这行代码哈,当添加了这行代码之后,可以将在项目目录下保存动态创建的class文件,com/sun/proxy/$Proxy0.class

可以看到生成的代理类$Proxy0继承自Proxy类,并实现了UserService接口,并且在add()方法中通过其父类Proxy中维护的InvocationHandler对象调用invoke()方法,这也就成功的解释了前面调用userService.add()方法,会回调到invoke()方法。

这时候我们再把代码改造一下,如下:

public class CustomInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    public CustomInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("====== 方法开始 ======");
        Object result = method.invoke(target, args);
        System.out.println("====== 方法结束 ======");
        return result;
    }
}

public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        UserServiceImpl target = new UserServiceImpl();
        Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
        UserService userService = (UserService) constructor.newInstance(new CustomInvocationHandler(target));
        userService.add();
    }
}

这样就完成了对目标对象功能的增强,前面我们提到过Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)

已经被标注为过时,推荐我们使用Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法。

动态代理设计思想

好的,到这里,我们来总结一下JDK动态的设计思想:

使用JDK动态代理,使得我们免去编写代理类,只需要将增强功能编写在InvocationHandlerinvoke方法中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383652.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

考研复试——计算机网络

文章目录计算机网络1. 说下计算机网络体系结构2. 说一下每层协议有哪些&#xff1f;3. 数据在各层之间是如何传输的呢&#xff1f;4. [从浏览器地址栏输入 url 到显示主页的过程&#xff1f;](https://blog.csdn.net/weixin_46351593/article/details/115386029)5. 说说 HTTP 常…

Guitar Pro8中文版打谱编曲软件

许多打谱编曲软件中都有吉他乐器的插件&#xff0c;插入音轨即可使用&#xff0c;除此以外&#xff0c;还有一款专门针对吉他的音乐软件&#xff0c;就是Guitar Pro。Guitar Pro是吉他类音乐软件中比较有代表性的&#xff0c;从开发至今不断更新优化&#xff0c;目前的软件版本…

如何远程访问公司的电脑?

是否可以从其他地方远程访问公司的电脑&#xff1f;答案是肯定的。一个可靠的远程桌面工具可以让您从另一台设备远程访问您的工作电脑&#xff0c;无论位置如何&#xff0c;无论您是在咖啡厅、酒店还是家中&#xff0c;您都可以从另一台电脑或移动设备远程访问您工作的电脑以处…

【算法基础】深度优先搜索(DFS) 广度优先搜索(BFS)

一、DFS & BFS 1. 深度优先搜索DFS 深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。 2. 广度优先搜索BFS 广度优先搜索较之深度优先搜索之不同在于,深度…

TCP内部的十大核心机制

文章目录1、确认应答机制2、超时重传机制3、连接管理机制1、三次握手2、四次挥手4、滑动窗口机制5、流量控制机制6、拥塞控制7、延时应答机制8、捎带应答机制9、面向字节流10、特殊情况1、确认应答机制 TCP是可靠传输&#xff0c;那么TCP协议能够实现可靠传输的核心机制就是确…

Anker推出Security SmartTrack卡,详谈苹果Find My技术

Anker 旗下品牌 Eufy 近日在欧洲、美国市场推出了 Security SmartTrack 卡。其工作原理和苹果 AirTag 类似&#xff0c;允许用户通过手机定位追踪到绑定的物品。 Security SmartTrack卡通过蓝牙连接&#xff0c;范围为 260 英尺&#xff08;约 80 米&#xff09;。采用防水设…

跨境数据传输是日常业务中经常且至关重要的组成部分

跨境数据传输是日常业务中经常且至关重要的组成部分。在过去的20年中&#xff0c;由于全球通信网络和业务流程的发展&#xff0c;全球数据流的模式已迅速发展。随着数据从数据中心移到数据中心和/或跨边界移动&#xff0c;安全漏洞已成为切实的风险。有可能违反国家和国际数据传…

INOBITEC PRO DICOM VIEWER 2.9.0 Crack

inobitec dicom高级 3D 重建&#xff0c;以 OBJ、STL、PLY 格式导出表面&#xff0c;先进的多计划重建&#xff0c;添加标记和标记线&#xff0c;将系列与高级工具相结合&#xff0c;具有多种选择的虚拟内窥镜检查&#xff0c;从视口录制视频&#xff08;仅限 64 位版本&#x…

CSS的6个新特性

1、容器查询&#xff08;Container Queries&#xff09; 容器查询container类似于媒体查询media&#xff0c;区别在于查询所依据的对象不同。媒体查询依据的是浏览器的视窗大小&#xff0c;容器查询依据的是元素的父元素或者祖先元素的大小。 有关容器查询的属性一共有三个&a…

vue2的动画和过渡效果

文章目录过渡 & 动画Transition 组件基于 CSS 的过渡效果CSS 过渡类名 class为过渡效果命名CSS 过渡 transition实例1&#xff1a;实例2&#xff1a;CSS 动画自定义过渡的类名同时使用 transition 和 animation深层级过渡与显式过渡时长性能考量JavaScript 动画可复用过渡效…

STM32 HAL库硬I2C的TOF050C模块

前言最近在倒腾毕业设计&#xff0c;需要用到TOF050C&#xff0c;但是现有的案例都是软IIC&#xff0c;并且还是基于STM32F103的&#xff0c;笔者用的STM32F767&#xff0c;没有GPIO->CRH寄存器。问题来了&#xff0c;如果我每次都要去看寄存器手册属实费时间&#xff0c;这…

案例08-让软件的使用者成为软件的设计者

一&#xff1a;背景介绍 对于需求的开发每天可能都会有上线的情况&#xff0c;为了防止每次上线拉取代码或者修改配置而引发的冲突以及发生了冲突应该找谁一起确定一下代码留下那一部分的情况。所以在开发的群中会有一个表格来记录每个需求上线修改的环境、是否修改数据库、是否…

【论文阅读】Research on video adversarial attack with long living cycle

论文链接&#xff1a;添加链接描述 Method OPTIMIZATION PROBLEM DESCRIPTION XXX是浮点数域中的对抗视频示例&#xff0c;XcX_cXc​表示encoded的视频对抗示例。设EXˆ−XE Xˆ−XEXˆ−X表示在对抗中增加的扰动&#xff0c;EcXc−XE_c Xc − XEc​Xc−X表示视频压缩编码损…

【线性筛+DP】最大和

看错题了&#xff0c;呃呃&#xff0c;其实就是个简单DP最大和 - 蓝桥云课 (lanqiao.cn)题意&#xff1a;思路&#xff1a;设dp[i]为以1为终点的最大和&#xff0c;然后枚举状态和决策就行了主要是线性筛的应用&#xff0c;它可以预处理出一个数的最小质因子是多少Code&#xf…

如何用BurpSuite抓取手机数据包

文章目录前言准备工具Burp Suite物理机或虚拟机(移动设备)手机抓包网络环境开启burp并设置代理手机配置代理安装Burp证书开始抓包踩坑后记前言 最近挖了一波src&#xff0c;挖来挖去发现有很多公众号或者app没有测试&#xff0c;这就需要Burp能够抓取手机的数据包了&#xff0…

九州云出席全球人工智能开发者先锋大会,圆桌论道开源未来

2月25日-26日&#xff0c;2023年全球人工智能开发者先锋大会&#xff08;GAIDC&#xff09;在临港成功召开。本届盛会以“向光而行的开发者”为主题&#xff0c;汇集政府职能部门领导、国内外知名专家学者、具有国际影响力的开源创业者&#xff0c;聚焦前瞻探索、开源开放、人才…

Linux gcc/g++编译链接头文件和库(动态库.so 和 静态库.a)

最近在学习log4cpp库时&#xff0c;使用g去编译&#xff0c;却发现自己不会链接...&#xff0c;这哪能行&#xff0c;于是网上钻研&#xff0c;终于解决&#xff0c;现在记录下来分享给遇到同样问题的人。 gcc和g类似&#xff0c;这里就以g为例&#xff01; 刚好用到的log4cpp…

测开:vue入门(1)

目录 一、背景 二、介绍 三、创建项目 3.1 创建vue项目 方式二&#xff1a;直接在html页面中&#xff0c;引入vue 3.2 直接在html页面中&#xff0c;引入vue 3.2.1 引入在线的vue&#xff08;方式一&#xff09; 3.2.2 将vue 下载到本地&#xff08;方式二&#xff09; …

代码随想录算法训练营第二天| 977. 有序数组的平方、209. 长度最小子数组、59.螺旋矩阵II

977 有序数组的平方题目链接&#xff1a;977 有序数组的平方介绍给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。思路看到题目的第一反应&#xff0c;首先负数的平方跟正数的平方是相同的&…

Git系列:常见指令辨析

Git系列&#xff1a;常见指令辨析指令辨析工作区、暂存区、版本库傻傻分不清楚&#xff1f;主干和分支的关系是什么&#xff1f;git fetch/merge/pull辨析日志查看时&#xff0c;git log与git reflog的区别是&#xff1f;git diff和status的区别是&#xff1f;相关资料本文小结…