1. 问题思考
我们经常用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"
? 下面我们就来尝试一下。
2. 巧用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改为MyFunctional public 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()
方法也重命名。
3. 原理分析
我们知道实现了序列化接口的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 more
3.1 Java序列化机制
虚拟机在调用write(obj)
序列化对象前,如果被序列化的对象有writeReplace
方法,则会先调用该方法,用该方法返回的SerializedLambda
对象去做序列化,即被序列化的对象被替换了。
根据这个原理,lambda表达式User::getSex
在序列化前也会调用writeReplace()
,然后返回一个SerializedLambda
对象(真正的被序列化的对象),该对象中包含了lambda表达式的所有信息,比如函数名implMethodName
、函数签名implMethodSignature
等等,由于这些信息都是以字段形式存在的,因此可以被序列化,这样就解决了函数无法被序列化的问题。
3.2 巧用writeReplace()
既然在序列化对象时虚拟机可以调用writeReplace()
方法,那么我们也可以通过反射的方式来手动调用writeReplace()
方法,返回SerializedLambda
对象,然后再调用serializedLambda.getImplMethodName()
得到表达式中的方法名getSex
,从而实现了根据表达式User::getSex
得到字段名"sex"
的转换。回顾上述示例中的代码片段:
public static <T> String getPropertyName(MyFunctional<T> lambda) { try { Class lambdaClass=lambda.getClass(); Method method = lambdaClass.getDeclaredMethod("writeReplace"); //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); } }
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; } ... ... }
4. 扩展知识
用过Mybatis-Plus
的同学都知道,它提供了一个 条件构造器 LambdaQueryWrapper
,使用示例如下:
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); //查询姓名等于“张三”的人 queryWrapper.eq(User::getUserName, "张三"); //避免使用如下方式 //`queryWrapper.eq("userName", "张三")`;
它的实现原理和我上面讲的类似,都是利用SerializedLambda
来实现的。感兴趣的同学可以去看一下Mybatis-Plus
的源码。
摘自: