从Mybatis-Plus开始熟悉SerializedLambda

打印 上一主题 下一主题

主题 919|帖子 919|积分 2757

从Mybatis-Plus开始熟悉SerializedLambda

背景

对于利用过Mybatis-Plus的Java开发者来说,肯定对以下代码不生疏:
  1. @TableName("t_user")
  2. @Data
  3. public class User {
  4.         private String id;
  5.         private String name;
  6.         private String password;
  7.         private String gender;
  8.         private int age;
  9. }
复制代码
  1. @Mapper
  2. public interface UserDAO extends BaseMapper<User> {
  3. }
复制代码
  1. @Service
  2. public class UserService {
  3.         @Resource
  4.         private UserDAO userDAO;
  5.         public List<User> getUsersBetween(int minAge, int maxAge) {
  6.                 return userDAO.selectList(new LambdaQueryWrapper<User>()
  7.                                 .ge(User::getAge, minAge)
  8.                                 .le(User::getAge, maxAge));
  9.         }
  10. }
复制代码
在引入Mybatis-Plus之后,只需要按照上述代码定义出基础的DO、DAO和Service,而不用再本身显式编写对应的SQL,就能完成大部分通例的CRUD操作。Mybatis-Plus的具体利用方法和实现原理此处不展开,有兴趣的读者可以移步Mybatis-Plus官网了解更多信息。
第一次看到UserService中getUsersBetween()方法的实现时,可能有不少读者会产生一些疑惑:

  • User::getAge这是什么语法?
  • Mybatis-Plus是如何根据这个这个User::getAge来推测出生成SQL时的列名的?
接下来我们就从这两个问题入手,来了解Java 8开始引入的SerializedLambda
User::getAge的背后——Lambda表达式和方法引用

Lambda表达式

Lambda表达式是Java 8开始引入的一大新特性,是一个非常有用的语法糖,让Java开发者也可以体验一下“函数式”编程的感觉。Lambda表达式重要的功能之一就是简化了我们创建匿名类的过程,当然,这里的匿名类只能有一个方法。举个例子,当我们想创建一个线程时,利用匿名类可以这样处理:
  1. public static void main(String[] args) throws InterruptedException {
  2.         //匿名类实现了Runnable接口
  3.         Thread thread = new Thread(new Runnable() {
  4.                 //重写run方法
  5.                 @Override
  6.                 public void run() {
  7.                         System.out.println("stdout from thread: " + Thread.currentThread().getName());
  8.                 }
  9.         });
  10.         thread.start();
  11.         thread.join();
  12. }
复制代码
而利用Lambda表达式则可以简化为:
  1. public static void main(String[] args) throws InterruptedException {
  2.         Thread thread = new Thread(() -> System.out.println("stdout from thread: " + Thread.currentThread().getName()));
  3.         thread.start();
  4.         thread.join();
  5. }
复制代码
这就是Lambda表达式最根本的也是最为焦点的功能——让编写实现只有一个抽象方法的接口的匿名类变得简朴。而这种只有一个抽象方法的接口被称为函数式接口
只能有一个抽象方法的言外之意是函数式接口可以有其他的非抽象方法,如静态方法和默认方法
通常函数式接口会利用@FunctionalInterface注解修饰,表示这是一个函数式接口。此注解的作用是让编译器检查被注解的接口是否符合函数式接口的规范,若不符合编译器会产生对应的错误
好奇什么时候会报错的小同伴可参考官方文档形貌:

  • If a type is annotated with this annotation type, compilers are required to generate an error message unless:
  • The type is an interface type and not an annotation type, enum, or class.
    The annotated type satisfies the requirements of a functional interface
更多Lambda表达式干系的内容可参考官方文档:Lambda Expression和其他资料。
方法引用

有时我们编写的Lambda表达式仅仅是简朴地调用了一个方法,而没有进行其他操作,这时候就可以再一次进行简化,甚至连Lambda表达式都不用写了,直接写被调用的方法引用就行了。 依旧以创建一个线程为例:
  1. public class Main {
  2.         public static void main(String[] args) throws InterruptedException {
  3.                 //这里Lambda表达式只有一个作用,就是调用别的方法来处理任务
  4.                 Thread thread = new Thread(() -> sayHello());
  5.                 thread.start();
  6.                 thread.join();
  7.         }
  8.         public static void sayHello() {
  9.                 System.out.println("stdout from thread: " + Thread.currentThread().getName());
  10.         }
  11. }
