MybatisPlus中凭什么可以使用Lambda来构建语句?

1,305 阅读14分钟

mybatisplus中凭什么可以使用Lambda来构建语句?

本文主要讲解两个内容

  1. mybatisplus中Lambda构建语句的时候是怎么通过方法引用得到相关表字段的。
  2. java序列化中的一些内置方法(扩展知道的可以略过)
  3. 通过方法引用得到其方法名称
  4. 源码中是如何实现的的

问题引入

熟悉mybatisplus的小伙伴应该清楚 可以使用Lambda来构建查询、更新语句,举个栗子

    @Data
    @TableName("user") //表名
    class User{
        private String name;
        private String password;
        private Integer age;
        private String email;
    }
​
new LambdaQueryWrapper<User>().eq(User::getAge, 1);

上述代码的意义是 构建查询user表中 age=1的用户,这里有个问题是我们传入的是一个方法引用为什么可以得到 'age'这个字段。

如果我们传的是一个class对象那我们可以通过下面的代码来得到具体的字段名。

Method getAge = user.getClass().getDeclaredMethod("getAge");
getAge.getName().substring(3).toLowerCase()

那如果是一个方法引用该如何获取到方法的名称呢?

可以通过 SerializedLambda 去获取函数的名称 可以看到在成员变量中有一个functionalInterfaceMethodName,他就保存了方法引用的函数名,通过注释可以了解到可以通过 writeReplace方法得到。那么writeReplace又是什么呢?

/**
Lambda 表达式的序列化形式。此类的属性表示 Lambda 工厂站点中存在的信息,包括静态元工厂参数(例如主功能接口方法的标识和实现方法的标识)以及动态元工厂参数(例如在捕获 Lambda 时从词法范围捕获的值)。
SerializedLambda 的实现者(例如编译器或语言运行时库)应确保实例正确反序列化。实现此目的的一种方法是确保writeReplace方法返回SerializedLambda的实例,而不是允许默认序列化继续进行。
SerializedLambda有一个readResolve方法,该方法在捕获类中查找名为$deserializeLambda$(SerializedLambda)的(可能是私有的)静态方法,将其自身作为第一个参数来调用,并返回结果。实现$deserializeLambda$的 Lambda 类负责验证SerializedLambda的属性是否与该类实际捕获的 lambda 一致。
通过反序列化序列化形式生成的函数对象的身份是不可预测的,因此身份敏感的操作(例如引用相等,对象锁定和System.identityHashCode()可能会在不同的实现中产生不同的结果,甚至在同一实现中的不同反序列化时也会产生不同的结果。
*/
public final class SerializedLambda implements Serializable {
    @java.io.Serial
    private static final long serialVersionUID = 8025925345765570181L;
    /**
     * The capturing class.
     */
    private final Class<?> capturingClass;
    /**
     * The functional interface class.
     */
    private final String functionalInterfaceClass;
    /**
     * The functional interface method name.
     */
    private final String functionalInterfaceMethodName;
    /**
     * The functional interface method signature.
     */
    private final String functionalInterfaceMethodSignature;
    /**
     * The implementation class.
     */
    private final String implClass;
    /**
     * The implementation method name.
     */
    private final String implMethodName;
    /**
     * The implementation method signature.
     */
    private final String implMethodSignature;
    /**
     * The implementation method kind.
     */
    private final int implMethodKind;
    /**
     * The instantiated method type.
     */
    private final String instantiatedMethodType;
    /**
     * The captured arguments.
     */
    @SuppressWarnings("serial") // Not statically typed as Serializable
    private final Object[] capturedArgs;
​

上面的注释是我直接翻译源码中的注释,可能会不通顺。但是我们可以翻译后的注释和类变量得到一些有用的信息

