谈谈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,能拿到方法名称,自然就可以根据约定获取到属性名,进而推断出字段名称

相关推荐
RoyLin15 小时前
C++ 基础与核心概念
前端·后端·node.js
aiopencode15 小时前
Charles 抓包 HTTPS 原理详解,从 CONNECT 到 SSL Proxying、常见问题与真机调试实战(含 Sniffmaster 补充方案)
后端
泉城老铁15 小时前
springboot 框架集成工作流的开源框架有哪些呢
spring boot·后端·工作流引擎
aloha_15 小时前
Ubuntu/Debian 系统中,通过包管理工具安装 Redis
后端
别惹CC15 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
java·人工智能·spring
Java水解15 小时前
深入探索Spring:Bean管理与Spring Boot自动配置原理
后端·spring
我是天龙_绍15 小时前
iframe 的 src 链接里带了参数(比如 token 或签名),想在 Nginx 层做鉴权
后端
大佐不会说日语~15 小时前
若依框架 (Spring Boot 3) 集成 knife4j 实现 OpenAPI 文档增强
spring boot·后端·python
Java微观世界15 小时前
枚举不止是常量!Java枚举的高级用法与单例最佳实践
后端
hhh小张15 小时前
HttpServlet(4):Cookie🍪与Session💻
后端