复制代码
对于上述代码,好像设计者认为() -> sayHello()这个表达式都有点多余,所以引入了方法引用,可以将上述代码简化为:
  1. public class Main {
  2.         public static void main(String[] args) throws InterruptedException {
  3.                 //Main::sayHello即是方法引用的写法
  4.                 Thread thread = new Thread(Main::sayHello);
  5.                 thread.start();
  6.                 thread.join();
  7.         }
  8.         public static void sayHello() {
  9.                 System.out.println("stdout from thread: " + Thread.currentThread().getName());
  10.         }
  11. }
复制代码
按官方文档的说法就是,这种形式更加紧凑,可读性更高。用文档的原话就是:
You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it's often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name.
这里有个小细节,最后一句话提到they are compact, easy-to-read lambda expressions...也恰好给方法引用定了性,即方法引用本身还是一种Lambda表达式,只是形式比力特殊罢了
回到主题,说到这里,相信读者也就明白了,User::getAge不外就是一个方法引用罢了,而更本质一点,也不外就是一个Lambda表达式而已,而其语义可以理解为它指向了User类中的getAge方法
说明白了User::getAge是何物之后,接下来就该看看Mybatis-Plus是如何利用它的了
Mybatis-Plus是怎么利用方法引用的?

通过源码跟踪,会发现Mybatis-Plus中有一个名为AbstractLambdaWrapper的类,其中有一个名为columnToString()的方法,其作用就是通过Getter提取出列名。其实现如下:
  1. //Mybatis-Plus中将Getter转换为列名的方法。参数column即为对应要解析的Getter的方法引用
  2. protected String columnToString(SFunction<T, ?> column) {
  3.         return this.columnToString(column, true);
  4. }
  5. protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {
  6.         ColumnCache cache = this.getColumnCache(column);
  7.         return onlyColumn ? cache.getColumn() : cache.getColumnSelect();
  8. }
复制代码
columnToString()仅是一个入口,具体逻辑则是在同类的getColumnCache()方法中:
  1. protected ColumnCache getColumnCache(SFunction<T, ?> column) {
  2.         //从Getter方法引用中提取元数据。元数据中就包含了Getter的方法名
  3.         LambdaMeta meta = LambdaUtils.extract(column);
  4.         //从Getter方法名中截取字段名
  5.         String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
  6.         //下边是Mybatis-Plus缓存相关的逻辑,可忽略
  7.         Class<?> instantiatedClass = meta.getInstantiatedClass();
  8.         this.tryInitCache(instantiatedClass);
  9.         return this.getColumnCache(fieldName, instantiatedClass);
  10. }
复制代码
从上述代码中可知,从Getter方法引用中提取Getter方法的具体名称的逻辑是在LambdaUtils.extract()中完成的,再来看看这个方法的实现:
  1. public static <T> LambdaMeta extract(SFunction<T, ?> func) {
  2.         if (func instanceof Proxy) {
  3.                 //从IDEA代理对象获取,这个逻辑不重要,可以忽略掉
  4.                 return new IdeaProxyLambdaMeta((Proxy)func);
  5.         } else {
  6.                 try {
  7.                         //重点在这里,通过反射从方法引用(Lambda表达式)中找到'writeReplace'方法
  8.                         Method method = func.getClass().getDeclaredMethod("writeReplace");
  9.                         method.setAccessible(true);
  10.                         //反射调用writeReplace方法,将结果强制转型为 SerializedLambda
  11.                         return new ReflectLambdaMeta((SerializedLambda)method.invoke(func), func.getClass().getClassLoader());
  12.                 } catch (Throwable var2) {
  13.                         return new ShadowLambdaMeta(com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.extract(func));
  14.                 }
  15.         }
  16. }
