Many friends may have used MyBatis-Plus. When we construct the where condition here, we can directly specify the attribute name through method reference:
LambdaQueryWrapper<Book> qw = new LambdaQueryWrapper <>(); qw.eq(Book::getId, 2 ); List<Book> list = bookMapper.selectList(qw); System.out.println( "list = " + list);
Book::getId This is the method reference. Brother Song has also written an article before to introduce the relevant content, so I won’t go into details here. Here we will simply talk about why MP can identify the attribute name here through Book::getId.
1. Source code analysis
This problem is actually easy to solve. We just follow the qw.eq method and look down. During the execution of this method, we will come to the getColumnCache method several times. This method is where the attribute values are parsed.
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); }
First, the Lambda expression we passed in is parsed into a LambdaMeta object through the LambdaUtils.extract method.
public static <T> LambdaMeta extract (SFunction<T, ?> func) { // 1. In IDEA debugging mode, the lambda expression is a proxy if (func instanceof Proxy) { return new IdeaProxyLambdaMeta ((Proxy) func); } // 2. Reflective reading try { Method method = func.getClass().getDeclaredMethod( "writeReplace" ); method.setAccessible( true ); return new ReflectLambdaMeta ((SerializedLambda) method.invoke(func), func.getClass().getClassLoader()); } catch (Throwable e) { // 3. If reflection fails, use serialization to read return new ShadowLambdaMeta (com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.extract(func)); } }
The focus of this section is actually the reflection reading section. This is to find a method named writeReplace from the Lambda we passed in, and execute this method through reflection, and then encapsulate the execution result into a ReflectLambdaMeta object and return it.
Next, return to the getColumnCache method and continue to String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
obtain the attribute name through .
There is a meta.getImplMethodName()
method here. What this method actually gets is the method name in our Lambda expression, which is getId, and then processes the method name through PropertyNamer.methodToProperty, and finally gets the property name:
public static String methodToProperty (String name) { if (name.startsWith( "is" )) { name = name.substring( 2 ); } else if (name.startsWith( "get" ) || name.startsWith( "set" )) { name = name.substring( 3 ); } else { throw new ReflectionException ( "Error parsing property name '" + name + "'. Didn't start with 'is', 'get' or 'set'." ); } if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt( 1 ))) { name = name.substring( 0 , 1 ).toLowerCase(Locale.ENGLISH) + name.substring( 1 ); } return name; }
As you can see, this parsing process is actually to remove the prefixes get/set/is from the method name, and then return the remaining string with the first letter lowercase.
This is why we pass in Book::getId and finally get the name id.
The question now becomes what exactly is the writeReplace method?
2. writeReplace
This method is actually automatically generated by the bottom layer of the system. We can save the bytecode generated by the Lambda expression at runtime and then decompile it, so that we can see the writeReplace method.
If you need to save the bytecode generated when Lambda is running, you need to add the following content to the startup parameters:
-Djdk.internal.lambda.dumpProxyClasses= /Users/ sang /workspace/ code /mp_demo/ lambda/
The part after the equal sign specifies the storage location of the generated bytecode. You can configure it according to your actual situation.
Taking the Lambda expression at the beginning of this article as an example, after the final generated bytecode is decompiled, the content is as follows:
final class MpDemo02ApplicationTests$$Lambda$1164 implements SFunction { private MpDemo02ApplicationTests$$Lambda$ 1164 () { } public Object apply (Object var1) { return ((Book)var1).getId(); } private final Object writeReplace () { return new SerializedLambda (MpDemo02ApplicationTests.class, "com/baomidou/mybatisplus/core/toolkit/support/SFunction" , "apply" , "(Ljava/lang/Object;)Ljava/lang/Object; " , 5 , "org/javaboy/mp_demo02/model/Book" , "getId" , "()Ljava/lang/Integer;" , "(Lorg/javaboy/mp_demo02/model/Book;)Ljava/lang/Object; " , new Object [ 0 ]); } }
As you can see, the apply method is actually an overridden interface method. In this method, the incoming object is forced to the Book type, and then its getId method is called.
Then you can see that after decompilation, there is an additional writeReplace method. The return value of this method is a SerializedLambda. This SerializedLambda object is actually a description of the Lambda expression. Basically, each parameter can be understood by name. Let me talk about the seventh parameter here. The value is getId. The variable name of this parameter is implMethodName. This is the variable name given in our Lambda expression. This is also the value obtained by meta.getImplMethodName() in the first section.
Now it is clear why the attribute name can be obtained by writing Book::getId.
3. Expand knowledge
Some friends have noticed that in qw.eq(Book::getId, 2);
the method, the first parameter is an instance of SFunction, so let’s say I directly give an instance of SFunction without using Lambda. Please note, this way of writing is incorrect!
The reason is that after the previous source code analysis, we found that in MP to obtain the attribute name based on Book::getId, a key point is to use the bytecode generated by Lambda during execution. If you have not used Lambda, then The so-called Lambda bytecode will not be generated, and the writeReplace method does not exist. According to the source code analyzed above, the attribute name cannot be obtained.
Some friends also said that since it is Lambda, can I not use method references? Can I write it like this?
LambdaQueryWrapper<Book> qw = new LambdaQueryWrapper <>(); qw.eq(b -> b.getId(), 2 ); List<Book> list = bookMapper.selectList(qw); System.out.println( "list = " + list);
This is also a Lambda, but if you write it like this, an error will be reported after running it. why? Let’s take a look at what the bytecode generated by Lambda looks like after decompilation:
final class MpDemo02ApplicationTests$$Lambda$1164 implements SFunction { private MpDemo02ApplicationTests$$Lambda$ 1164 () { } public Object apply (Object var1) { return MpDemo02ApplicationTests.lambda$test18$3fed5817$ 1 ((Book)var1); } private final Object writeReplace () { return new SerializedLambda (MpDemo02ApplicationTests.class, "com/baomidou/mybatisplus/core/toolkit/support/SFunction" , "apply" , "(Ljava/lang/Object;)Ljava/lang/Object; " , 6 , "org/javaboy/mp_demo02/MpDemo02ApplicationTests" , "lambda$test18$3fed5817$1" , "(Lorg/javaboy/mp_demo02/model/Book;)Ljava/lang/Object;" , "(Lorg/javaboy/ mp_demo02/model/Book;)Ljava/lang/Object;" , new Object [ 0 ]); } }
First of all, everyone noticed that the apply method generates something different. MpDemo02ApplicationTests.lambda$test18$3fed5817$1
The method is called in apply and the Book object is passed in as a parameter. The content of this method is equivalent to return book.getId();
. Then in the writeReplace method, when returning the SerializedLambda object, the value of implMethodName is lambda$test18$3fed5817$1
. Going back to the source code analysis at the beginning of this article, you will find that such a method name cannot extract the attribute name we want. So this way of writing is also incorrect.
From here you can also see that b -> b.getId()
a Lambda similar to this Book::getId
is different from a method reference at the bottom level.
Let me give you an example, such as the following piece of code:
public class Demo01 { public static void main (String[] args) { Consumer<String> out1 = System.out::println; out1.accept( "javaboy" ); Consumer<String> out2 = s -> System.out.println(s); out2.accept( "A little rain in Jiangnan" ); } }
There are two outputs here, the first is a method reference and the second is a regular Lambda expression. The execution effects of these two are the same, but the underlying principles are different.
Let’s first look at the first underlying generated Lambda bytecode:
final class Demo01$$Lambda$14 implements Consumer { private final PrintStream arg$ 1 ; private Demo01$$Lambda$ 14 (PrintStream var1) { this .arg$ 1 = var1; } public void accept (Object var1) { this .arg$ 1. println((String)var1); } }
As you can see, the value PrintStream of System.out is passed in as a parameter of the constructor and assigned to the arg$1 variable. When the accept method is called, the arg$1.println method is called to output the string.
The Lambda bytecode generated for the second bottom layer is as follows:
final class Demo01$$Lambda$16 implements Consumer { private Demo01$$Lambda$ 16 () { } public void accept (Object var1) { Demo01.lambda$main$ 0 ((String)var1); } }
As you can see, there is a new lambda$main$0
method here. The underlying logic of this method is actually written when we customize Lambda System.out.println(s)
.
3. Summary
Okay, here is a short article to discuss with my friends qw.eq(Book::getId, 2);
the underlying logic of the method in MP.