  1. SerializedLambda 是用来保存Lambda表达式的序列化形式会去保存具体的实现方法名称(implMethodName字段)
  2. 要实现Serializable接口,因为必须实现Serializable接口,jvm才会调用writeReplace。
  3. writeReplace的实现是由编译器或语言运行时库来完成的,也就是说只要是Lambda并且是继承了Serializable接口就可以调用对象的writeReplace方法返回一个SerializedLambda 对象。

java序列化

大家知道java对象要进行网络传输或者本地储存就必须要实现Serializable接口,Serializable接口没有方法或字段,仅用于标识可序列化的语义。但是在jvm内部会调用几个特殊的方法来完成java对象的序列化,打开Serializable的源码在注释上会有这样一段话

/**
 * Serializability of a class is enabled by the class implementing the
 * java.io.Serializable interface. Classes that do not implement this
 * interface will not have any of their state serialized or
 * deserialized.  All subtypes of a serializable class are themselves
 * serializable.  The serialization interface has no methods or fields
 * and serves only to identify the semantics of being serializable. <p>
 *
 * To allow subtypes of non-serializable classes to be serialized, the
 * subtype may assume responsibility for saving and restoring the
 * state of the supertype's public, protected, and (if accessible)
 * package fields.  The subtype may assume this responsibility only if
 * the class it extends has an accessible no-arg constructor to
 * initialize the class's state.  It is an error to declare a class
 * Serializable if this is not the case.  The error will be detected at
 * runtime. <p>
 *
 * During deserialization, the fields of non-serializable classes will
 * be initialized using the public or protected no-arg constructor of
 * the class.  A no-arg constructor must be accessible to the subclass
 * that is serializable.  The fields of serializable subclasses will
 * be restored from the stream. <p>
 *
 * When traversing a graph, an object may be encountered that does not
 * support the Serializable interface. In this case the
 * NotSerializableException will be thrown and will identify the class
 * of the non-serializable object. <p>
 *
 * Classes that require special handling during the serialization and
 * deserialization process must implement special methods with these exact
 * signatures:
 *
 * <PRE>
 * private void writeObject(java.io.ObjectOutputStream out)
 *     throws IOException
 * private void readObject(java.io.ObjectInputStream in)
 *     throws IOException, ClassNotFoundException;
 * private void readObjectNoData()
 *     throws ObjectStreamException;
 * </PRE>
 *
 * <p>The writeObject method is responsible for writing the state of the
 * object for its particular class so that the corresponding
 * readObject method can restore it.  The default mechanism for saving
 * the Object's fields can be invoked by calling
 * out.defaultWriteObject. The method does not need to concern
 * itself with the state belonging to its superclasses or subclasses.
 * State is saved by writing the individual fields to the
 * ObjectOutputStream using the writeObject method or by using the
 * methods for primitive data types supported by DataOutput.
 *
 * <p>The readObject method is responsible for reading from the stream and
 * restoring the classes fields. It may call in.defaultReadObject to invoke
 * the default mechanism for restoring the object's non-static and
 * non-transient fields.  The defaultReadObject method uses information in
 * the stream to assign the fields of the object saved in the stream with the
 * correspondingly named fields in the current object.  This handles the case
 * when the class has evolved to add new fields. The method does not need to
 * concern itself with the state belonging to its superclasses or subclasses.
 * State is saved by writing the individual fields to the
 * ObjectOutputStream using the writeObject method or by using the
 * methods for primitive data types supported by DataOutput.
 *
 * <p>The readObjectNoData method is responsible for initializing the state of
 * the object for its particular class in the event that the serialization
 * stream does not list the given class as a superclass of the object being
 * deserialized.  This may occur in cases where the receiving party uses a
 * different version of the deserialized instance's class than the sending
 * party, and the receiver's version extends classes that are not extended by
 * the sender's version.  This may also occur if the serialization stream has
 * been tampered; hence, readObjectNoData is useful for initializing
 * deserialized objects properly despite a "hostile" or incomplete source
 * stream.
 *
 * <p>Serializable classes that need to designate an alternative object to be
 * used when writing an object to the stream should implement this
 * special method with the exact signature:
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
 * </PRE><p>
 *
 * This writeReplace method is invoked by serialization if the method
 * exists and it would be accessible from a method defined within the
 * class of the object being serialized. Thus, the method can have private,
 * protected and package-private access. Subclass access to this method
 * follows java accessibility rules. <p>
 *
 * Classes that need to designate a replacement when an instance of it
 * is read from the stream should implement this special method with the
 * exact signature.
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 * </PRE><p>
 *
 * This readResolve method follows the same invocation rules and
 * accessibility rules as writeReplace.<p>
 *
 * The serialization runtime associates with each serializable class a version
 * number, called a serialVersionUID, which is used during deserialization to
 * verify that the sender and receiver of a serialized object have loaded
 * classes for that object that are compatible with respect to serialization.
 * If the receiver has loaded a class for the object that has a different
 * serialVersionUID than that of the corresponding sender's class, then
 * deserialization will result in an {@link InvalidClassException}.  A
 * serializable class can declare its own serialVersionUID explicitly by
 * declaring a field named <code>"serialVersionUID"</code> that must be static,
 * final, and of type <code>long</code>:
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
 * </PRE>
 *
 * If a serializable class does not explicitly declare a serialVersionUID, then
 * the serialization runtime will calculate a default serialVersionUID value
 * for that class based on various aspects of the class, as described in the
 * Java(TM) Object Serialization Specification.  However, it is <em>strongly
 * recommended</em> that all serializable classes explicitly declare
 * serialVersionUID values, since the default serialVersionUID computation is
 * highly sensitive to class details that may vary depending on compiler
 * implementations, and can thus result in unexpected
 * <code>InvalidClassException</code>s during deserialization.  Therefore, to
 * guarantee a consistent serialVersionUID value across different java compiler
 * implementations, a serializable class must declare an explicit
 * serialVersionUID value.  It is also strongly advised that explicit
 * serialVersionUID declarations use the <code>private</code> modifier where
 * possible, since such declarations apply only to the immediately declaring
 * class--serialVersionUID fields are not useful as inherited members. Array
 * classes cannot declare an explicit serialVersionUID, so they always have
 * the default computed value, but the requirement for matching
 * serialVersionUID values is waived for array classes.
 */
public interface Serializable {

由于本人英语巨菜借助翻译软件的帮助下得到如下的信息

类的可序列化性由实现 java.io.Serializable 接口的类启用。未实现此接口的类将不会序列化或反序列化其任何状态。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
为了允许序列化不可序列化类的子类型,子类型可以承担保存和恢复超类型的公共、受保护和(如果可访问)包字段状态的责任。只有当子类型扩展的类具有可访问的无参数构造函数来初始化类的状态时,子类型才可以承担此责任。如果不是这种情况,则将类声明为可序列化是错误的。将在运行时检测到错误。
在反序列化过程中,将使用该类的公共或受保护的无参数构造函数初始化不可序列化类的字段。无参数构造函数必须可供可序列化的子类访问。可序列化子类的字段将从流中恢复。
遍历图时,可能会遇到不支持 Serializable 接口的对象。在这种情况下,将抛出 NotSerializableException 并标识不可序列化对象的类。
在序列化和反序列化过程中需要特殊处理的类必须实现具有以下精确签名的特殊方法:
  private void writeObject(java.io.ObjectOutputStream out)
      throws IOException
  private void readObject(java.io.ObjectInputStream in)
      throws IOException, ClassNotFoundException;
  private void readObjectNoData()
      throws ObjectStreamException;
  
writeObject 方法负责为特定类写入对象的状态,以便相应的 readObject 方法可以恢复它。可以通过调用 out.defaultWriteObject 来调用用于保存对象字段的默认机制。该方法无需关心属于其超类或子类的状态。通过使用 writeObject 方法将各个字段写入 ObjectOutputStream 或使用 DataOutput 支持的原始数据类型的方法来保存状态。
readObject 方法负责从流中读取并恢复类字段。它可以调用 in.defaultReadObject 来调用用于恢复对象的非静态和非瞬态字段的默认机制。defaultReadObject 方法使用流中的信息将保存在流中的对象的字段分配给当前对象中相应命名的字段。这处理类已发展为添加新字段的情况。该方法不需要关注属于其超类或子类的状态。通过使用 writeObject 方法将各个字段写入 ObjectOutputStream 或使用 DataOutput 支持的原始数据类型的方法来保存状态。
如果序列化流未将给定类列为被反序列化对象的超类,readObjectNoData 方法负责初始化特定类的对象状态。如果接收方使用的反序列化实例类的版本与发送方不同,并且接收方的版本扩展了发送方的版本未扩展的类,则可能会发生这种情况。如果序列化流已被篡改,也可能会发生这种情况;因此,尽管源流“不友好”或不完整,readObjectNoData 仍可用于正确初始化反序列化对象。
在将对象写入流时需要指定要使用的替代对象的可序列化类应该使用精确签名来实现此特殊方法:
  ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
  
如果此方法存在,并且可以通过序列化对象的类中定义的方法访问,则序列化将调用此 writeReplace 方法。因此,该方法可以具有私有、受保护和包私有访问权限。子类对此方法的访问遵循 Java 可访问性规则。
当从流中读取实例时需要指定替换的类应该使用精确的签名来实现这个特殊方法。
  ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
  
此 readResolve 方法遵循与 writeReplace 相同的调用规则和可访问性规则。
序列化运行时将一个版本号(称为 serialVersionUID)与每个可序列化类关联起来,该版本号在反序列化期间用于验证序列化对象的发送者和接收者是否已为该对象加载了与序列化兼容的类。如果接收者为该对象加载的类的 serialVersionUID 与相应发送者的类的 serialVersionUID 不同,则反序列化将导致InvalidClassException 。可序列化类可以通过声明名为"serialVersionUID"的字段来显式声明自己的 serialVersionUID,该字段必须是静态、最终的且类型为long :
  ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
  
如果可序列化类未明确声明 serialVersionUID,则序列化运行时将根据类的各个方面计算该类的默认 serialVersionUID 值,如 Java(TM) 对象序列化规范中所述。但是,强烈建议所有可序列化类都明确声明 serialVersionUID 值,因为默认 serialVersionUID 计算对类细节高度敏感,这些细节可能因编译器实现而异,因此在反序列化期间可能导致意外的InvalidClassException 。因此,为了保证在不同的 Java 编译器实现中 serialVersionUID 值一致,可序列化类必须声明显式 serialVersionUID 值。还强烈建议显式 serialVersionUID 声明尽可能使用private修饰符,因为此类声明仅适用于直接声明的类 - serialVersionUID 字段作为继承成员无用。数组类不能声明显式的 serialVersionUID,因此它们始终具有默认的计算值,但对于数组类,无需匹配 serialVersionUID 值。

也就是说如果我们想要自定义序列化过程就可以实现 writeObject readObject writeReplace readResolve这几个方法。这些方法不在接口中定义而是在java内部完成调用的。我们需要严格按照方法格式实现才能被jvm识别

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
​
    private static final long serialVersionUID = 1L;
​
    private String name;
​
    private String password;
​
    private Integer age;
​
    private void writeObject(java.io.ObjectOutputStream out) throws IOException{
        System.out.println("writeObject执行了");
        out.defaultWriteObject();
    }
​
    Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace执行了");
        //这里就可以对源对象执行一些操作 比如对字段的修改、或者返回类型。如果修改了返回类型就需要在readResolve()方法中读取相应的类型
        this.password="****";
        return this;
    }
​
    Object readResolve() throws ObjectStreamException{
        System.out.println("readResolve执行了");
        return this;
    }
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException{
        System.out.println("readObject执行了");
        in.defaultReadObject();
    }
}

