当前位置: 首页 > news >正文

通过方法引用获取属性名的底层逻辑是什么?

很多小伙伴可能都用过 MyBatis-Plus,这里边我们构造 where 条件的时候,可以直接通过方法引用的方式去指定属性名:

LambdaQueryWrapper<Book> qw = new LambdaQueryWrapper<>();
qw.eq(Book::getId, 2);
List<Book> list = bookMapper.selectList(qw);
System.out.println("list = " + list);

Book::getId 这就是方法引用,之前也专门写过文章介绍相关内容,这里就不再多说。这里我们就单纯来说说为什么 MP 通过 Book::getId 就可以识别出来这里的属性名。

1. 源码分析

这个问题其实好解决,我们顺着 qw.eq 这个方法往下看就可以了,这个方法在执行的过程中几经辗转会来到 getColumnCache 方法中,这个方法就是解析出来属性值的地方。

protected ColumnCache getColumnCache(SFunction<T, ?> column) {LambdaMeta meta = LambdaUtils.extract(column);String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());Class<?> instantiatedClass = meta.getInstantiatedClass();tryInitCache(instantiatedClass);return getColumnCache(fieldName, instantiatedClass);
}

首先这里先将我们传入的 Lambda 表达式通过 LambdaUtils.extract 方法解析出来一个 LambdaMeta 对象。

public static <T> LambdaMeta extract(SFunction<T, ?> func) {// 1. IDEA 调试模式下 lambda 表达式是一个代理if (func instanceof Proxy) {return new IdeaProxyLambdaMeta((Proxy) func);}// 2. 反射读取try {Method method = func.getClass().getDeclaredMethod("writeReplace");method.setAccessible(true);return new ReflectLambdaMeta((SerializedLambda) method.invoke(func), func.getClass().getClassLoader());} catch (Throwable e) {// 3. 反射失败使用序列化的方式读取return new ShadowLambdaMeta(com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.extract(func));}
}

这块的重点其实就在反射读取这块,这是从我们传入的 Lambda 中找到了一个名为 writeReplace 的方法,并且通过反射执行了这个方法,然后将执行结果封装为一个 ReflectLambdaMeta 对象返回。

接下来回到 getColumnCache 方法中,继续通过 String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName()); 获取到属性名称。

这里有一个 meta.getImplMethodName() 方法,这个方法的拿到的其实就是我们 Lambda 表达式中的方法名,也就是 getId,然后再通过 PropertyNamer.methodToProperty 对这个方法名进行处理,最终拿到属性名:

public static String methodToProperty(String name) {if (name.startsWith("is")) {name = name.substring(2);} else if (name.startsWith("get") || name.startsWith("set")) {name = name.substring(3);} else {throw new ReflectionException("Error parsing property name '" + name + "'.  Didn't start with 'is', 'get' or 'set'.");}if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) {name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);}return name;
}

大家看到,这个解析的过程其实就是把方法名的前缀 get/set/is 这些去掉,然后剩余的字符串首字母小写之后返回。

这就是我们传入 Book::getId,最终能够拿到 id 这个名称的原因。

现在的问题变成了 writeReplace 方法究竟是个什么方法?

2. writeReplace

这个方法其实是系统底层自动生成的。我们可以将 Lambda 表达式在运行时生成的字节码保存下来,然后进行反编译,这样就能够看到 writeReplace 方法了。

如果需要将 Lambda 运行时生成的字节码保存,需要在启动参数中添加如下内容:

-Djdk.internal.lambda.dumpProxyClasses=/Users/sang/workspace/code/mp_demo/lambda/

等于号后面的部分是指定生成的字节码的保存位置,大家可以根据自己的实际情况去配置。

以本文一开头的 Lambda 表达式为例,最终生成的字节码反编译之后,内容如下:

