背景介绍
写这篇文章得从一个现象说起。使用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) 明确告诉框架,我们想要查询的字段嘛?他们理所应当的认为这里的推理过程应该是:
- SimpleModel::getOtherId,这里指明了方法名称
- 通过解析方法名称得到对象中的属性名为otherId
- 根据约定可知数据库中对应的字段是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,能拿到方法名称,自然就可以根据约定获取到属性名,进而推断出字段名称