LambdaQueryWrapper的实现原理分析和lambda的序列化问题

 更新时间:2022年01月11日 08:46:35   作者:之诚  
这篇文章主要介绍了LambdaQueryWrapper的实现原理分析和lambda的序列化问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教。

LambdaQueryWrapper的实现原理

mybatis-plus的LambdaQueryWrapper的lambda来组合查询字段的功能十分好用,但是它是如何实现的呢?

通过查看mybatis的源码发现它的功能主要是四个类来实现的。

在这里插入图片描述

我将其copy下来分析下。

SFunction 类

/**
 * 支持序列化的 Function
 *
 * @author miemie
 * @since 2018-05-12
 */
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}

我们知道每个lambda表达式都有一个对应的接口, 而mybatis-plus就是使用上面的接口来声明lambda表达式的。 可以看到它实现了Serializable接口。

LambdaUtils

/**
 * Lambda 解析工具类
 *
 * @author HCL, MieMie
 * @since 2018-05-10
 */
public final class LambdaUtils {
.....................
 /**
     * 获取对应的表字段与对象的属性关系对象
     *
     * @param func
     * @param <T>
     * @return
     */
    public static <T> EntityTableDefine.ColumnProp getColumnProp(SFunction<T, ?> func) {
        SerializedLambda resolve = LambdaUtils.resolve(func);
        return getColumnProp(resolve);
    }
  /**
     * 解析 lambda 表达式, 该方法只是调用了 {@link SerializedLambda#resolve(SFunction)} 中的方法,在此基础上加了缓存。
     * 该缓存可能会在任意不定的时间被清除
     *
     * @param func 需要解析的 lambda 对象
     * @param <T>  类型,被调用的 Function 对象的目标类型
     * @return 返回解析后的结果
     * @see SerializedLambda#resolve(SFunction)
     */
    public static <T> SerializedLambda resolve(SFunction<T, ?> func) {
        Class<?> clazz = func.getClass();
        return Optional.ofNullable(FUNC_CACHE.get(clazz))
                .map(WeakReference::get)
                .orElseGet(() -> {
                    SerializedLambda lambda = SerializedLambda.resolve(func);
                    FUNC_CACHE.put(clazz, new WeakReference<>(lambda));
                    return lambda;
                });
    }
  
  
  ................... 
}

把其中最重要的两个方法贴出来,resolve 方法才是重点。 可以看到其中调用了SerializedLambda.resolve(func);方法。

SerializedLambda

/**
 * 这个类是从 {@link java.lang.invoke.SerializedLambda} 里面 copy 过来的,
 * 字段信息完全一样
 * <p>负责将一个支持序列的 Function 序列化为 SerializedLambda</p>
 *
 * @author HCL
 * @since 2018/05/10
 */
@SuppressWarnings("unused")
public class SerializedLambda implements Serializable {
  ........
    
/**
     * 通过反序列化转换 lambda 表达式,该方法只能序列化 lambda 表达式,不能序列化接口实现或者正常非 lambda 写法的对象
     *
     * @param lambda lambda对象
     * @return 返回解析后的 SerializedLambda
     */
    public static SerializedLambda resolve(SFunction<?, ?> lambda) {
        if (!lambda.getClass().isSynthetic()) {
            throw ExceptionUtils.mpe("该方法仅能传入 lambda 表达式产生的合成类");
        }
        try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) {
            /**
             * 实现反序列化的类型的替换, 使用我们自定义的类型来替换java.lang.invoke.SerializedLambda类。
             * 为何可以替换成功, 因为反序列化的时候使用的是反射的方式赋值的, 只要两个类的方法名称或者字段名一样,反射调用是没有问题的。
             * @param objectStreamClass
             * @return
             * @throws IOException
             * @throws ClassNotFoundException
             */
            @Override
            protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
                Class<?> clazz = super.resolveClass(objectStreamClass);
                return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz;
            }
        }) {
            //因为前面的替换,这里获取的就是我们自己定义的SerializedLambda类
            return (SerializedLambda) objIn.readObject();
        } catch (ClassNotFoundException | IOException e) {
            throw ExceptionUtils.mpe("This is impossible to happen", e);
        }
    }
  
  .............. 
    
 }

