Java Record 序列化相关

Record 在设计之初,就是为了找寻一种纯表示数据的类型载体。Java 的 class 现在经过不断的迭代做功能加法,用法已经非常复杂,各种语法糖,各种多态构造器,各种继承设计导致针对 Java 的序列化框架也做得非常复杂,要考虑的情况有很多很多。每次 Java 升级,如果对类结构有做改动或者加入了新特性,那么序列化框架就都需要改来兼容。这样会阻碍 Java 的发展,于是设计出了 Record 这个专门用来存储数据的类型。

经过上一节的分析我们知道,Record 类型声明后就是 final 的,在编译后,根据 Record 源码插入相关域与方法的字节码,包括:

  1. 自动生成的 private final field
  2. 自动生成的全属性构造器
  3. 自动生成的 public getter 方法
  4. 自动生成的 hashCode(),equals(),toString() 方法:

    1. 从字节码可以看出,这三个方法的底层实现是 invokeDynamic 另一个方法
    2. 调用的是 ObjectMethods.java 这个类中的 bootstrap 方法

里面的所有元素都是不可变的,这样对序列化来讲方便了很多,省略掉很多要考虑的因素,比如字段父子类继承与覆盖等等。序列化一个 Record,只需要关注这个 Record 本身,将其中的所有 field 读取出来即可,并且这些 field 都是 final 的反序列化的时候,仅通过 Record 的规范构造函数(canonical constructor)即给全属性赋值的构造函数。

接下来我们通过一个简单的例子来看下 Record 与普通类的序列化区别。

我们在这里使用了 lombok 简化代码,假设有 UserClass

@Data
public class UserClass implements Serializable {
    private final int id;
    private final int age;
}

还有与它有相同 field 的 UserRecord

public record UserRecord(int id, int age) implements Serializable {}

编写使用 Java 原生序列化的代码:

public class SerializationTest {
    public static void main(String[] args) throws Exception {
        try (FileOutputStream fileOutputStream = new  FileOutputStream("data");
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
            //先写入 UserClass
            objectOutputStream.writeObject(new UserClass(1, -1));
            //再写入 UserRecord
            objectOutputStream.writeObject(new UserRecord(2, -1));
        }
    }
}

执行,将两个对象写入了文件 data 中,然后,再编写代码从这个文件中读取出来并输出:

public class DeSerializationTest {
    public static void main(String[] args) throws Exception {
        try (FileInputStream fileInputStream = new  FileInputStream("data");
             ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
            //读取 UserClass
            System.out.println(objectInputStream.readObject());
            //读取 UserRecord
            System.out.println(objectInputStream.readObject());
        }
    }
}

执行后,会看到输出:

UserClass(id=1, age=-1)
UserRecord[id=1, age=-1]

构造器测试

接下来,我们修改下源码,在 UserClass 和 UserRecord 中增加 id 和 age 都不能小于 1 的判断。并且,额外给 UserRecord 增加一个构造器,来验证反序列化使用的是 UserRecord 全属性构造器。

@Data
public class UserClass implements Serializable {
    private final int id;
    private final int age;

    public UserClass(int id, int age) {
        if (id < 0 || age < 0) {
            throw new IllegalArgumentException("id and age should be larger than 0");
        }
        this.id = id;
        this.age = age;
    }
}
public record UserRecord(int id, int age) implements Serializable {
    public UserRecord {
        if (id < 0 || age < 0) {
            throw new IllegalArgumentException("id and age should be larger than 0");
        }
    }

    public UserRecord(int id) {
        this(id, 0);
    }
}

再次执行代码 DeSerializationTest,我们会发现有报错,但是 UserClass 被反序列化出来了:

UserClass(id=1, age=-1)
Exception in thread "main" java.io.InvalidObjectException: id and age should be larger than 0
    at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2348)
    at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2236)
    at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1742)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:514)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
    at DeSerializationTest.main(DeSerializationTest.java:13)
Caused by: java.lang.IllegalArgumentException: id and age should be larger than 0
    at UserRecord.<init>(UserRecord.java:6)
    at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2346)
    ... 5 more

兼容性测试

我们再来看如果删除一个字段会怎么样:

@Data
public class UserClass implements Serializable {
    private final int age;
}
public record UserRecord(int age) implements Serializable {
}

执行代码,读取 UserClass 的时候就会报错,这也是符合预期的,因为这在普通类对象的反序列化说明中就说这种是不兼容修改。将 UserClass 的字段恢复,重新执行代码,发现成功:

UserClass(id=1, age=-1)
UserRecord[age=-1]

也就是说,Record 是默认兼容缺失字段的反序列化的

我们将字段恢复,再来看多一个字段会怎么样:

@Data
public class UserClass implements Serializable {
    private final int id;
    private final int sex;
    private final int age;
}
public record UserRecord(int id, int sex, int age) implements Serializable {
}

执行代码,读取 UserClass 的时候就会报错,这也是符合预期的。将 UserClass 的字段恢复,重新执行代码,发现成功:

