浅谈JDK动态代理(上)

news2024/10/3 0:34:53
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

到目前为止,在Java基础进阶这个章节,我们已经帮大家梳理了很多晦涩但极其重要的知识点,包括反射、注解和泛型。这些都是我们迈向中高级程序员的小碎步,我们已经离“成熟的码农”越来越近了,但还不够。今天,我们仍需一起再往前走一小步:JDK动态代理。个人认为Java基础有“四大神兽”,除了刚才说的反射、注解和泛型,JDK动态代理就是最后一道坎。

“动态代理”四个字一出来,估计很多初学者已经开始冒冷汗。它之所以给人感觉很难,有三点原因:

  • 代码形式很诡异,让人搞不清调用逻辑
  • 用到了反射,而很多初学者不了解反射(现在你应该感觉好些了)
  • 包含代理模式的思想,本身比较抽象

尽管动态代理看起来似乎有一定难度,但却必须拿下。因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理 + 责任链,环环相扣。所以说,搞编程的,拼到到最后还是看基本功,要么是语言基础、要么是计算机基础。

一个小需求:给原有方法添加日志打印

假设你刚进入一个项目组,项目中存在一个Calculator类,代表一个计算器,它可以进行加减乘除操作:

public class Calculator {

	// 加
	public int add(int a, int b) {
		int result = a + b;
		return result;
	}

	// 减
	public int subtract(int a, int b) {
		int result = a - b;
		return result;
	}

	// 乘法、除法...
}

现在老大给你提了一个需求:在每个方法执行前后打印日志。

你有什么好的方案?

方案一:直接修改

很多人最直观的想法是直接修改Calculator类:

public class Calculator {

	// 加
	public int add(int a, int b) {
		System.out.println("add方法开始...");
		int result = a + b;
		System.out.println("add方法结束...");
		return result;
	}

	// 减
	public int subtract(int a, int b) {
		System.out.println("subtract方法开始...");
		int result = a - b;
		System.out.println("subtract方法结束...");
		return result;
	}

	// 乘法、除法...
}

上面的方案是有问题的:

  • 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
  • 如果Calculator类内部有几十个、上百个方法,修改量太大
  • 存在重复代码(都是在核心代码前后打印日志)
  • 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了,于是你又要打开Calculator花十分钟删除日志打印的代码(或回滚分支)!

所以,此种方案PASS!

方案二:静态代理实现日志打印

“静态代理”四个字包含了两个概念:静态、代理。我们先来了解什么叫“代理”,至于何为“静态”,需要和“动态”对比着讲。

代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。

常用的代理方式可以粗分为:静态代理动态代理

静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。

是不是有点晕?是的,我最讨厌这种干巴巴的描述。简而言之,就是这样:

按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:

  • 将Calculator抽取为接口
  • 创建目标类CalculatorImpl实现Calculator
  • 创建代理类CalculatorProxy实现Calculator

抽取接口

/**
 * Calculator接口
 */
public interface Calculator {
	int add(int a, int b);
	int subtract(int a, int b);
}

原目标类实现接口

/**
 * 目标类,实现Calculator接口(如果一开始就面向接口编程,其实是不存在这一步的,CalculatorImpl原本就实现Calculator接口)
 */
public class CalculatorImpl implements Calculator {

	// 加
	public int add(int a, int b) {
		int result = a + b;
		return result;
	}

	// 减
	public int subtract(int a, int b) {
		int result = a - b;
		return result;
	}

	// 乘法、除法...
}

新增代理类并实现接口

/**
 * 静态代理类,实现Calculator接口
 */
public class CalculatorProxy implements Calculator {
    // 代理对象内部维护一个目标对象引用
	private Calculator target;
        
    // 通过构造方法,传入目标对象
	public CalculatorProxy(Calculator target) {
		this.target = target;
	}

    // 调用目标对象的add,并在前后打印日志
	@Override
	public int add(int a, int b) {
		System.out.println("add方法开始...");
		int result = target.add(a, b);
		System.out.println("add方法结束...");
		return result;
	}

    // 调用目标对象的subtract,并在前后打印日志
	@Override
	public int subtract(int a, int b) {
		System.out.println("subtract方法开始...");
		int result = target.subtract(a, b);
		System.out.println("subtract方法结束...");
		return result;
	}

	// 乘法、除法...
}

测试案例

使用代理对象完成加减乘除,并且打印日志:

