0x01:背景
在很多数据库ORM框架中,经常有支持DSL方式通过方法引用要设置操作的表的列名字,这种方式相比传入字符串等要安全的多,本文目的就是总结背后的原理。
通常的使用方式比如在Mybatis-Plus中, 就有ne(Entity::getId) 的方式获取id字段名称,就以此为突破点看看是怎么实现的
public class AutoResultMapTest extends BaseDbTest<EntityMapper> {
@Test
void test() {
doTestAutoCommit(m -> m.insert(new Entity().setName("老王").setGg(new Entity.Gg("老王"))));
doTest(m -> {
Entity entity = m.selectOne(null);
assertThat(entity).as("插入正常").isNotNull();
assertThat(entity.getName()).as("名称不一致正常").isNotNull();
assertThat(entity.getGg()).as("typeHandler正常").isNotNull();
assertThat(entity.getGg().getName()).as("是老王").isEqualTo("老王");
});
doTest(m -> {
Entity entity = new Entity().setName("老王");
m.selectOne(Wrappers.lambdaQuery(entity).ne(Entity::getId, 1));
});
}
@Override
protected List<String> tableSql() {
return Arrays.asList("drop table if exists entity",
"CREATE TABLE IF NOT EXISTS entity (\n" +
"id BIGINT(20) NOT NULL,\n" +
"x_name VARCHAR(20) NOT NULL,\n" +
"gg VARCHAR(255) NULL DEFAULT NULL,\n" +
"PRIMARY KEY (id)" +
")");
}
}
0x02:原理
通过跟踪代码最终到达
com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper#getColumnCache(com.baomidou.mybatisplus.core.toolkit.support.SFunction<T,?>)
/**
* 获取 SerializedLambda 对应的列信息,从 lambda 表达式中推测实体类
* <p>
* 如果获取不到列信息,那么本次条件组装将会失败
*
* @return 列
* @throws com.baomidou.mybatisplus.core.exceptions.MybatisPlusException 获取不到列信息时抛出异常
*/
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);
}
其实主要就是通过 LambdaUtils.extract(column) 构建 LambdaMeta ,看下LambdaMeta的定义
/**
* Lambda 信息
* <p>
* Created by hcl at 2021/5/14
*/
public interface LambdaMeta {
/**
* 获取 lambda 表达式实现方法的名称
*
* @return lambda 表达式对应的实现方法名称
*/
String getImplMethodName();
/**
* 实例化该方法的类
*
* @return 返回对应的类名称
*/
Class<?> getInstantiatedClass();
}
注释已经写的很清楚了,其中getImplMethodName就是 Lambda 表达式对应的实现方法的名称,对于最上面那个单测的例子,这里 getImplMethodName方法就会返回 getId。接着深入到 LambdaUtils.extract(column) 方法中
/**
* 该缓存可能会在任意不定的时间被清除
*
* @param func 需要解析的 lambda 对象
* @param <T> 类型,被调用的 Function 对象的目标类型
* @return 返回解析后的结果
*/
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));
}
}
可以看到有三种实现方式,分别是
- 在IDEA调试模式下的实现方式
- 通过反射读取的方式
- 反射失败使用序列化的方式读取
下面重点讲解第2,第3种,其实2,3方式底层使用的 物料是一样的,而这个 物料就是反射要操作的对象,其实也就是上面的func或者说func.getClass.
所以问题现在转化成了 func 或者说 func.getClass 到底是什么样的?这个对象我们知道是一个 方法引用的Lambda表达式 - Entity::getId ,然后我们又知道,JDK对于 Lambda 表达式其实都是在运行时会动态生成实现相应Lambda接口的实例,所以如果把JDK动态生成的实例的字节码拿到然后反编译看下不就很清楚了吗
通过添加VM Options
-Djdk.internal.lambda.dumpProxyClasses=/User/xxx
就可以把JDK动态生成的类持久化到本地磁盘,然后查看了。下面以一个之前另一个Lambda方法引用TestEntity::getName 的dump文件为例看下
final class QueryTest$$Lambda$2 implements StaticMethodReferenceColumn {
private QueryTest$$Lambda$2() {
}
@Hidden
public Object apply(Object var1) {
return ((QueryTest.TestEntity)var1).getName();
}
private final Object writeReplace() {
return new SerializedLambda(QueryTest.class, "org/hswebframework/ezorm/core/StaticMethodReferenceColumn", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "org/hswebframework/ezorm/core/dsl/QueryTest$TestEntity", "getName", "()Ljava/lang/String;", "(Lorg/hswebframework/ezorm/core/dsl/QueryTest$TestEntity;)Ljava/lang/Object;", new Object[0]);
}
}
可以看到其中就有一个叫 writeReplace 的方法,这不是巧合,LambdaMeta#extract方法第2种方式就是想拿到 writeReplace的返回值 SerializedLambda 。所以我们需要的东西其实都在 SerializedLambda 里,其中SerializedLambda#implMethodName 就是方法引用的名称。
至于第三种其实也是为了拿到 SerializedLambda ,而通过序列化和反序列化的方式 拿到 SerializedLambda ,具体代码不再解读,有兴趣的小伙伴可以看下 com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda#extract 。
0x03:注意事项
从上面也可以看出来,为了拿到方法引用的名称,基本和序列化和反序列化有很大的关系,这也对我们的Lambda的方法引用提出了以下要求:
- 方法引用的implClass要实现序列化接口
- 函数式接口要继承序列化接口