The premise

In my spare time after work, I want to write a set of lightweight ORM framework that abandons reflection call based on JDBC with Javassist as the core. In the process, I read the source code of Mybatis, TK-Mapper, Mybatis – Plus and Spring-boot-starter-JDBC. The LambdaQueryWrapper found in Mybatis – Plus can get method information (actually CallSite information) from the currently called Lambda expression. Here is a complete record. This article is based on JDK11, other versions of the JDK may not be appropriate.

The magic of Lambda expression serialization

LambdaMetafactory = LambdaMetafactory = LambdaMetafactory

Serializable features. In general, generated function objects (specifically, function objects based on Lambda expressions) do not need to support serialization. If this feature is required, FLAG_SERIALIZABLE (a static integer property of LambdaMetafactory with a value of 1 << 0) can be used to indicate that a function object is serialized. Once function objects that support serialization are used, they are serialized as SerializedLambda classes. These SerializedLambda instances require the assistance of additional “capture classes” (capture classes, as described in the Caller parameter to MethodHandles.Lookup), See SerializedLambda for more information.

Search for FLAG_SERIALIZABLE in the comment for LambdaMetafactory:

An instance of a function object that sets the FLAG_SERIALIZABLE flag implements the Serializable interface and has a method named writeReplace that returns a value of type SerializedLambda. The caller to the method that calls these function objects (the “capture class” mentioned earlier) must have a method named $deserializeLambda$, as described by the SerializedLambda class.

Finally, looking at the description of SerializedLambda, the comment has four large sections, which are posted here and each section extracts core information:

The paragraphs are as follows:

  • Paragraph 1:SerializedLambdaisLambdaSerialized form of an expression, this class is storedLambdaRuntime information about an expression
  • Paragraph 2: To make sureLambdaOne way the compiler or language library can ensure that the expression’s serialization implementation is correctwriteReplaceMethod returns aSerializedLambdaThe instance
  • Paragraph 3:SerializedLambdaTo provide areadResolveMethod, which functions like calling static methods in the “capture class”$deserializeLambda$(SerializedLambda)And with its own instance as an input parameter, the process is understood as a deserialization process
  • Paragraph 4: Identification of identity-sensitive operations on function objects resulting from serialization and deserialization (e.gSystem.identityHashCode(), object locking, etc.) is unpredictable

The final conclusion is: If a functional interface implements the Serializable interface, then its instance automatically generates a writeReplace method that returns the SerializedLambda instance, from which runtime information about the functional interface can be obtained. These runtime information are the properties of SerializedLambda:

attribute meaning
capturingClass “Capture class”, currentLambdaThe class in which the expression appears
functionalInterfaceClass Name, separated by a “/”, returnedLambdaThe static type of an object
functionalInterfaceMethodName Name of a functional interface method
functionalInterfaceMethodSignature Functional interface method signature (actually parameter type and return value type, or erased type if generics are used)
implClass Name, separated by “/”, holding the type of the implementation method of the functional interface method (the implementation class that implements the functional interface method)
implMethodName The implementation method name of a functional interface method
implMethodSignature Implementation of functional interface methods Method signatures (real parameter types and return value types)
instantiatedMethodType A functional interface type replaced by an instance type variable
capturedArgs LambdaCaptured dynamic parameters
implMethodKind Implementation methodologicalMethodHandletype

As a practical example, define a functional interface that implements Serializable and call it:

public class App {

    @FunctionalInterface
    public interface CustomerFunction<S.T> extends Serializable {

        T convert(S source);
    }

    public static void main(String[] args) throws Exception {
        CustomerFunction<String, Long> function = Long::parseLong;
        Long result = function.convert("123");
        System.out.println(result);
        Method method = function.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(true); SerializedLambda serializedLambda = (SerializedLambda)method.invoke(function); System.out.println(serializedLambda.getCapturingClass()); }}Copy the code

The following debugging information is displayed:

This allows you to get the run-time information at the call point of the functional interface instance when calling the method, or even the type before the generic parameter is erased, and a lot of tricks can be derived. Such as:

public class ConditionApp {

    @FunctionalInterface
    public interface CustomerFunction<S.T> extends Serializable {

        T convert(S source);
    }

    @Data
    public static class User {

        private String name;
        private String site;
    }

    public static void main(String[] args) throws Exception {
        Condition c1 = addCondition(User::getName, "="."throwable");
        System.out.println("c1 = " + c1);
        Condition c2 = addCondition(User::getSite, "IN"."('throwx.cn','vlts.cn')");
        System.out.println("c1 = " + c2);
    }

    private static <S> Condition addCondition(CustomerFunction
       
         function, String operation, Object value)
       ,> throws Exception {
        Condition condition = new Condition();
        Method method = function.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(true);
        SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
        String implMethodName = serializedLambda.getImplMethodName();
        int idx;
        if ((idx = implMethodName.lastIndexOf("get"> =))0) {
            condition.setField(Character.toLowerCase(implMethodName.charAt(idx + 3)) + implMethodName.substring(idx + 4));
        }
        condition.setEntityKlass(Class.forName(serializedLambda.getImplClass().replace("/".".")));
        condition.setOperation(operation);
        condition.setValue(value);
        return condition;
    }