UserClass(id=1, age=-1)
UserRecord[id=2, sex=0, age=-1]

也就是说,Record 是默认兼容字段变多的反序列化的

最后测试一下 Record 的 field 类型如果变了呢:

public record UserRecord(int id, Integer age) implements Serializable {
}

执行代码发现失败,因为类型不匹配了(就算是包装类也不行):

UserClass(id=1, age=-1)
Exception in thread "main" java.io.InvalidClassException: UserRecord; incompatible types for field age
    at java.base/java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2391)
    at java.base/java.io.ObjectStreamClass.getReflector(ObjectStreamClass.java:2286)
    at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:788)
    at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2060)
    at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1907)
    at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2209)
    at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1742)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:514)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
    at DeSerializationTest.main(DeSerializationTest.java:13)

一些主流的序列化框架的兼容

由于 Record 限制了序列化与反序列化的唯一方式,所以其实兼容起来很简单,比起 Java Class 改个结构,加个特性导致的序列化框架更改来说还要简单。

这三个框架中实现对于 Record 的兼容思路都很类似,也比较简单,即:

  1. 实现一个针对 Record 的专用的 Serializer 以及Deserializer。
  2. 通过反射(Java Reflection)或者句柄(Java MethodHandle)验证当前版本的 Java 是否支持 Record,以及获取 Record 的规范构造函数(canonical constructor)以及各种 field 的 getter 进行反序列化和序列化。给大家两个工具类进行参考,分别是使用反射(Java Reflection)和句柄(Java MethodHandle)实现:

    import java.lang.reflect.Constructor;
    import java.lang.reflect.Method;
    import java.util.Arrays;
    import java.util.Comparator;
    import common.RecComponent;
    
    /**
     * Utility methods for record serialization, using Java Core Reflection.
     */
    public class ReflectUtils {
     private static final Method IS_RECORD;
     private static final Method GET_RECORD_COMPONENTS;
     private static final Method GET_NAME;
     private static final Method GET_TYPE;
    
     static {
         Method isRecord;
         Method getRecordComponents;
         Method getName;
         Method getType;
    
         try {
             // reflective machinery required to access the record components
             // without a static dependency on Java SE 14 APIs
             Class<?> c = Class.forName("java.lang.reflect.RecordComponent");
             isRecord = Class.class.getDeclaredMethod("isRecord");
             getRecordComponents = Class.class.getMethod("getRecordComponents");
             getName = c.getMethod("getName");
             getType = c.getMethod("getType");
         } catch (ClassNotFoundException | NoSuchMethodException e) {
             // pre-Java-14
             isRecord = null;
             getRecordComponents = null;
             getName = null;
             getType = null;
         }
    
         IS_RECORD = isRecord;
         GET_RECORD_COMPONENTS = getRecordComponents;
         GET_NAME = getName;
         GET_TYPE = getType;
     }
    
     /** Returns true if, and only if, the given class is a record class. */
     static boolean isRecord(Class<?> type) {
         try {
             return (boolean) IS_RECORD.invoke(type);
         } catch (Throwable t) {
             throw new RuntimeException("Could not determine type (" + type + ")");
         }
     }
    
     /**
      * Returns an ordered array of the record components for the given record
      * class. The order is imposed by the given comparator. If the given
      * comparator is null, the order is that of the record components in the
      * record attribute of the class file.
      */
     static <T> RecComponent[] recordComponents(Class<T> type,
                                                Comparator<RecComponent> comparator) {
         try {
             Object[] rawComponents = (Object[]) GET_RECORD_COMPONENTS.invoke(type);
             RecComponent[] recordComponents = new RecComponent[rawComponents.length];
             for (int i = 0; i < rawComponents.length; i++) {
                 final Object comp = rawComponents[i];
                 recordComponents[i] = new RecComponent(
                         (String) GET_NAME.invoke(comp),
                         (Class<?>) GET_TYPE.invoke(comp), i);
             }
             if (comparator != null) Arrays.sort(recordComponents, comparator);
             return recordComponents;
         } catch (Throwable t) {
             throw new RuntimeException("Could not retrieve record components (" + type.getName() + ")");
         }
     }
    
     /** Retrieves the value of the record component for the given record object. */
     static Object componentValue(Object recordObject,
                                          RecComponent recordComponent) {
         try {
             Method get = recordObject.getClass().getDeclaredMethod(recordComponent.name());
             return get.invoke(recordObject);
         } catch (Throwable t) {
             throw new RuntimeException("Could not retrieve record components ("
                     + recordObject.getClass().getName() + ")");
         }
     }
    
     /**
      * Invokes the canonical constructor of a record class with the
      * given argument values.
      */
     static <T> T invokeCanonicalConstructor(Class<T> recordType,
                                                     RecComponent[] recordComponents,
                                                     Object[] args) {
         try {
             Class<?>[] paramTypes = Arrays.stream(recordComponents)
                     .map(RecComponent::type)
                     .toArray(Class<?>[]::new);
             Constructor<T> canonicalConstructor = recordType.getConstructor(paramTypes);
             return canonicalConstructor.newInstance(args);
         } catch (Throwable t) {
             throw new RuntimeException("Could not construct type (" + recordType.getName() + ")");
         }
     }
    }
