Java Record 的一些思考 - 序列化相关
Java Record 序列化相关
Record 在设计之初,就是为了找寻一种纯表示数据的类型载体。Java 的 class 现在经过不断的迭代做功能加法,用法已经非常复杂,各种语法糖,各种多态构造器,各种继承设计导致针对 Java 的序列化框架也做得非常复杂,要考虑的情况有很多很多。每次 Java 升级,如果对类结构有做改动或者加入了新特性,那么序列化框架就都需要改来兼容。这样会阻碍 Java 的发展,于是设计出了 Record 这个专门用来存储数据的类型。
经过上一节的分析我们知道,Record 类型声明后就是 final 的,在编译后,根据 Record 源码插入相关域与方法的字节码,包括:
- 自动生成的 private final field
- 自动生成的全属性构造器
- 自动生成的 public getter 方法
- 自动生成的 hashCode(),equals(),toString() 方法:
- 从字节码可以看出,这三个方法的底层实现是 invokeDynamic 另一个方法
- 调用的是
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 的兼容思路都很类似,也比较简单,即:
- 实现一个针对 Record 的专用的 Serializer 以及Deserializer。
- 通过反射(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:
Java Record 的一些思考 - 序列化相关的更多相关文章
- Java Record 的一些思考 - 默认方法使用以及基于预编译生成相关字节码的底层实现
快速上手 Record 类 我们先举一个简单例子,声明一个用户 Record. public record User(long id, String name, int age) {} 这样编写代码之 ...
- 为什么hadoop中用到的序列化不是java的serilaziable接口去序列化而是使用Writable序列化框架
继上一个模块之后,此次分析的内容是来到了Hadoop IO相关的模块了,IO系统的模块可谓是一个比较大的模块,在Hadoop Common中的io,主要包括2个大的子模块构成,1个是以Writable ...
- java中可定制的序列化过程 writeObject与readObject
来源于:[http://bluepopopo.iteye.com/blog/486548] 什么是writeObject 和readObject?可定制的序列化过程 这篇文章很直接,简单易懂.尝试着翻 ...
- spring mvc返回json字符串数据,只需要返回一个java bean对象就行,只要这个java bean 对象实现了序列化serializeable
1.spring mvc返回json数据,只需要返回一个java bean对象就行,只要这个java bean 对象实现了序列化serializeable 2. @RequestMapping(val ...
- Java中Calendar(日历)相关API举例
Java中Calendar(日历)相关API举例,实现功能:输入一个年份和月份打印出这个月的日历. package calendarPrint; import java.util.Calendar; ...
- Java序列化相关
java类实现serializable有什么好处或意义 一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才是可序列化的.因此如果要序列化某些类的对象,这些类就必须实现Ser ...
- java io系列06之 序列化总结(Serializable 和 Externalizable)
本章,我们对序列化进行深入的学习和探讨.学习内容,包括序列化的作用.用途.用法,以及对实现序列化的2种方式Serializable和Externalizable的深入研究. 转载请注明出处:http: ...
- Java和C#的socket通信相关(转)
这几天在博客园上看到好几个写Java和C#的socket通信的帖子.但是都为指出其中关键点. C# socket通信组件有很多,在vs 使用nuget搜索socket组件有很多类似的.本人使用的是自己 ...
- JAVA RPC(二)序列化协议杂谈
序列化和反序列化作为Java里一个较为基础的知识点,大家心里也有那么几句要说的,但我相信很多小伙伴掌握的也就是那么几句而已,如果再深究问一下Java如何实现序列化和反序列化的,就可能不知所措了!遥记当 ...
随机推荐
- Output of C++ Program | Set 2
Predict the output of below C++ programs. Question 1 1 #include<iostream> 2 using namespace st ...
- spring boot集成mybatis框架
概述 中文官网:http://www.mybatis.cn 参考教程:https://www.w3cschool.cn/mybatis MyBatis Plus:http://mp.baomidou. ...
- spring boot项目创建与使用
概述 spring boot通常使用maven创建,重点在于pom.xml配置,有了pom.xml配置,可以先创建一个空的maven项目,然后从maven下载相关jar包. spring boot d ...
- Default Constructors
A constructor without any arguments or with default value for every argument, is said to be default ...
- dom4j解析XML学习
原理:把dom与SAX进行了封装 优点:JDOM的一个智能分支.扩充了其灵活性增加了一些额外的功能. package com.dom4j.xml; import java.io.FileNotFoun ...
- Maven项目打包成war包并启动war包运行
1 项目打包 1.1 右键点击所需要打包的项目,点击如图所示 Maven clean,这里 Maven 会清除掉之前对这个项目所有的打包信息. 1.2进行完 Maven clean 操作后,在ecli ...
- 【C/C++】二维数组的传参的方法/二维字符数组的声明,使用,输入,传参
[问题] 定义了一个子函数,传参的内容是一个二维数组 编译提示错误 因为多维数组作为形参传入时,必须声明除第一位维外的确定值,否则系统无法编译(算不出偏移地址) [二维数组的传参] 方法一:形参为二维 ...
- 用graphviz可视化决策树
1.安装graphviz. graphviz本身是一个绘图工具软件,下载地址在:http://www.graphviz.org/.如果你是linux,可以用apt-get或者yum的方法安装.如果是w ...
- 【经验分享】win10 cmake 构建 Tengine 工程
欢迎关注我的公众号 [极智视界],回复001获取Google编程规范 O_o >_< o_O O_o ~_~ o_O 本教程详细记录了在 win10 环境中 ...
- 【JAVA今法修真】 第二章 一气化三清 线程分心念
这是我的微信公众号,希望有兴趣的朋友能够一起交流,也希望能够多多支持新人作者,你的每一份关注都是我写文章的动力:南橘ryc 天有八纪,地分九州,万法仙门与天道剑宗一并坐落在东北方通辽州. 与李小庚想象 ...