public class Test {
	public static void main(String[] args) {
		// 把目标对象通过构造器塞入代理对象
		Calculator calculator = new CalculatorProxy(new CalculatorImpl());
		// 代理对象调用目标对象方法完成计算,并在前后打印日志
		calculator.add(1, 2);
		calculator.subtract(2, 1);
	}
}  

静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了上一种方案4大缺点中的第1、4两点:

  • 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭(✅,如果一开始就面向接口编程,这一步其实是不需要的)
  • 如果Calculator类内部有几十个、上百个方法,修改量太大(❎,目标类有多少个方法,代理类就要重写多少个方法)
  • 存在重复代码(都是在核心代码前后打印日志)(❎,代理类中的日志代码是重复的)
  • 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了(✅,别用代理类就好了)

静态代理的问题

上面的代码中,为了给目标类做日志增强,我们编写了代理类,而且准备了一个构造器接收目标对象。代理代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!

如果现在我们系统需要全面改造,要给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类...

自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!

你细品上面加粗的这句话,是不是好像一句废话?没有类哪来的对象?!

其实我的意思是,能否让JVM根据接口自动生成代理对象呢?

比如,有没有一个方法,我传入接口+增强的代码(比如打印日志),它就给我自动返回代理对象呢?这样就能省去编写代理类这个无用的“中介”了,没有中间商赚差价,岂不爽哉?

JDK,能做到吗?

预知后事如何,请听下回分解~

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

Java面向对象(高级)-- 类中属性赋值的位置及过程

文章目录 一、赋值顺序(1)赋值的位置及顺序(2)举例(3)字节码文件(4)进一步探索(5)最终赋值顺序(6)实际开发如何选 二、(超纲)关于字节…

梦开始的地方——Adobe Premiere Pro

今天,我们来说说一款老生常谈的相信也是很多人都经常迫切需要的软件。Adobe Premiere Pro,简称Pr,是由Adobe公司开发的一款视频编辑软件。 Premiere Pro是视频编辑爱好者和专业人士必不可少的视频编辑工具。它可以提升您的创作能力和创作自由…

Element中el-table组件右侧空白隐藏-滚动条

开发情况&#xff1a; 固定table高度时&#xff0c;出现滚动条&#xff0c;我们希望隐藏滚动条&#xff0c;或修改滚动条样式&#xff0c;出现table右边出现15px 的固定留白。 代码示例 <el-table class"controlTable" header-row-class-name"controlHead…

httpd(Web服务器)

名词解释 1、URL&#xff1a;Uniform Resource Locator&#xff0c;统⼀资源定位符 2、⽹址格式&#xff1a;<协议>://<主机或主机名>[:port]/<⽬录资源,路径> 3、主机地址/主机名&#xff1a;主机地址是服务器在因特⽹所在的IP地址。主机名就需要域名解析…

arduino入门一:点亮第一个led