package invoke;

import common.RecComponent;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Comparator;
import static java.lang.invoke.MethodType.methodType;

/**
 * Utility methods for record serialization, using MethodHandles.
 */
public class InvokeUtils {
    private static final MethodHandle MH_IS_RECORD;
    private static final MethodHandle MH_GET_RECORD_COMPONENTS;
    private static final MethodHandle MH_GET_NAME;
    private static final MethodHandle MH_GET_TYPE;
    private static final MethodHandles.Lookup LOOKUP;

    static {
        MethodHandle MH_isRecord;
        MethodHandle MH_getRecordComponents;
        MethodHandle MH_getName;
        MethodHandle MH_getType;
        LOOKUP = MethodHandles.lookup();

        try {
            // reflective machinery required to access the record components
            // without a static dependency on Java SE 14 APIs
            Class<?> c = Class.forName("java.lang.reflect.RecordComponent");
            MH_isRecord = LOOKUP.findVirtual(Class.class, "isRecord", methodType(boolean.class));
            MH_getRecordComponents = LOOKUP.findVirtual(Class.class, "getRecordComponents",
                    methodType(Array.newInstance(c, 0).getClass()))
                    .asType(methodType(Object[].class, Class.class));
            MH_getName = LOOKUP.findVirtual(c, "getName", methodType(String.class))
                    .asType(methodType(String.class, Object.class));
            MH_getType = LOOKUP.findVirtual(c, "getType", methodType(Class.class))
                    .asType(methodType(Class.class, Object.class));
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            // pre-Java-14
            MH_isRecord = null;
            MH_getRecordComponents = null;
            MH_getName = null;
            MH_getType = null;
        } catch (IllegalAccessException unexpected) {
            throw new AssertionError(unexpected);
        }

        MH_IS_RECORD = MH_isRecord;
        MH_GET_RECORD_COMPONENTS = MH_getRecordComponents;
        MH_GET_NAME = MH_getName;
        MH_GET_TYPE = MH_getType;
    }

    /** Returns true if, and only if, the given class is a record class. */
    static boolean isRecord(Class<?> type) {
        try {
            return (boolean) MH_IS_RECORD.invokeExact(type);
        } catch (Throwable t) {
            throw new RuntimeException("Could not determine type (" + type + ")");
        }
    }

    /**
     * Returns an ordered array of the record components for the given record
     * class. The order is imposed by the given comparator. If the given
     * comparator is null, the order is that of the record components in the
     * record attribute of the class file.
     */
    static <T> RecComponent[] recordComponents(Class<T> type,
                                               Comparator<RecComponent> comparator) {
        try {
            Object[] rawComponents = (Object[]) MH_GET_RECORD_COMPONENTS.invokeExact(type);
            RecComponent[] recordComponents = new RecComponent[rawComponents.length];
            for (int i = 0; i < rawComponents.length; i++) {
                final Object comp = rawComponents[i];
                recordComponents[i] = new RecComponent(
                        (String) MH_GET_NAME.invokeExact(comp),
                        (Class<?>) MH_GET_TYPE.invokeExact(comp), i);
            }
            if (comparator != null) Arrays.sort(recordComponents, comparator);
            return recordComponents;
        } catch (Throwable t) {
            throw new RuntimeException("Could not retrieve record components (" + type.getName() + ")");
        }
    }

    /** Retrieves the value of the record component for the given record object. */
    static Object componentValue(Object recordObject,
                                         RecComponent recordComponent) {
        try {
            MethodHandle MH_get = LOOKUP.findVirtual(recordObject.getClass(),
                    recordComponent.name(),
                    methodType(recordComponent.type()));
            return (Object) MH_get.invoke(recordObject);
        } catch (Throwable t) {
            throw new RuntimeException("Could not retrieve record components ("
                    + recordObject.getClass().getName() + ")");
        }
    }

    /**
     * Invokes the canonical constructor of a record class with the
     * given argument values.
     */
    static <T> T invokeCanonicalConstructor(Class<T> recordType,
                                                    RecComponent[] recordComponents,
                                                    Object[] args) {
        try {
            Class<?>[] paramTypes = Arrays.stream(recordComponents)
                    .map(RecComponent::type)
                    .toArray(Class<?>[]::new);
            MethodHandle MH_canonicalConstructor =
                    LOOKUP.findConstructor(recordType, methodType(void.class, paramTypes))
                            .asType(methodType(Object.class, paramTypes));
            return (T)MH_canonicalConstructor.invokeWithArguments(args);
        } catch (Throwable t) {
            throw new RuntimeException("Could not construct type (" + recordType.getName() + ")");
        }
    }
}
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer


hashcon
68 声望20 粉丝

Mycat和java jfr unit的核心贡献者,贡献过OpenJDK,Spring Cloud. Apache Bookkeeper. Apache RocketMQ等项目,后台leader