final class MpDemo02ApplicationTests$$Lambda$1164 implements SFunction {private MpDemo02ApplicationTests$$Lambda$1164() {}public Object apply(Object var1) {return ((Book)var1).getId();}private final Object writeReplace() {return new SerializedLambda(MpDemo02ApplicationTests.class, "com/baomidou/mybatisplus/core/toolkit/support/SFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "org/javaboy/mp_demo02/model/Book", "getId", "()Ljava/lang/Integer;", "(Lorg/javaboy/mp_demo02/model/Book;)Ljava/lang/Object;", new Object[0]);}
}

大家可以看到,apply 方法实际上是重写的接口的方法,在这个方法中将传入的对象强转为 Book 类型,然后调用其 getId 方法。

然后大家看到,反编译之后多了一个 writeReplace 方法,这个方法的返回值是一个 SerializedLambda,这个 SerializedLambda 对象其实就是对 Lambda 表达式的描述。基本上每个参数都能做到见名知意,我这里说一下第七个参数,值是 getId,这个参数的变量名是 implMethodName,这就是我们 Lambda 表达式中给出来的变量名。这也是第一小节中,meta.getImplMethodName() 所获取到的值。

这下就清楚了,为什么写了 Book::getId 就能拿到属性名了。

3. 扩展知识

有的小伙伴注意到,在 qw.eq(Book::getId, 2); 方法中,第一个参数是一个 SFunction 的实例,那就说我直接给一个 SFunction 的实例,不用 Lambda。大家注意,这种写法不对!

原因在于经过前面的源码分析之后,我们发现,MP 中根据 Book::getId 去获取属性名称,一个关键点是利用 Lambda 在执行的时候生成的字节码去获取,如果你都没有用 Lambda,那也就不会生成所谓的 Lambda 字节码,也就不存在 writeReplace 方法,按照前文所分析的源码,就无法获取到属性名称。

还有小伙伴说,既然是 Lambda,那么我不用方法引用行不行?我像下面这样写行不行?

LambdaQueryWrapper<Book> qw = new LambdaQueryWrapper<>();
qw.eq(b -> b.getId(), 2);
List<Book> list = bookMapper.selectList(qw);
System.out.println("list = " + list);

这也是一个 Lambda,但是如果你这样写了,运行之后就会报错。为什么呢?我们来看下这个 Lambda 生成的字节码反编译之后是什么样的:

final class MpDemo02ApplicationTests$$Lambda$1164 implements SFunction {private MpDemo02ApplicationTests$$Lambda$1164() {}public Object apply(Object var1) {return MpDemo02ApplicationTests.lambda$test18$3fed5817$1((Book)var1);}private final Object writeReplace() {return new SerializedLambda(MpDemo02ApplicationTests.class, "com/baomidou/mybatisplus/core/toolkit/support/SFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 6, "org/javaboy/mp_demo02/MpDemo02ApplicationTests", "lambda$test18$3fed5817$1", "(Lorg/javaboy/mp_demo02/model/Book;)Ljava/lang/Object;", "(Lorg/javaboy/mp_demo02/model/Book;)Ljava/lang/Object;", new Object[0]);}
}

首先大家注意到 apply 方法生成的就不一样,apply 里边调用了 MpDemo02ApplicationTests.lambda$test18$3fed5817$1 方法,传入了 Book 对象作为参数。这个方法内容相当于就是 return book.getId();。然后在 writeReplace 方法中,返回 SerializedLambda 对象的时候,implMethodName 的值就是 lambda$test18$3fed5817$1 了。回到本文一开始的源码分析中,你会发现这样的方法名就无法提取出来我们想要的属性名。所以这种写法也不对。

从这里大家也可以看到,类似于 b -> b.getId() 这样的 Lambda,和方法引用 Book::getId 在底层是不同的。

再给小伙伴们举个例子,比如下面一段代码:

public class Demo01 {public static void main(String[] args) {Consumer<String> out1 = System.out::println;out1.accept("javaboy");Consumer<String> out2 = s -> System.out.println(s);out2.accept("江南一点雨");}
}

这里有两个输出,第一个是一个方法引用,第二个则是一个常规的 Lambda 表达式。这两个执行起来效果是一致的,但是底层原理不同。

先来看第一个底层生成的 Lambda 字节码:

final class Demo01$$Lambda$14 implements Consumer {private final PrintStream arg$1;private Demo01$$Lambda$14(PrintStream var1) {this.arg$1 = var1;}public void accept(Object var1) {this.arg$1.println((String)var1);}
}

可以看到,这里把 System.out 的值 PrintStream 作为构造函数的参数传进来赋值给 arg$1 变量,当调用 accept 方法的时候,再调用 arg$1.println 方法将字符串输出。

对于第二个底层生成的 Lambda 字节码如下:

final class Demo01$$Lambda$16 implements Consumer {private Demo01$$Lambda$16() {}public void accept(Object var1) {Demo01.lambda$main$0((String)var1);}
}

可以看到,这里有一个新的 lambda$main$0 方法,这个方法的底层逻辑其实就是我们自定义 Lambda 的时候写的 System.out.println(s)

3. 小结

好啦,一篇小文,和小伙伴们探讨下 MP 中 qw.eq(Book::getId, 2); 方法的底层逻辑。

前端的世界总是在不断变化,作为开发者,我们需要保持好奇心和学习热情,不断探索新的技术,只有这样,我们才能在这个快速发展的时代中立于不败之地。Web Components 就是这样一个值得我们深入探索的领域,让我们拭目以待,它将给前端世界带来怎样的变革。

介绍一款程序员都应该知道的软件JNPF快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。

这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;集成了代码生成器,支持前后端业务代码生成,实现快速开发,提升工作效率;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3。如果你有闲暇时间,可以做个知识拓展。

看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

相关文章:

通过方法引用获取属性名的底层逻辑是什么?

很多小伙伴可能都用过 MyBatis-Plus&#xff0c;这里边我们构造 where 条件的时候&#xff0c;可以直接通过方法引用的方式去指定属性名&#xff1a; LambdaQueryWrapper<Book> qw new LambdaQueryWrapper<>(); qw.eq(Book::getId, 2); List<Book> list bo…...

自学错误合集--项目打包报错,运行报错持续更新中

java后端自学错误总结 一.项目打包报错2.项目打包之后运行报错 二.项目运行报错 一.项目打包报错 javac: &#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ļ&#xfffd;: E:\xx\xx\xx\docer-xx\src\main\java\xx\xx\xx\xx\xx\xx.java &#xfffd;&#xff…...

KUKA机器人故障报警信息处理(一)

1、KSS00276 机器人参数不等于机器人类型 ①登录专家模式 ②示教器操作&#xff1a;【菜单】—【显示】—【变量】—【单个】 ③名称输入&#xff1a;$ROBTRAFO[] 新值&#xff1a;TRAFONAME[] ④点击【设定值】。 2、电池报警&#xff1a; ①“充电电池警告-发现老化的蓄电池…...

数仓开发:DIM层数据处理

一、了解DIM层 这个就是数仓开发的分层架构 我们现在是在DIM层&#xff0c;从ods表中数据进行加工处理&#xff0c;导入到dwd层&#xff0c;但是记住我们依然是在DIM层&#xff0c;而非是上面的ODS和DWD层。 二、处理维度表数据 ①先确认hive的配置 -- 开启动态分区方案 -- …...

echars设置渐变颜色的方法

在我们日常的开发中&#xff0c;难免会遇到有需求&#xff0c;需要使用echars设置渐变的图表&#xff0c;如果我们需要设置给图表设置渐变颜色的话&#xff0c;我们只需要在 series 配置项中 添加相应的属性配置项即可。 方式一&#xff1a;colorStops type&#xff1a;‘lin…...

SpringBoot3项目打包和运行

六、SpringBoot3项目打包和运行 6.1 添加打包插件 在Spring Boot项目中添加spring-boot-maven-plugin插件是为了支持将项目打包成可执行的可运行jar包。如果不添加spring-boot-maven-plugin插件配置&#xff0c;使用常规的java -jar命令来运行打包后的Spring Boot项目是无法找…...

Spring Cloud Gateway的部署

不要将 Spring Cloud Gateway 部署到 Tomcat 可以将Spring Cloud Gateway打成jar包&#xff0c;并通过jar包部署&#xff0c;步骤&#xff1a; 1. 修改构建配置 确保你的pom.xml文件中的打包方式为jar。 <packaging>jar</packaging> 2 打包项目 mvn clean pack…...

算法提高之树的最长路径

算法提高之树的最长路径 核心思想&#xff1a;树形dp 枚举路径的中间节点用f1[i] 表示i的子树到i的最长距离,f2[i]表示次长距离最终答案就是max(f1[i]f2[i]) #include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N …...

git/gerrit使用遇到的问题

Push时出现的多个问题及其解决 branch【...】not found 这个错误通常出现在 Git 命令中指定的分支名称中包含特殊字符或者语法错误时。需要确保指定的分支名称是正确的&#xff0c;并且没有任何不支持的字符。 例如&#xff0c;如果分支名称是 feature/branch&#xff0c;应该…...

机器学习第二天(监督学习,无监督学习,强化学习,混合学习)

1.是什么 基于数据寻找规律从而建立关系&#xff0c;进行升级&#xff0c;如果是以前的固定算式那就是符号学习了 2.基本框架 3.监督学习和无监督式学习&#xff1a; 监督学习&#xff1a;根据正确结果进行数据的训练&#xff1b; 在监督式学习中&#xff0c;训练数据包括输…...

Rust 解决循环引用

导航 循环引用一、现象二、解决 循环引用 循环引用出现的一个场景就是你指向我&#xff0c;我指向你&#xff0c;导致程序崩溃 解决方式可以通过弱指针&#xff0c;而Rust中的弱指针就是Weak 在Rc中&#xff0c;可以实现&#xff0c;对一个变量&#xff0c;持有多个不可变引…...

ICC2:如何解决pin density过高引起的绕线问题

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 为了追求极致的利用率,综合往往会使用大量的AOI/OAI等多pin cell,然而后端实现过程中,工具为了解决绕线难题,又会通过降低local density的方法实现反向奔赴,即便如此,绕线后仍会残留不少问题,…...

Buuctf-Misc题目练习

打开后是一个gif动图&#xff0c;可以使用stegsolve工具进行逐帧看。 File Format:文件格式 Data Extract:数据提取 Steregram Solve:立体试图 可以左右控制偏移 Frame Browser:帧浏览器 Image Combiner:拼图&#xff0c;图片拼接 所以可以知道我们要选这个Frame Browser …...

费马小定理详解

费马小定理 定义&#xff1a; 设 p 为素数&#xff0c;a 为整数&#xff0c;则 a p ≡ a ( m o d p ) a^p \equiv a\ (\mod p) ap≡a (modp) &#xff0c;若 p ∤ a p \nmid a p∤a &#xff0c;则 a p − 1 ≡ 1 ( m o d p ) a^{p-1} \equiv 1\ (\mod p) ap−1≡1 (modp)…...

PXE批量安装

系统装机的三种引导方式 u盘光盘网络装机 光盘&#xff1a; 1.类似于usb模式 2.刻录模式 系统安装过程 加载boot loader Boot Loader 是在操作系统内核运行之前运行的一段小程序。通过这段小程序&#xff0c;我们可以初始化硬件设备、建立内存空间的映射图&#xff0c;从…...

stm32f103c8t6最小系统板

STM32F103C8T6最小系统板是为基于ARM Cortex-M3内核的STM32F103C8T6微控制器设计的电路板&#xff0c;它包含了单片机正常运行所需的最基本组件。以下是构成STM32F103C8T6最小系统板的基本部分&#xff1a; 单片机芯片&#xff1a;STM32F103C8T6本身&#xff0c;它是一款32位微…...

QCefView 在 Linux 下的编译(更新)

在前面的文章《QT 应用程序中集成浏览器》中已经介绍过 QCefView 的构建。这几天发现 QCefView 代码进行了更新,构建方式也发生了一点点变化,所以在此更新一下 QCefView 的编译方法。 QCefView 其实包含了两个项目,一个就是 QCefView 项目本身,另外一个就是 CefViewCore。…...

无卤素产品是什么?有什么作用?

无卤素产品&#xff0c;即在生产过程中完全不使用卤素元素——氟、氯、溴、碘等——的产品。 卤素元素&#xff0c;虽然在电子设备、材料等领域应用广泛&#xff0c;却也可能潜藏危害。其阻燃剂&#xff0c;一旦在产品生命周期结束后释放&#xff0c;将对土壤和水体造成污染&a…...

esp32-cam 1. 出厂固件编译与测试

0. 环境 - ubuntu18 - esp32-cam - usb转ttl ch340 硬件连接 esp32-camch340板子U0RTXDU0TRXDGNDGND5V5V 1. 安装依赖 sudo apt-get install vim sudo apt install git sudo apt-get install git wget flex bison gperf python python-pip python-setuptools python-serial p…...

题目:线性代数

问题描述&#xff1a; 解题思路&#xff1a; 列相乘&#xff0c;然后行相加。 注意点&#xff1a;由于元素数据范围最大为1e6&#xff0c;两个元素相乘乘积最大为1e12&#xff0c;如果元素类型为int则在乘的过程中就会爆炸&#xff0c;所以需要开long long类型。 AC代码…...

Python|GIF 解析与构建(5):手搓截屏和帧率控制

目录 Python&#xff5c;GIF 解析与构建&#xff08;5&#xff09;&#xff1a;手搓截屏和帧率控制 一、引言 二、技术实现&#xff1a;手搓截屏模块 2.1 核心原理 2.2 代码解析&#xff1a;ScreenshotData类 2.2.1 截图函数&#xff1a;capture_screen 三、技术实现&…...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

Go 语言接口详解

Go 语言接口详解 核心概念 接口定义 在 Go 语言中&#xff0c;接口是一种抽象类型&#xff0c;它定义了一组方法的集合&#xff1a; // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的&#xff1a; // 矩形结构体…...

全球首个30米分辨率湿地数据集(2000—2022)

数据简介 今天我们分享的数据是全球30米分辨率湿地数据集&#xff0c;包含8种湿地亚类&#xff0c;该数据以0.5X0.5的瓦片存储&#xff0c;我们整理了所有属于中国的瓦片名称与其对应省份&#xff0c;方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

质量体系的重要

质量体系是为确保产品、服务或过程质量满足规定要求&#xff0c;由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面&#xff1a; &#x1f3db;️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限&#xff0c;形成层级清晰的管理网络&#xf…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

Linux-07 ubuntu 的 chrome 启动不了

文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了&#xff0c;报错如下四、启动不了&#xff0c;解决如下 总结 问题原因 在应用中可以看到chrome&#xff0c;但是打不开(说明&#xff1a;原来的ubuntu系统出问题了&#xff0c;这个是备用的硬盘&a…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成

厌倦手动写WordPress文章&#xff1f;AI自动生成&#xff0c;效率提升10倍&#xff01; 支持多语言、自动配图、定时发布&#xff0c;让内容创作更轻松&#xff01; AI内容生成 → 不想每天写文章&#xff1f;AI一键生成高质量内容&#xff01;多语言支持 → 跨境电商必备&am…...