通过方法引用获取属性名的底层逻辑是什么?
很多小伙伴可能都用过 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);
方法的底层逻辑。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
mysqldump 备份产生大量慢查询,有办法过滤么?
MySQL 8.0.30 新功能,再也不用担心大量无效日志了! 作者:李富强,爱可生 DBA 团队成员,熟悉 MySQL,TiDB,OceanBase 等数据库。相信持续把对的事情做好一点,会有不一样的收获。 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 本文约 1600 字,预计阅读需要 5 分钟。 新特性介绍 MySQL 8.0.30 版本中,mysqldump 逻辑备份工具引入了 mysqld-long-query-time 选项,用于设置 mysqldump 备份的会话级别慢查询阈值 long_query_time。 生产环境 long_query_time 一般设置的比较小,由于 mysqldump 备份时执行的是全表扫描,SQL 执行时间很容易超过 long_query_time,导致慢查询日志中记录大量备份产生的慢查询。通过指定 mysqld-long-query-time 选项,给 mysqldump 单独设定合适的会话级别慢查询阈值,可以减少慢查询日志中大量无效慢查询。 使用方法 mysqldump 指定备份选项 mysqld-long...
- 下一篇
QAnything-1.3.0,支持纯python笔记本运行,支持混合检索
QAnything 1.3.0 更新了,这次带来两个主要功能,一个是纯python的安装,另一个是混合检索。更多详情见: https://github.com/netease-youdao/QAnything/releases 纯python安装 我们刚发布qanything开源的时候,希望用户可以用这个代码来直接在生产环境中部署使用,为了性能,它引入了很多第三方的库和服务,比如milvus,mysql,tritonserver,elasticsearch等。这些服务本身也非常庞大复杂,我们做了docker镜像和dockerfiles,试图将一些依赖打包起来,用户只要拉下来就可以用。但是还是有很多人遇到麻烦。比如不能在mac等笔记本上运行。 所以这次,这次我们发布了一个纯python的轻量级的版本,可以在mac等笔记本上跑起来,可以不依赖gpu。安装过程极其简单: 第一步:拉代码到本地 gitclonehttps://github.com/netease-youdao/QAnything 如果是国内的访问不了github的,可以用gitee,我们已经同步了代码。 git clone h...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7设置SWAP分区,小内存服务器的救世主
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群