然后我们对User对象序列化

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        user.setPassword("123456");
        Path path = Paths.get("user.txt");
        try(ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(path))){
            objectOutputStream.writeObject(user);
            objectOutputStream.flush();
        }catch (Exception e){
            throw new RuntimeException(e);
        }
        try (ObjectInputStream inputStream = new ObjectInputStream(Files.newInputStream(path))) {
            User user1 = (User) inputStream.readObject();
            System.out.println(user1);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
}

得到结果

writeReplace执行了
writeObject执行了
readObject执行了
readResolve执行了
User(name=张三, password=****, age=18)

可以发现这四个方法的先后执行顺序。

函数式接口如何序列化?

我们可以创建一个函数式接口继承Serializable接口这样就可以使用内置的writeReplace()方法来得到SerializedLambda类从而获取具体的方法名在根据方法名得到对应的字段名。

@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    SFunction<User, Integer> function = User::getAge;
    Method method =function.getClass().getDeclaredMethod("writeReplace");
    method.setAccessible(true);
    SerializedLambda object =(SerializedLambda) method.invoke(function);
    System.out.println(object);
    System.out.println(object.getImplMethodName());

输出的结果为

SerializedLambda[capturingClass=class com.sgp.serialization.Main, functionalInterfaceMethod=com/sgp/serialization/SFunction.apply:(Ljava/lang/Object;)Ljava/lang/Object;, implementation=invokeVirtual com/sgp/serialization/User.getAge:()Ljava/lang/Integer;, instantiatedMethodType=(Lcom/sgp/serialization/User;)Ljava/lang/Integer;, numCaptured=0]
getAge

