浅谈JDK动态代理(上)

作者简介:大家好,我是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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于 Flink SQL 和 Paimon 构建流式湖仓新方案

本文整理自阿里云智能开源表存储负责人,Founder of Paimon,Flink PMC 成员李劲松在云栖大会开源大数据专场的分享。本篇内容主要分为四部分: 数据分析架构演进介绍 Apache PaimonFlink Paimon 流式湖仓流式湖仓Demo演示 数据分析架构演进 …

Flutter 父子组件通信

在Flutter 中父组件调用子组件的方法可以通过GlobalKey实现&#xff0c;而子组件调用父组件方法可以通过回调函数实现。 父组件 class _MyHomePageState extends State<MyHomePage> {final GlobalKey<LoadPencilState> loadPencilKey GlobalKey<LoadPencilSt…

在 Python 的 requests 二进制数据的传输方式发生了变化

在Python编程中&#xff0c;requests库是一个非常有用的工具&#xff0c;用于发送HTTP请求。由于其简单易用的API和广泛的兼容性&#xff0c;requests库已经成为Python开发者中最常用的网络请求库之一。 然而&#xff0c;最近在requests 0.10.1版本中&#xff0c;POST二进制数据…

课堂巡课如何提升教学质量?简单才是硬道理

随着教育技术的不断发展&#xff0c;在线巡课系统逐渐成为学校管理和教育质量提升的重要工具。在线巡课系统通过数字化手段&#xff0c;为学校提供了更加高效、精准的巡课管理方式&#xff0c;有力地支持了教育教学的改进和优化。 客户案例 小学巡课项目 山东某小学引入了泛地…

python练习题

1.身体质量指数 BMI指数即身体健康指数&#xff0c;它与人的体重和身高相关&#xff0c;是目前国际常用的衡量人体胖瘦程度以及是否健康的一个标准。已知BMI值的计算公式如下&#xff1a; 体质指数&#xff08;BMI&#xff09; 体重&#xff08;kg&#xff09;身高^2&#xf…

基于qemu_v8+optee 3.17平台的ca/ta Demo

1、整体集成构建 基于官方构建&#xff0c;加入自定义ca/ta后一体构建到rootfs&#xff0c;在qemu上运行 $ mkdir -p <optee-project> $ cd <optee-project> $ repo init -u https://github.com/OP-TEE/manifest.git -m ${TARGET}.xml [-b ${BRANCH}] $ repo syn…

IDEA的插件市场无法打开,无法连接到https://plugins.jetbrains.com/

1&#xff1a;网上搜到的&#xff1a; 在这里测试https://plugins.jetbrains.com/ 能否连接到&#xff0c;可以的话就成功&#xff0c;但是我一直失败&#xff0c;网络配置与防火墙也没问题。 2&#xff1a;我成功的方法&#xff1a; 把这个勾取消再测试&#xff0c;成功&…

【Linux】make/Makefile 进度条小程序

目录 一&#xff0c;认识 make/makefile 二&#xff0c;实例代码 1&#xff0c;依赖关系 2&#xff0c;原理 3&#xff0c;项目清理 4&#xff0c;测试讲解 三&#xff0c;Linux第一个小程序&#xff0d;进度条 game.h game.c test.c 程序详解 一&#xff0c;认识 m…

Notepad-- ubuntu下载安装

Notepad-- ubuntu下载安装 下载 Gitee链接&#xff1a; https://gitee.com/cxasm/notepad– 安装 sudo apt install *.deb运行 /opt/apps/com.hmja.notepad/files/Notepad--出错 需要安装qt5 sudo apt-get install qt5-default

C#,简单修改Visual Studio 2022设置以支持C#最新版本的编译器,尊享编程之趣

1 PLS README & CHAPTER 5 用一个超简单的例子说明各版本 C# 的差异。 使用新版本&#xff08;比如C#.11&#xff09;&#xff0c;当然有一定的好处。我们在写程序的时候一般这样&#xff1a; Visual Studio 2022 默认只能这样写&#xff1a; string imageFile Path.C…

如何解决跨国访问Microsoft 365网络卡顿问题?

作为主流的协调办公工具&#xff0c;Microsoft 365(旧称Office 365)是众多企业每天必须访问的应用&#xff0c;但由于多种原因&#xff0c;许多企业在跨区域访问Microsoft 365服务器时常面临卡顿、掉线等问题&#xff0c;对工作效率产生严重影响。 对此&#xff0c;连官方也专门…

视频监控平台EasyCVR+智能分析网关+物联网,联合打造智能环卫监控系统

一、背景介绍 城市作为人们生活的载体&#xff0c;有着有无数楼宇和四通八达的街道&#xff0c;这些建筑的整洁与卫生的背后&#xff0c;是无数环卫工作人员的努力。环卫工人通过清理垃圾、打扫街道、清洗公共设施等工作&#xff0c;保持城市的整洁和卫生&#xff0c;防止垃圾…

java学习part08权限

1.权限表格 外部类都是公有和缺省&#xff0c;因为其他两种对于外部类没有意义 一些内部成分都各种权限都可以 2.如何体现java封装性 答&#xff0c;通过权限控制&#xff0c;保证哪些可以给人看到&#xff0c;哪些不能

MySQL--慢查询(一)

1. 查看慢查询日志是否开启 show variables like slow_query%; show variables like slow_query_log; 参数说明&#xff1a; 1、slow_query_log&#xff1a;这个参数设置为ON&#xff0c;可以捕获执行时间超过一定数值的SQL语句。 2、long_query_time&#xff1a;当SQL语句执行…

【Sorted Set】Redis常用数据类型: ZSet [使用手册]

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 目录 ⑤Redis Zset 操作命令汇总1. zadd 添加或…

软件系统测试有哪些测试流程?系统测试报告编写注意事项

在软件开发的过程中&#xff0c;系统测试是至关重要的一环&#xff0c;它的目的是验证和评估软件产品是否符合预期的质量标准&#xff0c;以确保系统的稳定性、可靠性和安全性。 一、软件系统测试的测试流程 1、需求分析与测试计划制定&#xff1a;根据需求分析确定测试目标、…

Linux:设置Ubuntu的root用户密码

执行以下命令&#xff1a; 给root用户设置密码 sudo passwd 输入两次密码 切换root su root 退出root用户 exit

需求调研计划及用户需求调研单

1.目的 2.概述 3.需求调研计划 3.1调研目的 3.2调研范围 3.2.1.调研的职能范围 3.2.2.调研的业务范围 3.2.3.调研的地点范围 3.3调研方式 3.4调研阶段 3.5具体时间安排 软件开发全文档获取&#xff1a;点我获取 1、需求调研计划 2、用户需求调研单 项目名称 客…

boardmix AI思维导图,一键自动生成思维导图!

在日常学习和工作中&#xff0c;我们常常需要记忆和整理大量的知识点和思维结构。 此时&#xff0c;思维导图的存在就大大方便了我们的工作。与传统的文本笔记不同&#xff0c;思维导图可以结合文字、图像、颜色等多种元素&#xff0c;帮助我们更好地整理和分析知识的关系&…

腾讯又出王炸产品!使用混元大模型进行数据报表测试

最近腾讯出了自己的大模型&#xff0c;命名混元。 现在已经开始内测&#xff0c;感谢腾讯小伙伴卢晓明同学帮我们提前申请到了内测机会&#xff0c;接下来我们用腾讯混元大模型与实际工作结合&#xff0c;开始我的报表测试之旅。 腾讯混元大模型官方入口:https://hunyuan.ten…