复制代码
在LambdaUtils.extract()中,通过对Lambda表达式进行反射查找一个名为writeReplace()的方法并调用,最终得到的结果强制转型为SerializedLambda范例。这就是通过方法引用得到方法具体名称的最重要的步骤
在LambdaUtils.extract()实行完成后得到一个LambdaMeta对象,这个对象中封装了Lambda表达式(在这里就是某个Getter的方法引用)的元数据,其中的getImplMethodName()方法的实现本质就是调用了SerializedLambda的同名方法:
  1. public class ReflectLambdaMeta implements LambdaMeta {
  2.         ...
  3.         private final SerializedLambda lambda;
  4.         ...
  5.         public String getImplMethodName() {
  6.                 return this.lambda.getImplMethodName();
  7.         }
  8.         ...
  9. }
复制代码
再来看调用LambdaUtils.extract()后getColumnCache()函数中的代码:
  1. String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
复制代码
这里调用上边提到的getImplMethodName()方法,最终得到的就是某个方法引用对应的方法名称,然后通过methodToProperty()再将方法名称转换为字段名称:
  1. //逻辑比较简单,就是按照Getter的命名规则
  2. //将getXXX 或 isXXX 的get和is前缀给拿掉,剩下的XXX就是属性名
  3. public static String methodToProperty(String name) {
  4.         if (name.startsWith("is")) {
  5.                 name = name.substring(2);
  6.         } else {
  7.                 if (!name.startsWith("get") && !name.startsWith("set")) {
  8.                         throw new ReflectionException("Error parsing property name '" + name + "'.  Didn't start with 'is', 'get' or 'set'.");
  9.                 }
  10.                 name = name.substring(3);
  11.         }
  12.         if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) {
  13.                 name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
  14.         }
  15.         return name;
  16. }
复制代码
到这里,第二个问题,Mybaits-Plus是如何将User::getAge转换成对应列名的逻辑也就清楚了:

  • Mybatis-Plus的AbstractLambdaWrapper中columnToString(User::getAge)负责得到字符串形式的列名
  • columnToString(User::getAge)则是调用getColumnCache(User::getAge)方法来提取列名
  • getColumnCache(User::getAge)中利用LambdaUtils.extract(User::getAge)来反射获取User::getAge这个方法引用(Lambda表达式)的元数据。(焦点是得到SerializedLambda对象)
  • 通过SerializedLambda的getImplMethodName()方法得到了方法引用的具体名称
注意,SerializedLambda类是JDK的,不是Mybatis-Plus的


  • 得到方法名称后,再通过methodToProperty()从方法名获取字段名,这一步重要是剔掉is大概get前缀
从这里也能看出来,符合标准Getter命名规范的才气被解析,即遵循getXXX / isXXX格式
最后增补一点,这只是将User::getAge这种方法引用最终转为"age"这样的属性名的逻辑。Mybatis-Plus中后续还有一些注解可以控制列名的映射,这里暂不讨论
SerializedLambda

通过前面的铺垫,终于到了介绍本文的主角——SerializedLambda的时刻了
那什么是SerializedLambda?SerializedLambda顾名思义就是序列化后的Lambda。这个类中记录了Lambda表达式的上下文信息,重要包括:

  • 捕捉类信息(capturingClass):即这个Lambda表达式是在哪个类中用到的
  • 函数接口类(functionalInterfaceClass):函数接口类路径
  • 函数接口的方法名(functionalInterfaceMethodName):函数接口中抽象方法的名称
  • 函数接口方法签名(functionalInterfaceMethodSignature):函数接口中抽象方法的签名
  • 实现类(implClass):哪个类实现了此函数接口
  • 实现方法名(implMethodName):实现此函数接口对应的方法名
  • 实现方法的签名(implMethodSignature):实现此函数接口对应的方法的签名
  • 实现方法范例(implMethodKind):getStatic/invokeVirtual/invokeStatic等调用范例
  • 捕捉的参数(capturedArgs):Lambda表达式可能会用到外部变量,这里记录捕捉到的变量
从SerializedLambda包含的信息可知,我们可以通过这个范例的对象拿到关于Lambda表达式的一些基础信息。而Mybatis-Plus正是利用了这一点,其拿到了某个Getter的方法引用(一定记着方法引用也是一种Lambda),然后调用writeReplace()方法得到关于该方法引用的SerializedLambda对象,这个对象就包含了这个方法引用的形貌信息,其中就包含了这个方法引用对应方法的名称(implMethodName)
总的来说,SerializedLambda可以理解为Lambda表达式的序列化形式,而序列化重要就是将内存对象的关键属性提出来转化为可传输和可持久化的形式,我们可以通过序列化后的结果大致了解到该对象的结构。SerializedLambda的一大作用正是如此,我们可以通过它来了解到原始Lambda表达式大概是由哪些关键因素构成的
无中生有的writeReplace方法

