ORM框架中获取Lambda方法引用的字段名称

108 阅读4分钟

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));
    }
}
    

可以看到有三种实现方式,分别是

  1. 在IDEA调试模式下的实现方式
  2. 通过反射读取的方式
  3. 反射失败使用序列化的方式读取

下面重点讲解第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的方法引用提出了以下要求:

  1. 方法引用的implClass要实现序列化接口
  2. 函数式接口要继承序列化接口