方法引用的序列化小秘密被我发现了...

背景

在使用MPLambdaQueryWrapper时,可以直接通过方法引用来指定要匹配数据库字段,比如以下的使用:

// 代码
Wrappers.<User>lambdaQuery()
    .eq(User::UserName, "小明");

// 会被MP转换为 如下SQL
SELECT * FROM user WHERE user_name = "小明"
通过方法引用转换为数据库字段,MP是如何实现的呢?

MP转换的实现原理

  • 1、反射获取到方法引用对应属性

    • 1.1、通过方法引用获取方法名(重点在这里)

    • 1.2、根据方法名获取属性名

    • 1.3、根据属性名反射获取到属性对象(Field对象)

  • 2、反射读取该属性上的@TableField注解

  • 3、获取目标数据库字段的字符串

    • 3.1、属性上存在@TabelField注解,则获取其value()属性的字符串值作为目标数据库字段

    • 4.2、属性上不存在@TableField注解,则反射获取该属性的属性名,再根据驼峰命名法的规则对属性名进行解析,最终得出对应的字符串值作为目标数据库字段

以上的步骤中,1.1是重点,该如何通过方法引用获取方法名呢?让我来带你揭秘!

通过方法引用获取方法名称原理

当我们要对一个Java类的对象进行序列化时,我们序列化的就是这个对象的属性值,那么当我们要对一个lambda表达式创建的对象进行序列化时,需要序列化的是什么呢?答案就是lambda表达式对应方法内的代码,因为每个lambda表达式中实现代码是动态的。

PS:

  • 1、当然如果lambda表达式中传入了外部变量,那也会序列化这个变量

  • 2、针对lambda表达式中方法代码的序列化并不是真的把每行代码都序列化,【有多种方式,其中一种方式就是把方法中的代码抽取成一个静态方法,然后只需要序列化这个方法的全限定名即可。】这块知识点的内容可以看R大的回答,看完会让你醍醐灌顶Java中普通lambda表达式和方法引用本质上有什么区别? - 知乎 (zhihu.com)

Java中针对lambda表达式的对象的序列化(前提是函数式接口实现了Serialize接口),在运行时生成的类中提供了writeReplace(),该方法返回了SerializedLambda类,这个类中的属性都是lambda表达式需要序列的信息:

public final class SerializedLambda implements Serializable {
    private static final long serialVersionUID = 8025925345765570181L;
    private final Class<?> capturingClass;
    private final String functionalInterfaceClass;
    private final String functionalInterfaceMethodName;
    private final String functionalInterfaceMethodSignature;
    private final String implClass;	// lambda表达式中动态代码所在的类
    private final String implMethodName;	// implClass中的方法,再结合其他几个属性即可定位到lambda表达式中动态代码所在的方法
    private final String implMethodSignature;
    private final int implMethodKind;
    private final String instantiatedMethodType;
    private final Object[] capturedArgs;
	// ...
}

什么?你问我为什么Lambda表达式需要通过writeReplace()方法来进行序列化,采用默认的序列化方式不行么?

答:不行!!!默认的序列化方式只会序列化属性,并不会序列化方法,而lambda表达式还需要序列化自定义实现的方法里的代码逻辑

其中一种实现方式就是:Java将lambda表达式中实现的代码逻辑存放在lambda表达式所在类的一个静态方法中,然后在运行时生成的lambda表达式对应的类中调用这个静态方法,而JVM为lambda表达式生成的writeReplace()的返回值中保存了所在类的静态方法的全限定名,从而实现lambda表达式的代码逻辑的序列化。

  • PS:writeReplace()是用于覆盖默认的序列化方式的,当支持序列化的类中存在writeReplace()时,则JVM会采用writeReplace()来覆盖序列化方式,详细内容见`Serializable`接口的注解。

c3740f51b19040a5b2e605e86d171f1f.png

动手实现

光说不练假把式,这不,我们直接来上手实践!让我们来试着自己动手实现一个根据方法引用获取对应数据库字段的例子。下面是所需的几个类:

/**
 * 支持序列化的自定义函数式接口
 * @param <T>
 * @param <R>
 */
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}
/**
 * 指定数据库字段名称
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableField {

    /**
     * 数据库字段名称
     * @return
     */
    String value();
}
public class User {

    @TableField("USER_NAME")
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}
/**
 * 测试类
 */
public class Test {

    public static void main(String[] args) {

        // 1、根据get方法的方法引用,获取对应的属性对象
        Field field = getField(User::getUserName);

        // 2、尝试获取指定的数据库字段
        TableField tableField = field.getDeclaredAnnotation(TableField.class);
        if (tableField != null) {
            System.out.println("根据方法引用获取到的数据库字段为:" + tableField.value());
        }
    }

    /**
     * 通过get方法的方法引用,获取对应的属性
     * @param fn
     * @param <T>
     * @param <R>
     * @return
     */
    private static <T, R> Field getField(SFunction<T, R> fn) {

        // 1、获取writeReplace方法对象
        Method writeReplace;
        try {
            writeReplace = fn.getClass().getDeclaredMethod("writeReplace");
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }

        // 2、执行writeReplace(),获取SerializedLambda对象
        writeReplace.setAccessible(true);
        SerializedLambda serializedLambda;
        try {
            serializedLambda = (SerializedLambda) writeReplace.invoke(fn);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }

        // 3、获取方法引用的方法名
        String implMethodName = serializedLambda.getImplMethodName();
        if (!implMethodName.startsWith("get")) {
            throw new RuntimeException("方法名格式错误,并不以'get'开始");
        }

        // 4、处理方法名,获取属性名
        String fieldName = implMethodName.substring(3);
        String firstChar  = fieldName.substring(0, 1);
        fieldName = fieldName.replaceFirst(firstChar , firstChar.toLowerCase());

        // 5、根据属性名,获取属性对象
        Field field;
        try {
            field = Class.forName(serializedLambda.getImplClass().replace("/", ".")).getDeclaredField(fieldName);
        } catch (ClassNotFoundException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

        return field;
    }
}

结果

根据方法引用获取到的数据库字段为:USER_NAME

总结

1、当函数式接口实现了Serializable接口时,JVM就会为了实现了该接口的lambda表达式在运行时生成一个方法writeReplace()。这个方法会返回一个SerializedLambda对象,而该对象中就包含了lambda表达式需要序列化的全部信息。

  • 尤其是implMethodName属性,可以让我们获取到方法引用的方法名称,进而实现像MP一样通过方法引用转换为数据库字段的功能。

2、针对lambda表达式中方法代码的序列化并不是真的把每行代码都序列化,【有多种方式,其中一种方式就是把方法中的代码抽取成一个静态方法,然后只需要序列化这个方法的全限定名即可。】

参考

Java中普通lambda表达式和方法引用本质上有什么区别? - 知乎 (zhihu.com)

java骚操作之通过lambda表达式获取get方法引用的属性 - 简书 (jianshu.com)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值