谈谈SerializedLambda

背景介绍

写这篇文章得从一个现象说起。使用Mybatis Plus的开发可能对如下代码非常熟悉:

假设存在一个数据库表的映射结构,我们叫它SimpleModel

java 复制代码
@Getter
@Setter
@Accessors(chain = true)
@EqualsAndHashCode(callSuper=false)
@TableName("tbl_simple")
public class SimpleModel implements Serializable {

    private Integer code;
    
    private Long otherId;
        
}

我们有一个根据otherId获取数据库数据的需求,那么对应的Wraper Query语句大概是:

java 复制代码
public SimpleModel findByOtherId(Integer otherId) {
    QueryWrapper<SimpleModel> wrapper = new QueryWrapper<>();
    wrapper
        .lambda()
        .eq(SimpleModel::getOtherId, otherId)
        .last("LIMIT 1");
        
    return baseMapper.selectOne(wrapper);
}

经过框架处理,会生成对应的查询语句:

sql 复制代码
SELECT code, other_id
FROM tbl_simple
WHERE other_id = #{otherId}
LIMIT 1

所以问题来了,other_id到底是怎么解析而来的呢?
或者说本质是otherId到底是怎么解析而来的呢?

从普通Lambda说起

上面的问题,可能有人会不加思索,脱口而出:那不是因为eq(SimpleModel::getOtherId, otherId) 明确告诉框架,我们想要查询的字段嘛?他们理所应当的认为这里的推理过程应该是:

  1. SimpleModel::getOtherId,这里指明了方法名称
  2. 通过解析方法名称得到对象中的属性名为otherId
  3. 根据约定可知数据库中对应的字段是other_id
java 复制代码
Function<SimpleModel, Long> func = SimpleModel::getOtherId;
Class<?> clazz = func.getClass();

使用func变量接收这个对象,然后拿到Class对象,然后解析方法,这不是很简单吗?

很显然第一步假设就是错误的,这么推理的基本上对Java Lambda的理解十分有限。因为Java Lambda本质上是一个对象,SimpleModel::getOtherId可以是java.util.function.Function(入参一个,返回一个)的一个实现。

java 复制代码
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    static <T> Function<T, T> identity() {
        return t -> t;
    }

}

所以func.getClass()获取到的Class对象只能解析到java.util.function.Function,java.lang.Object(Lambda对象基类依然是Object)中声明的方法。

不信我们可以尝试一下: 事实胜于雄辩,根本就不可能解析到otherId相关的任何信息。

Mybatis Plus的魔法

当时思考到此处,按照我的知识储备,无法解释Mybatis Plus凭什么可以解析到方法名。源码之下无秘密,于是我直接翻看了相关代码:

java 复制代码
/* com.baomidou.mybatisplus.core.conditions.interfaces.Compare#eq(R, java.lang.Object) */
default Children eq(R column, Object val) {
    return eq(true, column, val);
}

仅仅只看声明,无法得到有价值的信息,于是我们继续往下看其中一个实现:

java 复制代码
public class LambdaQueryWrapper<T> 
    extends AbstractLambdaWrapper<T, LambdaQueryWrapper<T>>
    implements Query<LambdaQueryWrapper<T>, T, SFunction<T, ?>> {
}

由此可以得出,在Mybatis plus源码中SimpleModel::getOtherId并没有被声明为Jdk自带的Function类型,很奇怪只从方法的功能性上Function明明完全足够了,为什么还要额外声明一个SFunction类型呢?

java 复制代码
@FunctionalInterface
public interface SFunction<T, R> 
    extends Function<T, R>, Serializable {

}

而且这个声明仅仅是继承了Function接口,唯一的区别就是还额外继承了Serializable接口,直觉告诉我秘密可能就在序列化这里,事实也是如此。

核心原理

JVM对Lambda的处理机制

当JVM处理Lambda表达式时,会根据目标接口的特性采用不同的策略:

  • 普通函数接口:JVM会生成一个轻量级的代理类,这个类不保留原始方法的详细信息
  • 可序列化函数接口:为了支持序列化,JVM必须保留足够的元信息来重建Lambda表达式

序列化机制的关键作用

实现了Serializable接口的Lambda表达式会包含一个特殊的方法writeReplace(),这个方法返回一个SerializedLambda对象,其中包含了丰富的元信息。

java 复制代码
public class SerializedLambda implements Serializable {

    private static final long serialVersionUID = 8025925345765570181L;

    private Class<?> capturingClass;
    
    private String functionalInterfaceClass;
    
    private String functionalInterfaceMethodName;
    
    private String functionalInterfaceMethodSignature;
    
    private String implClass;
    
    private String implMethodName;
    
    private String implMethodSignature;
    
    private int implMethodKind;
    
    private String instantiatedMethodType;
    
    private Object[] capturedArgs;
    
}

通过SerializedLambda完成解析

于是我按照资料说明进行尝试,很明显继承序列化接口之后,DeclaredMethods方法列表明显多出一个名为"writeReplace"的方法。 那我们只需要想办法调用writeReplace方法,然后拿到Lambda的元信息即可;

Java 复制代码
interface SerialFunction<T, R> extends Function<T, R>, Serializable {

}


public static void main(String[] args) throws Exception {
    /* 声明为SerialFunction类型 */
    SerialFunction<SimpleModel, Long> func = SimpleModel::getOtherId;
    /* 反射获取writeReplace Method */
    Method writeReplace = func.getClass().getDeclaredMethod("writeReplace");
    writeReplace.setAccessible(true);
    /* 强转为SerializedLambda类型 */
    SerializedLambda serializedLambda = (SerializedLambda)writeReplace.invoke(func);
    /* 获取Lambda的原信息 */
    String implMethodName = serializedLambda.getImplMethodName();
}

获取到方法名为getOtherId,能拿到方法名称,自然就可以根据约定获取到属性名,进而推断出字段名称

相关推荐
Joey_Chen7 小时前
【Golang开发】Gin框架学习笔记——服务器的运行机制
后端·go
雨绸缪7 小时前
Excel上传失败,在剪切板有大量信息。是否保存其内容...
后端
带刺的坐椅7 小时前
Solon 权限认证之 Sa-Token 的使用与详解
java·sa-token·web·solon
二闹7 小时前
Python中那个看似神秘的if __name__ == __main__,一次给你讲明白
后端·python
用户8356290780518 小时前
C# 转换 Word 文档为图片:解锁文档处理的新维度
后端·c#
HABuo8 小时前
【C++进阶篇】学习C++就看这篇--->多态超详解
c语言·开发语言·c++·后端·学习
麦兜*8 小时前
MongoDB 源码编译与调试:深入理解存储引擎设计 内容详细
java·数据库·spring boot·mongodb·spring
编啊编程啊程8 小时前
响应式编程框架Reactor【9】
java·网络·python·spring·tomcat·maven·hibernate
前端老鹰8 小时前
Node.js 命令行交互王者:inquirer 模块实战指南
后端·node.js