不灭的焱

革命尚未成功,同志仍须努力下载JDK17

作者:php-note.com  发布于:2022-05-08 12:27  分类:Java基础  编辑

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接口)。

如果你把MyFunctionalSerializable继承去掉,再执行上述代码:

@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的源码。

 

 

摘自:

  1. Lambda表达式秒用——SerializedLambda序列化
  2. JDK中Lambda表达式的序列化与SerializedLambda的巧妙使用
  3. Lambda表达式序列化
  4. Lambda表达式获得泛型