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

@Datapublic 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 全属性结构器。

@Datapublic 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

兼容性测试

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

@Datapublic 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 是默认兼容缺失字段的反序列化的

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

@Datapublic 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 改个构造,加个个性导致的序列化框架更改来说还要简略。

  • Jackson:

    • Issue: Support for record types in JDK 14
    • Pull Request: Support for record types in JDK 14
    • 对应版本:jackson-databind-2.12.0
  • Kryo

    • Issue: Java 14 records : how to deal with them?
    • Pull Request: Add support for Records in JDK 14
    • 对应版本:kryo-5.1.0
  • XStream

    • Issue: Support for record types in JDK 14
    • Pull Request: Add support for Record types in JDK 14
    • 对应版本:1.5.x,还未公布

这三个框架中实现对于 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