在前文获取SerializedLambda对象时有这么几行代码:
  1. ...
  2. func.getClass().getDeclaredMethod("writeReplace");
  3. method.setAccessible(true);
  4. (SerializedLambda)method.invoke(func);
  5. ...
复制代码
这是典型的反射调用代码,反射这里就不多展开说了。可能很多人关心的是,这个writeReplace()方法从何而来?有何用处?
writeReplace()并非专为SerializedLambda而设计,这个方法其实是Java的序列化机制自带的一个扩展点,任何需要被序列化的类,可以在类中声明这个方法来控制序列化此类对象时利用的替换对象。这样说起来可能有点绕,下边我们来看一个简朴的示例:
假设有一个User类,定义如下:
  1. @Data
  2. public class User implements Serializable {
  3.         private String id;
  4.         private String name;
  5.         private String password;
  6.         private String gender;
  7.         private int age;
  8.         //声明writeReplace方法
  9.         public Object writeReplace() throws ObjectStreamException {
  10.                 System.out.println("User's writeReplace() is been called.");
  11.                 return "user";
  12.         }
  13. }
复制代码
接下来利用ObjectOutputStream来序列化User对象:
  1. public static void main(String[] args) throws Exception {
  2.         User user = new User();
  3.         user.setName("longqinx");
  4.         ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream());
  5.         out.writeObject(user);
  6. }
复制代码
实行上述代码后可以看到控制台输出了User's writeReplace() is been called.,证明我们在User类中声明的writeReplace方法确实被调用了
通过上述示例,我们可以得到初步的结论:writeReplace()方法是一个Java内部约定的方法,其作用是在序列化某个范例对象的时候,允许我们自定义一个替换对象去序列化。比如上述示例中序列化User对象时,我们利用一个String对象作为代替品。如果类中定义了此方法,则序列化时会主动调用,反之按通例序列化逻辑进行序列化
注意,这里的序列化指的是利用Java自身的序列化机制完成的序列化,而不是利用Jackson这种序列化框架
回到正题,编译器会Lambda表达式范例主动生成一个writeReplace()方法,该方法返回一个SerializedLambda作为真正序列化的对象,以此保证对Lambda表达式的正确序列化
而我们则可以利用这一性质,主动反射调用writeReplace()方法来获取SerializedLambda对象,从而得到Lambda表达式的一些元数据,有了这些元数据我们就能发挥创意做一些更有趣的东西
实战——实现一个根据Getter方法引用获取字段名的工具类

1. 定义函数接口
  1. @FunctionalInterface
  2. public interface Getter<T,R> extends Serializable {
  3.         R get(T t);
  4. }
复制代码

  • 注意,这里必须要继承自Serializable接口,不然编译器不会为对应的Lambda表达式生成writeReplace()方法,也就无法获取到SerializedLambda对象
2. 实现工具类
  1. public class FieldNameExtractor {
  2.         /**
  3.          * 从Getter方法引用提取字段名
  4.          *
  5.          * @param getter 方法引用,必须是getter的
  6.          * @return 字段名
  7.          */
  8.         public static <T, R> String extractFieldNameFromGetter(Getter<T, R> getter) {
  9.                 try {
  10.                         //反射获取writeReplace方法
  11.                         Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace");
  12.                         writeReplace.setAccessible(true);
  13.                         //调用writeReplace方法
  14.                         SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(getter);
  15.                         //获取实现方法,也就是方法引用对应的方法名
  16.                         String methodName = serializedLambda.getImplMethodName();
  17.                         return extractFieldName(methodName);
  18.                 } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
  19.                         throw new RuntimeException(e);
  20.                 }
  21.         }
  22.         private static String extractFieldName(String methodName) {
  23.                 String fieldName;
  24.                 if (methodName.startsWith("is")) {
  25.                         fieldName = methodName.substring(2);
  26.                 } else if (methodName.startsWith("get")) {
  27.                         fieldName = methodName.substring(3);
  28.                 } else {
  29.                         throw new IllegalArgumentException("method name should start with 'is' or 'get'");
  30.                 }
  31.                 return Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
  32.         }
  33. }