    @Data
    private static class Condition {

        privateClass<? > entityKlass;private String field;
        private String operation;
        privateObject value; }}// Execution result
c1 = ConditionApp.Condition(entityKlass=class club.throwable.lambda.ConditionApp$User.field=name, operation==, value=throwable)
c1 = ConditionApp.Condition(entityKlass=class club.throwable.lambda.ConditionApp$User.field=site, operation=IN, value=('throwx.cn'.'vlts.cn'))
Copy the code

A lot of people worry about the performance of reflection calls, but in older JDK versions, reflection performance has been greatly optimized to approximate the performance of direct calls, not to mention that some scenarios are small reflection calls and can be used safely.

SerializedLambda: SerializedLambda: SerializedLambda: SerializedLambda: SerializedLambda

public class SerializedLambdaApp {

    @FunctionalInterface
    public interface CustomRunnable extends Serializable {

        void run(a);
    }

    public static void main(String[] args) throws Exception {
        invoke(() -> {
        });
    }

    private static void invoke(CustomRunnable customRunnable) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(customRunnable);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(newByteArrayInputStream(baos.toByteArray())); Object target = ois.readObject(); System.out.println(target); }}Copy the code

The results are shown below:

Lambda expression serialization principle

ObjectStreamClass ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream ObjectInputStream

  • Prerequisites: The object to be serialized must be implementedSerializableinterface
  • Exists in the object to be serializedwriteReplaceMethod, the return value type from calling this method directly based on the incoming instance reflection as the target type of serialization, forLambdaThe expression isSerializedLambdatype
  • The deserialization process happens to be the reverse process and calls the methodreadResolve, just mentioned earlierSerializedLambdaThere are also private methods with the same name
  • LambdaThe implementation type of the expression isVMThe generated template class can be observed from the result that the instance before serialization and the instance after deserialization belong to different template classes. For the example in the previous section, the template class before serialization isclub.throwable.lambda.SerializedLambdaApp$$Lambda$14/0x0000000800065840, the deserialized template class isclub.throwable.lambda.SerializedLambdaApp$$Lambda$26/0x00000008000a4040

ObjectStreamClass is the class descriptor for serialization and deserialization implementations. The class description for object serialization and deserialization can be found in the member properties of the class, such as the writeReplace and readResolve methods mentioned here

The graphical process is as follows:

How to get SerializedLambda

From the previous analysis, there are two ways to get SerializedLambda instances of Lambda expressions:

  • Method 1: Based onLambdaExpression instance sumLambdaThe template class of the expression reflects the callwriteReplaceMethod, the return value isSerializedLambdaThe instance
  • Method 2: Obtain data based on serialization and deserializationSerializedLambdaThe instance

Examples can be written based on these two methods, such as the reflection method as follows:

// Reflection mode
public class ReflectionSolution {

    @FunctionalInterface
    public interface CustomerFunction<S.T> extends Serializable {

        T convert(S source);
    }

    public static void main(String[] args) throws Exception {
        CustomerFunction<String, Long> function = Long::parseLong;
        SerializedLambda serializedLambda = getSerializedLambda(function);
        System.out.println(serializedLambda.getCapturingClass());
    }

    public static SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
        Method writeReplaceMethod = serializable.getClass().getDeclaredMethod("writeReplace");
        writeReplaceMethod.setAccessible(true);
        return(SerializedLambda) writeReplaceMethod.invoke(serializable); }}Copy the code

Serialization and inverted sequence mode will be a little more complicated, because ObjectInputStream. ReadObject () method will eventually callback SerializedLambda. ReadResolve () method, which lead to the result returned is a new template class load instances of Lambda expressions, So we need to find a way to interrupt the call and return the result ahead of time. The solution is to construct a shadow type similar to SerializedLambda but without the readResolve() method:

package cn.vlts;
import java.io.Serializable;

/ * * * note here must and Java lang. Invoke. SerializedLambda, with the same name to different package name, this is to "cheat" judging a magical class name in ObjectStreamClass classNamesEqual * / () method
@SuppressWarnings("ALL")
public class SerializedLambda implements Serializable {
    private static final long serialVersionUID = 8025925345765570181L;
    privateClass<? > capturingClass;private  String functionalInterfaceClass;
    private  String functionalInterfaceMethodName;
    private  String functionalInterfaceMethodSignature;
    private  String implClass;
    private  String implMethodName;
    private  String implMethodSignature;
    private  int implMethodKind;
    private  String instantiatedMethodType;
    private  Object[] capturedArgs;

