What is the underlying logic of obtaining property names through method references?

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$1The 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::getIdis 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$0method 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.