void setup() { pinMode(12, OUTPUT);//12引脚设置为输出模式 } void loop() { digitalWrite(12, HIGH);//设置12引脚为高电平 delay(1000);//延迟1000毫秒&#xff08;1秒&#xff09; digitalWrite(12, LOW);//设置12引脚为低电平 delay(1000); }

【正则插件】前端正则插件以及预览插件推荐

1&#xff09; 2&#xff09; any-rule Pegex Previewer 下载好插件之后 在代码层右键选择 选择你需要的正则表达式&#xff0c;随后可以使用第二个插件 正则表达式插入之后顶部会有 Test Regex.. 点击会出现以下内容 将他 ctrl a 删除&#xff0c;输入你对应的正则表达…

HarmonyOS ArkTS HTTP数据请求(九)

1 概述 日常生活中我们使用应用程序看新闻、发送消息等&#xff0c;都需要连接到互联网&#xff0c;从服务端获取数据。例如&#xff0c;新闻应用可以从新闻服务器中获取最新的热点新闻&#xff0c;从而给用户打造更加丰富、更加实用的体验。 那么要实现这样一种能实时从服务…

Qt全球峰会2023中国站 参会概要

Qt全球峰会2023中国站 参会概要 前言峰会议程签到 & Demo 演示开场致辞Qt Group 产品总监演讲&#xff08;产品开发的趋势-开放的软件、工具和框架&#xff09;产品战略QtQuick or QtWidgets&#xff08;c or qml&#xff09;Qt如何定义AI个人看法 Qt 在券商数字化转型和信…

fseek 写操作定位无效问题

1、 fseek 代码中明明使用了fseek定位到行首。 fseek(p, 0, SEEK_SET); 但是写的内容&#xff0c;仍然添加到文件尾。 最后发现是fopen时&#xff0c;出现问题。 r 打开只读文件&#xff0c;该文件必须存在。 r 打开可读写的文件&#xff0c;该文件必须存在。 w 打开只写文…

配电房智能综合监控系统

配电房智能综合监控系统是一种针对配电房环境和设备进行实时监控和管理的系统。依托电易云-智慧电力物联网&#xff0c;它集成了多种先进技术&#xff0c;如物联网、大数据、AI视频智能分析等&#xff0c;实现对配电房全方位、智能化的监控和管理。 这个系统的主要功能可能包括…

12英寸双轴半自动划片机:颠覆传统划切工艺的五大优势

随着科技的飞速发展&#xff0c;半导体行业对精密划切设备的需求日益增长。在这篇文章中&#xff0c;我们将深入探讨12英寸双轴半自动划片机的优势&#xff0c;这种划片机在半导体制造过程中扮演着至关重要的角色。以下是这种划片机的五大优势。 一、高精度划切 12英寸双轴半自…

ABAP调用Https接口 Ssl证书导入

ABAP调用Https接口 Ssl证书导入 一、证书导入 谷歌浏览器打开对方系统URL地址&#xff0c;下载SSL Server certificate,步骤如下&#xff1a; 浏览器打开要导出certificate(证书)的网站&#xff0c;点击这个小锁的图标&#xff1a; 点击连接是安全的后面小播放按钮 点击证…

【深度学习】参数优化和训练技巧

寻找合适的学习率(learning rate) 学习率是一个非常非常重要的超参数&#xff0c;这个参数呢&#xff0c;面对不同规模、不同batch-size、不同优化方式、不同数据集&#xff0c;其最合适的值都是不确定的&#xff0c;我们无法光凭经验来准确地确定lr的值&#xff0c;我们唯一可…

22LLMSecEval数据集及其在评估大模型代码安全中的应用:GPT3和Codex根据LLMSecEval的提示生成代码和代码补全,CodeQL进行安全评估

LLMSecEval: A Dataset of Natural Language Prompts for Security Evaluations 写在最前面主要工作 课堂讨论大模型和密码方向&#xff08;没做&#xff0c;只是一个idea&#xff09; 相关研究提示集目标NL提示的建立NL提示的建立流程 数据集数据集分析 存在的问题 写在最前面…

编码的发展历史

编码的发展历史 ASCII&#xff1a; ASCII编码使用7位二进制数表示一个字符&#xff0c;范围从0到127。每个字符都有一个唯一的ASCII码值与之对应。例如&#xff0c;大写字母"A"的ASCII码是65&#xff0c;小写字母"a"的ASCII码是97。 ASCII字符集包括英文…

2023年亚太赛C题目保姆级思路代码 新能源电动汽车的发展趋势

2023年亚太赛已于23号上午6点正式开启&#xff01;本次题目难度主要在于数据都没给&#xff0c;需要进行数据收集和处理&#xff0c;总的难度看起来是C<B<A,本次我也将持续更新每道题目的思路&#xff0c; 大家也可以关注B站实时观看思路进度哦&#xff01; 不知名数学…

大数据湖及应用平台建设解决方案:PPT全39页,附下载

关键词&#xff1a;大数据湖建设&#xff0c;集团大数据湖&#xff0c;大数据湖仓一体&#xff0c;大数据湖建设解决方案 一、大数据湖定义 大数据湖是一个集中式存储和处理大量数据的平台&#xff0c;主要包括存储层、处理层、分析层和应用层四个部分。 1、存储层&#xff…

NX二次开发UF_CAM_set_clear_plane_data 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CAM_set_clear_plane_data Defined in: uf_cam_planes.h int UF_CAM_set_clear_plane_data(tag_t object_tag, double origin [ 3 ] , double normal [ 3 ] ) overview 概述 De…

40、Flink 的Apache Kafka connector(kafka source 和sink 说明及使用示例) 完整版

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

Altium Designer学习笔记9

忽视了一个最大的问题&#xff0c;就是元器件的封装&#xff0c;不应该是根据AD系统的封装走&#xff0c;而应该是根据立创商城上的规格书&#xff0c;确认每个封装的大小&#xff0c;画出封装图&#xff0c;然后才是布局和走线。 1、确认电容的封装采用0805&#xff0c;贴片电…