Lambda表达式秒用——SerializedLambda序列化
問題思考
我們經常用Spring的工具類BeanUtils.copyProperties()來做對象拷貝:
org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, String... ignoreProperties)比如有一個User類:
User.java
public class User {private Integer id;private String userName;private String sex;private Integer age;public User() {this.id = id;}public User(int id, String userName, String sex, int age) {this.id = id;this.userName = userName;this.sex = sex;this.age = age;}// getter、setter、toString 方法省略 }User對象拷貝:
User user1=new User(1,"張三","男",18); User user2=new User(); //最后一個可變參數 “String... ignoreProperties” 表示忽略拷貝的字段,這里設置為"sex"和"age": BeanUtils.copyProperties(user1,user2,"sex","age");后面兩個參數"sex"和"age"使用的字符串常量硬編碼來表示User的兩個字段名,這樣會有一個隱患,如果User類的字段名修改,比如"sex"改成"gender",那么 "sex"這種硬編碼代碼很容易漏改。
有沒有辦法解決這個問題?解決的辦法就是避免使用字符串常量硬編碼。
如果字段名不用字符串表示,用什么方式代替?
這讓我想到一種lambda表達式的方式,形如 User::getSex 這種,即表達式 (User u) -> u.getSex() 的簡寫形式。
能否用表達式 User::getSex 代替字符串 ”sex" ? 下面我們就來嘗試一下。
巧用Lambda
(1)我們定義一個函數式接口:
MyFunctional.java
import java.io.Serializable;//該注解表示這是一個函數式接口:即接口中有且只能一個函數聲明。 @FunctionalInterface //注意:一定要繼承序列化`Serializable`接口,后面會分析原因。 public interface MyFunctional<T> extends Serializable {Object apply(T source); }(2)寫一個自己的工具類:注意copyProperties方法的最后一個參數
MyBeanUtils.java
import org.springframework.beans.BeanUtils; import java.beans.Introspector; import java.lang.invoke.SerializedLambda; import java.lang.reflect.Method;public class MyBeanUtils {//模仿BeanUtils.copyProperties()寫一個自己的copyProperties方法,唯一的區別就是最后一個參數的類型由String改為MyFunctionalpublic static <T> void copyProperties(Object source, Object target, MyFunctional<T> ... ignoreProperties){String[] ignorePropertieNames=null;if(ignoreProperties!=null && ignoreProperties.length>0){ignorePropertieNames=new String[ignoreProperties.length];for (int i = 0; i < ignoreProperties.length; i++) {MyFunctional lambda=ignoreProperties[i];//根據lambda表達式得到字段名 ignorePropertieNames[i]=getPropertyName(lambda);}}//最終還是調用Spring的工具類:BeanUtils.copyProperties(source,target,ignorePropertieNames);}//獲取lamba表達式中調用方法對應的屬性名,比如lamba表達式:User::getSex,則返回字符串"sex"public static <T> String getPropertyName(MyFunctional<T> lambda) {try {//writeReplace從哪里來的?后面會講到Method method = lambda.getClass().getDeclaredMethod("writeReplace");method.setAccessible(Boolean.TRUE);//調用writeReplace()方法,返回一個SerializedLambda對象SerializedLambda serializedLambda = (SerializedLambda) method.invoke(lambda);//得到lambda表達式中調用的方法名,如 "User::getSex",則得到的是"getSex"String getterMethod = serializedLambda.getImplMethodName();//去掉”get"前綴,最終得到字段名“sex"String fieldName = Introspector.decapitalize(getterMethod.replace("get", ""));return fieldName;} catch (ReflectiveOperationException e) {throw new RuntimeException(e);}}}測試類:SerializedLambdaTest.java
public class SerializedLambdaTest {public static void main(String[] args) {User user1=new User(1,"張三","男",18);User user2=new User();User user3=new User();//通過Spring的 BeanUtils 工具類拷貝對象,并且忽略“sex"和”age"兩個字段BeanUtils.copyProperties(user1, user2, "sex", "age");//通過自定義的 MyBeanUtils 工具類拷貝對象,并且忽略“sex"和”age"兩個字段MyBeanUtils.copyProperties(user1, user3, User::getSex, User::getAge);System.out.println("user1 = "+user1);System.out.println("user2 = "+user2);System.out.println("user3 = "+user3);} }執行結果如下:
user1 = User(id=1, userName=張三, sex=男, age=18) user2 = User(id=1, userName=張三, sex=null, age=null) user3 = User(id=1, userName=張三, sex=null, age=null)根據執行結果我們發現,user2和user3是一樣的,說明我自己寫的 MyBeanUtils.copyProperties() 這個方法生效了。這樣做的好處就是,當User的sex字段重命名時,IDE工具可以把getSex()方法也重命名。
原理分析
我們知道實現了序列化接口的java對象是可以被序列化的(用于IO傳輸、持久化等),但是真正被序列化的其實只有對象的屬性,而方法(即函數)不能被序列化,可lamba表達式實際上是一個函數(函數式編程),那么“函數”通過什么方式來序列化呢? Java提供了一種機制,會將實現了Serializable接口的lambda表達式轉換成 SerializedLambda 對象之后再去做序列化。
我們修改一下 MyBeanUtils.getPropertyName() 方法,加一些打印信息:
public static String getPropertyName(MyFunctional lambda) {try {Class lambdaClass=lambda.getClass();System.out.println("-------------分割線1-----------");//打印類名:System.out.print("類名:");System.out.println(lambdaClass.getName());//打印接口名:System.out.print("接口名:");Arrays.stream(lambdaClass.getInterfaces()).forEach(System.out::print);System.out.println();//打印方法名:System.out.print("方法名:");for (Method method : lambdaClass.getDeclaredMethods()) {System.out.print(method.getName()+" ");}System.out.println();System.out.println("-------------分割線2-----------");System.out.println();Method method = lambdaClass.getDeclaredMethod("writeReplace");method.setAccessible(Boolean.TRUE);SerializedLambda serializedLambda = (SerializedLambda) method.invoke(lambda);String getterMethod = serializedLambda.getImplMethodName();System.out.println("lambda表達式調用的方法名:"+getterMethod);String fieldName = Introspector.decapitalize(getterMethod.replace("get", ""));System.out.println("根據方法名得到的字段名:"+fieldName);System.out.println();System.out.println("-------------分割線3-----------");System.out.println();System.out.println("SerializedLambda中的所有方法:");for (Method declaredMethod : serializedLambda.getClass().getDeclaredMethods()) {if(declaredMethod.getParameterCount()==0){declaredMethod.setAccessible(Boolean.TRUE);System.out.println("調用方法: "+declaredMethod.getName()+": "+declaredMethod.invoke(serializedLambda));}else{System.out.println("方法聲明:"+declaredMethod.getName()+"("+ Arrays.stream(declaredMethod.getParameterTypes()).map(Class::getName).collect(Collectors.joining(", "))+")");}}return fieldName;} catch (ReflectiveOperationException e) {throw new RuntimeException(e);} }然后執行如下測試代碼:
public static void main(String[] args) {MyBeanUtils.getPropertyName(User::getSex); }執行結果如下:
-------------分割線1----------- 類名:com.join.tools.lambda.SerializedLambdaTest$$Lambda$1/1452126962 接口名:interface com.join.tools.lambda.MyFunctional 方法名:apply writeReplace -------------分割線2-----------lambda表達式調用的方法名:getSex 根據方法名得到的字段名:sex-------------分割線3-----------SerializedLambda中的所有方法: 調用方法: toString: SerializedLambda[capturingClass=class com.join.tools.lambda.SerializedLambdaTest, functionalInterfaceMethod=com/join/tools/lambda/MyFunctional.apply:(Ljava/lang/Object;)Ljava/lang/Object;, implementation=invokeVirtual com/join/tools/lambda/User.getSex:()Ljava/lang/String;, instantiatedMethodType=(Lcom/join/practice/lambda/User;)Ljava/lang/Object;, numCaptured=0] 方法聲明:access$000(java.lang.invoke.SerializedLambda) 調用方法: readResolve: com.join.tools.lambda.SerializedLambdaTest$$Lambda$8/511754216@66a29884 方法聲明:getCapturedArg(int) 調用方法: getFunctionalInterfaceClass: com/join/tools/lambda/MyFunctional 調用方法: getFunctionalInterfaceMethodName: apply 調用方法: getFunctionalInterfaceMethodSignature: (Ljava/lang/Object;)Ljava/lang/Object; 調用方法: getImplClass: com/join/tools/lambda/User 調用方法: getImplMethodKind: 5 調用方法: getImplMethodName: getSex 調用方法: getImplMethodSignature: ()Ljava/lang/String; 調用方法: getCapturedArgCount: 0 調用方法: getCapturingClass: com/join/tools/lambda/SerializedLambdaTest 調用方法: getInstantiatedMethodType: (Lcom/join/practice/lambda/User;)Ljava/lang/Object;發現lambda表達式User::getSex實際上也是一個類,類名為:com.join.tools.lambda.SerializedLambdaTest$$Lambda$1/1452126962,這個類是虛擬機生成的,該類實現了MyFunctional 接口。
該類中除了我們定義的 apply() 方法之外還多了一個 writeReplace()方法,其實這個方法也是虛擬機加上去的,虛擬機會自動給實現Serializable接口的lambda表達式生成 writeReplace()方法(由于MyFunctional繼承了Serializable,因此它的lambda表達式都實現了Serializable接口)。
如果你把MyFunctional的Serializable繼承去掉,再執行上述代碼:
@FunctionalInterface public interface MyFunctional<T> /*extends Serializable*/ {Object apply(T source); }則會報如下錯誤:沒有這樣的方法writeReplace()
Exception in thread "main" java.lang.RuntimeException: java.lang.NoSuchMethodException: com.join.tools.lambda.SerializedLambdaTest$$Lambda$1/1198108795.writeReplace()at com.join.practice.lambda.MyBeanUtils.getPropertyName(MyBeanUtils.java:63)at com.join.practice.lambda.MyBeanUtils.copyProperties(MyBeanUtils.java:20)at com.join.practice.lambda.SerializedLambdaTest.main(SerializedLambdaTest.java:15) Caused by: java.lang.NoSuchMethodException: com.join.practice.lambda.SerializedLambdaTest$$Lambda$6/359023572.writeReplace()at java.lang.Class.getDeclaredMethod(Class.java:2130)at com.join.practice.lambda.MyBeanUtils.getPropertyName(MyBeanUtils.java:48)... 2 moreJava序列化機制
虛擬機在調用write(obj)序列化對象前,如果被序列化的對象有writeReplace方法,則會先調用該方法,用該方法返回的SerializedLambda對象去做序列化,即被序列化的對象被替換了。
根據這個原理,lambda表達式User::getSex在序列化前也會調用writeReplace(),然后返回一個SerializedLambda 對象(真正的被序列化的對象),該對象中包含了lambda表達式的所有信息,比如函數名implMethodName、函數簽名implMethodSignature等等,由于這些信息都是以字段形式存在的,因此可以被序列化,這樣就解決了函數無法被序列化的問題。
巧用writeReplace()
既然在序列化對象時虛擬機可以調用writeReplace()方法,那么我們也可以通過反射的方式來手動調用writeReplace()方法,返回 SerializedLambda對象,然后再調用serializedLambda.getImplMethodName()得到表達式中的方法名getSex,從而實現了根據表達式User::getSex得到字段名"sex"的轉換。回顧上述示例中的代碼片段:
java.lang.invoke.SerializedLambda 部分源碼如下:
public final class SerializedLambda implements Serializable {private static final long serialVersionUID = 8025925345765570181L;private final Class<?> capturingClass;private final String functionalInterfaceClass;private final String functionalInterfaceMethodName;private final String functionalInterfaceMethodSignature;//lambda表達式調用的類,比如示例中的User類private final String implClass;//lambda表達式中調用的函數名,如示例中的getSex private final String implMethodName;//lambda表達式中調用的函數簽名 private final String implMethodSignature;private final int implMethodKind;private final String instantiatedMethodType;private final Object[] capturedArgs;...... //示例中用到這個方法獲取函數名public String getImplMethodName() {return implMethodName;}... ... }擴展知識
用過Mybatis-Plus的同學都知道,它提供了一個 條件構造器 LambdaQueryWrapper ,使用示例如下:
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); //查詢姓名等于“張三”的人 queryWrapper.eq(User::getUserName, "張三"); //避免使用如下方式 //`queryWrapper.eq("userName", "張三")`;它的實現原理和我上面講的類似,都是利用SerializedLambda來實現的。感興趣的同學可以去看一下Mybatis-Plus的源碼。
總結
以上是生活随笔為你收集整理的Lambda表达式秒用——SerializedLambda序列化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 张五常和蒙代尔的对话
- 下一篇: 什么是IP?什么是DN/DNS?什么是h