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:
SerializedLambda
isLambda
Serialized form of an expression, this class is storedLambda
Runtime information about an expression - Paragraph 2: To make sure
Lambda
One way the compiler or language library can ensure that the expression’s serialization implementation is correctwriteReplace
Method returns aSerializedLambda
The instance - Paragraph 3:
SerializedLambda
To provide areadResolve
Method, 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.g
System.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”, currentLambda The class in which the expression appears |
functionalInterfaceClass |
Name, separated by a “/”, returnedLambda The 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 |
Lambda Captured dynamic parameters |
implMethodKind |
Implementation methodologicalMethodHandle type |
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 implemented
Serializable
interface - Exists in the object to be serialized
writeReplace
Method, the return value type from calling this method directly based on the incoming instance reflection as the target type of serialization, forLambda
The expression isSerializedLambda
type - The deserialization process happens to be the reverse process and calls the method
readResolve
, just mentioned earlierSerializedLambda
There are also private methods with the same name Lambda
The implementation type of the expression isVM
The 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 on
Lambda
Expression instance sumLambda
The template class of the expression reflects the callwriteReplace
Method, the return value isSerializedLambda
The instance - Method 2: Obtain data based on serialization and deserialization
SerializedLambda
The 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:
JDK11
The source codeMybatis-Plus
Related to the source code
(E-A-20211127 C-2-D)