这样我们就的到方法引用的函数名

mybatisplus中是如何实现的

LambdaQueryWrapper类:

    @Override
    public LambdaQueryWrapper<T> select(boolean condition, List<SFunction<T, ?>> columns) {
        return doSelect(condition, columns);
    }
​
    protected LambdaQueryWrapper<T> doSelect(boolean condition, List<SFunction<T, ?>> columns) {
        if (condition && CollectionUtils.isNotEmpty(columns)) {
            this.sqlSelect.setStringValue(columnsToString(false, columns));
        }
        return typedThis;
    }

在其父类AbstractLambdaWrapper中

protected String columnToString(SFunction<T, ?> *column*, boolean *onlyColumn*) {
    ColumnCache *cache* = getColumnCache(*column*);
    return *onlyColumn* ? *cache*.getColumn() : *cache*.getColumnSelect();
}
​
/**
     * 获取 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);
    }
    private ColumnCache getColumnCache(String fieldName, Class<?> lambdaClass) {
        //通过字段名获取字段对应的数据库中的列名
        ColumnCache columnCache = columnMap.get(LambdaUtils.formatKey(fieldName));
        Assert.notNull(columnCache, "can not find lambda cache for this property [%s] of entity [%s]",
            fieldName, lambdaClass.getName());
        return columnCache;
    }

LambdaUtils中

/**
     * 该缓存可能会在任意不定的时间被清除
     *
     * @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));
        }
    }

可以看到最终被封装成一个ReflectLambdaMeta类在通过private ColumnCache getColumnCache(String fieldName, Class<?> lambdaClass) 方法就可以的到列名。