SerializationUtils.serialize(lambda)方法就是正常的序列化类, 无什么特别的.

resolveClass方法才是重点方法, 这个方法的目的是获取反序列化后的类的类型,上面是被重新了。 参数ObjectStreamClass中是包含了反序列化后的类型,在jdk8之后lambda被反序列化后类型都是java.lang.invoke.SerializedLambda.class,这里重写进行了替换成自己定义的SerializedLambda类型。

两个类型的代码是一样的(没发现差异), mybatis-plus之所以复制这个类是为了方便控制吧(猜测)。 SerializedLambda类中就包含了lambda的方法的名称,而get/set方法的名称自然就能对应到具体的字段了。

至于为何可以替换的原因我在这个方法上面注释了。

思考

序列化和反序列化是比价消耗性能的, 所以mybatis-plus使用了static的Map和WeakReference来缓存了序列化后的SerializedLambda对象。 至于为何使用WeakReference的方式来做缓存, 可以参考下ThreadLocal的实现原理

其实mybatis-plus的实现方式显得繁琐了。其实没有必要去复制SerializedLambda类代码,也没有必要去真的序列化和反序列。

对象序列化中的 writeReplace 和 readResolve

  • writeReplace:在将对象序列化之前,如果对象的类或父类中存在writeReplace方法,则使用writeReplace的返回值作为真实被序列化的对象;writeReplace在writeObject之前执行;
  • readResolve:在将对象反序列化之后,ObjectInputStream.readObject返回之前,如果从对象流中反序列化得到的对象所属类或父类中存在readResolve方法,则使用readResolve的返回值作为ObjectInputStream.readObject的返回值;readResolve在readObject之后执行;

函数式接口如果继承了Serializable,使用Lambda表达式来传递函数式接口时,编译器会为Lambda表达式生成一个writeReplace方法,这个生成的writeReplace方法会返回java.lang.invoke.SerializedLambda;可以从反射Lambda表达式的Class证明writeReplace的存在(具体操作与截图在后面);所以在序列化Lambda表达式时,实际上写入对象流中的是一个SerializedLambda对象,且这个对象包含了Lambda表达式的一些描述信息;

SerializedLambda类中有readResolve方法,这个readResolve方法中通过反射调用了Lambda表达式所在外部类中的** deserializeLambda deserializeLambda deserializeLambda**方法,这个方法是编译器自动生成的,可以通过反编译.class字节码证明(具体操作与截图在后面); deserializeLambda deserializeLambda deserializeLambda方法内部解析SerializedLambda,并调用LambdaMetafactory.altMetafactory或LambdaMetafactory.metafactory方法(引导方法)得到一个调用点(CallSite),CallSite会被动态指定为Lambda表达式代表的函数式接口类型,并作为Lambda表达式返回;所以在从对象流反序列化得到SerializedLambda对象之后,又被转换成原来的Lambda表达式,通过ObjectInputStream.readObject返回;

从上面的黑体中就能够知道, 在序列化lambda的时候实际上是序列化了SerializedLambda对象,所以反序列化后就能获取SerializedLambda对象了。 实际上序列化的对象是通过writeReplace方法产生的,那么我们要获取SerializedLambda对象没必要真的序列化和反序列化一遍。 反射调用writeReplace方法就可以了。

具体示例如下