    public String getCapturingClass(a) {
        return capturingClass.getName().replace('. '.'/');
    }
    public String getFunctionalInterfaceClass(a) {
        return functionalInterfaceClass;
    }
    public String getFunctionalInterfaceMethodName(a) {
        return functionalInterfaceMethodName;
    }
    public String getFunctionalInterfaceMethodSignature(a) {
        return functionalInterfaceMethodSignature;
    }
    public String getImplClass(a) {
        return implClass;
    }
    public String getImplMethodName(a) {
        return implMethodName;
    }
    public String getImplMethodSignature(a) {
        return implMethodSignature;
    }
    public int getImplMethodKind(a) {
        return implMethodKind;
    }
    public final String getInstantiatedMethodType(a) {
        return instantiatedMethodType;
    }
    public int getCapturedArgCount(a) {
        return capturedArgs.length;
    }
    public Object getCapturedArg(int i) {
        returncapturedArgs[i]; }}public class SerializationSolution {

    @FunctionalInterface
    public interface CustomerFunction<S.T> extends Serializable {

        T convert(S source);
    }

    public static void main(String[] args) throws Exception {
        CustomerFunction<String, Long> function = Long::parseLong;
        cn.vlts.SerializedLambda serializedLambda = getSerializedLambda(function);
        System.out.println(serializedLambda.getCapturingClass());
    }

    private static cn.vlts.SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(serializable);
            oos.flush();
            try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())) {
                @Override
                protectedClass<? > resolveClass(ObjectStreamClass desc)throwsIOException, ClassNotFoundException { Class<? > klass =super.resolveClass(desc);
                    returnklass == java.lang.invoke.SerializedLambda.class ? cn.vlts.SerializedLambda.class : klass; {}})return(cn.vlts.SerializedLambda) ois.readObject(); }}}}Copy the code

The forgotten$deserializeLambda$methods

Mentioned above, Lambda expressions instance deserialization invokes the Java. When lang. Invoke. SerializedLambda. ReadResolve () method, and it’s been amazing source, this method is as follows:

private Object readResolve(a) throws ReflectiveOperationException {
    try {
        Method deserialize = AccessController.doPrivileged(new PrivilegedExceptionAction<>() {
            @Override
            public Method run(a) throws Exception {
                Method m = capturingClass.getDeclaredMethod("$deserializeLambda$", SerializedLambda.class);
                m.setAccessible(true);
                returnm; }});return deserialize.invoke(null.this);
    }
    catch (PrivilegedActionException e) {
        Exception cause = e.getException();
        if (cause instanceof ReflectiveOperationException)
            throw (ReflectiveOperationException) cause;
        else if (cause instanceof RuntimeException)
            throw (RuntimeException) cause;
        else
            throw new RuntimeException("Exception in SerializedLambda.readResolve", e); }}Copy the code

It looks like there is a static method in the “capture class” :

class CapturingClass {

    private static Object $deserializeLambda$(SerializedLambda serializedLambda){
        return[serializedLambda] => instances of Lambda expressions; }}Copy the code

Try retrieving the list of methods in the “capture class” :

public class CapturingClassApp {

    @FunctionalInterface
    public interface CustomRunnable extends Serializable {

        void run(a);
    }

    public static void main(String[] args) throws Exception {
        invoke(() -> {
        });
    }

    private static void invoke(CustomRunnable customRunnable) throws Exception {
        Method writeReplaceMethod = customRunnable.getClass().getDeclaredMethod("writeReplace");
        writeReplaceMethod.setAccessible(true); java.lang.invoke.SerializedLambda serializedLambda = (java.lang.invoke.SerializedLambda) writeReplaceMethod.invoke(customRunnable); Class<? > capturingClass = Class.forName(serializedLambda.getCapturingClass().replace("/"."."));
        ReflectionUtils.doWithMethods(capturingClass, method -> {
                    System.out.printf("Method name :%s, modifier :%s, method argument list :%s, method return value type :%s\n", method.getName(),
                            Modifier.toString(method.getModifiers()),
                            Arrays.toString(method.getParameterTypes()),
                            method.getReturnType().getName());
                },
                method -> Objects.equals(method.getName(), "$deserializeLambda$")); }}// Execution result$deserializeLambda$private static, method parameter list :[class java.lang.invoke.SerializedLambda], method return type:java.lang.Object
Copy the code

If there is a and the aforementioned Java lang. Invoke. SerializedLambda annotations to describe consistent “capture class” SerializedLambda instance into a Lambda expression method, because the search many places are not the discovery of traces of this method, Guessing $deserializeLambda$that methods are generated by the VM and can only be called by reflected methods is a bit of a hidden trick.

summary

Lambda expressions in the JDK have been available for many years, and it is surprising that we have only figured out how to serialize and deserialize them after so many years. Although this is not a complicated issue, it is one of the more interesting knowledge points we have seen recently.

References:

  • JDK11The source code
  • Mybatis-PlusRelated to the source code

(E-A-20211127 C-2-D)