问题思考
我们经常用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改为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()
方法也重命名。
原理分析
我们知道实现了序列化接口的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
Java序列化机制
虚拟机在调用write(obj)
序列化对象前,如果被序列化的对象有writeReplace
方法,则会先调用该方法,用该方法返回的SerializedLambda
对象去做序列化,即被序列化的对象被替换了。
根据这个原理,lambda表达式User::getSex
在序列化前也会调用writeReplace()
,然后返回一个SerializedLambda
对象(真正的被序列化的对象),该对象中包含了lambda表达式的所有信息,比如函数名implMethodName
、函数签名implMethodSignature
等等,由于这些信息都是以字段形式存在的,因此可以被序列化,这样就解决了函数无法被序列化的问题。
巧用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;
}
...
...
}
扩展知识
用过Mybatis-Plus
的同学都知道,它提供了一个 条件构造器 LambdaQueryWrapper
,使用示例如下:
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//查询姓名等于“张三”的人
queryWrapper.eq(User::getUserName, "张三");
//避免使用如下方式
//`queryWrapper.eq("userName", "张三")`;
它的实现原理和我上面讲的类似,都是利用SerializedLambda
来实现的。感兴趣的同学可以去看一下Mybatis-Plus
的源码。