package xyz.xiezc.ioc.starter.orm.lambda;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
@Data
public class LambdaTest {
    private String fieldA;
    public static void main(String[] args) throws Exception {
        SerializedLambda serializedLambda = doSFunction(LambdaTest::getFieldA);
        System.out.println("方法名:" + serializedLambda.getImplMethodName());
        System.out.println("类名:" + serializedLambda.getImplClass());
        System.out.println("serializedLambda:" + JSONUtil.toJsonStr(serializedLambda));
    }
    private static <T, R> java.lang.invoke.SerializedLambda doSFunction(SFunction<T, R> func) throws Exception {
        // 直接调用writeReplace
        Method writeReplace = func.getClass().getDeclaredMethod("writeReplace");
        writeReplace.setAccessible(true);
      	//反射调用
        Object sl = writeReplace.invoke(func);
        java.lang.invoke.SerializedLambda serializedLambda = (java.lang.invoke.SerializedLambda) sl;
        return serializedLambda;
    }
}

输出结果: 可以看到获取到了方法名和类名。 知道方法名再去掉get/set前缀就是字段名称了

方法名:getFieldA
类名:xyz/xiezc/ioc/starter/orm/lambda/LambdaTest
serializedLambda:{"implMethodName":"getFieldA","implClass":"xyz/xiezc/ioc/starter/orm/lambda/LambdaTest","functionalInterfaceClass":"xyz/xiezc/ioc/starter/orm/lambda/SFunction","capturingClass":"xyz/xiezc/ioc/starter/orm/lambda/LambdaTest","instantiatedMethodType":"(Lxyz/xiezc/ioc/starter/orm/lambda/LambdaTest;)Ljava/lang/String;","functionalInterfaceMethodSignature":"(Ljava/lang/Object;)Ljava/lang/Object;","implMethodSignature":"()Ljava/lang/String;","functionalInterfaceMethodName":"apply","implMethodKind":5}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java中@NotBlank限制属性不能为空

    java中@NotBlank限制属性不能为空

    在实体类的对应属性上添 @NotBlank注解,可以实现对空置的限制,本文就来介绍一下java中@NotBlank限制属性不能为空,感兴趣的可以了解一下
    2024-01-01
  • maven插件maven-assembly-plugin打包归纳文件zip/tar使用

    maven插件maven-assembly-plugin打包归纳文件zip/tar使用

    java项目运行的文件需要jar或者war格式,同时还需要使用Java命令,本文主要介绍了maven插件maven-assembly-plugin打包归纳文件zip/tar使用,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • java volatile关键字的含义详细介绍

    java volatile关键字的含义详细介绍

    这篇文章主要介绍了java volatile关键字的含义详解的相关资料,需要的朋友可以参考下
    2016-12-12
  • IDEA创建Maven工程Servlet的详细教程

    IDEA创建Maven工程Servlet的详细教程

    这篇文章主要介绍了IDEA创建Maven工程Servlet的详细教程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10
  • Spring Boot实现图片上传/加水印一把梭操作实例代码

    Spring Boot实现图片上传/加水印一把梭操作实例代码

    这篇文章主要给大家介绍了关于Spring Boot实现图片上传/加水印一把梭操作的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-11-11
  • 如何用Intellij idea2020打包jar的方法步骤

    如何用Intellij idea2020打包jar的方法步骤

    这篇文章主要介绍了如何用Intellij idea 2020打包jar的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04
  • Java中如何利用Set判断List集合中是否有重复元素

    Java中如何利用Set判断List集合中是否有重复元素

    在开发工作中,我们有时需要去判断List集合中是否含有重复的元素,这时候我们不需要找出重复的元素,我们只需要返回一个 Boolean 类型就可以了,下面通过本文给大家介绍Java中利用Set判断List集合中是否有重复元素,需要的朋友可以参考下
    2023-05-05
  • 线程池之newFixedThreadPool定长线程池的实例

    线程池之newFixedThreadPool定长线程池的实例

    这篇文章主要介绍了线程池之newFixedThreadPool定长线程池的实例,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • 通过实例解析Spring Ioc项目实现过程

    通过实例解析Spring Ioc项目实现过程

    这篇文章主要介绍了Spring Ioc项目实践过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • Java中构造、生成XML简明教程

    Java中构造、生成XML简明教程

    这篇文章主要介绍了Java中构造、生成XML简明教程,本文通过dom4j包来完成,需要的朋友可以参考下
    2014-08-08

最新评论