复制代码
3. 测试
  1. public class Main {
  2.         public static void main(String[] args) throws Exception {
  3.                 //输出name
  4.                 System.out.println(FieldNameExtractor.extractFieldNameFromGetter(User::getName));
  5.                 //输出age
  6.                 System.out.println(FieldNameExtractor.extractFieldNameFromGetter(User::getAge));
  7.         }
  8. }
复制代码
函数接口定义解惑

读者在看到上述示例代码后,可能存在疑惑,为何Getter这个函数式接口要这样定义,为什么有两个泛型参数T和R?
其实只用一个泛型参数即可,这时候应该这样定义:
  1. @FunctionalInterface
  2. public interface InstanceGetter<R> extends Serializable {
  3.         R get();
  4. }
复制代码
工具类中实现逻辑不变,只是调整参数范例即可:
  1. //参数改为InstanceGetter类型,其他不变
  2. public static <R> String extractFieldNameFromGetter(InstanceGetter<R> getter) {
  3.         try {
  4.                 //反射获取writeReplace方法
  5.                 Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace");
  6.                 writeReplace.setAccessible(true);
  7.                 //调用writeReplace方法
  8.                 SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(getter);
  9.                 //获取实现方法,也就是方法引用对应的方法名
  10.                 String methodName = serializedLambda.getImplMethodName();
  11.                 return extractFieldName(methodName);
  12.         } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
  13.                 throw new RuntimeException(e);
  14.         }
  15. }
复制代码
但在利用的时候,传递参数时就不能用User::getName或User::getAge这样的形式了,而应该先实例化User对象,用实例方法引用:
  1. public class Main {
  2.         public static void main(String[] args) throws Exception {
  3.                 User user = new User();
  4.                 //注意这里是 user::getName而不是User::getName,是用user这个实例来得到方法引用
  5.                 System.out.println(FieldNameExtractor.extractFieldNameFromGetter(user::getName));
  6.                 System.out.println(FieldNameExtractor.extractFieldNameFromGetter(user::getAge));
  7.         }
  8. }
复制代码
相信看了这两个对比之后读者也就能察觉到其中的不同了:User::getName是通过类名引用的,而user::getName是通过实例对象引用的
前者真正要被调用时,还得知道在哪个对象上调用(类似反射的invoke),所以会有一个泛型参数 T 来表示对象的范例,而R则是Getter的返回值范例;
后者则是通过实例对象得到的方法引用,这时候Lambda能捕捉到这个实例对象,因此在调用时自然也知道该在哪个对象上调用,此时就可以省去 T 这个泛型参数了
总结

回答一开始的问题


  • User::getAge这是什么语法?
Java 8开始引入Lambda表达式和方法引用的概念,User::getAge这种写法称为方法引用,其本质上也是一种Lambda表达式


  • Mybatis-Plus是如何根据这个这个User::getAge来推测出生成SQL时的列名的?
Java中有个SerializedLambda类,其用于表示序列化后的Lambda表达式,通过此类可以获取方法名、实现类名等众多关于Lambda表达式的元数据。对于一个可序列化的Lambda表达式,可通过反射调用其writeReplace方法获取关联的SerializedLambda对象。
当对User::getAge这个Lambda表达式实行此操作时,得到的SerializedLambda中就包含了User类中getAge()这个方法的名称、签名等信息。此时通过getter命名规范,去掉is或get前缀,并将首字符小写即可得到字段名
其他一些没有提到的

在笔者实际的研究过程中,充实利用了IDEA进行调试,但限于篇幅,这个过程并未在本文中详细形貌。感兴趣的读者可以本身动手去认真调试一番。这里给几个思路:

  • 在写函数式接口时,试一试继承Serializable和不继承时反射调用writeReplace()方法的结果
  • 拿到一个Lambda表达式对象,尝试反射一下其中有哪些方法
  • 反射一下利用了Lambda表达式的类,看看有什么特殊之处
  • 获取一个Lambda表达式关联的SerializedLambda对象,看看里边存了些什么

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

西河刘卡车医

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表