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 改個結構,加個特性導致的序列化框架更改來說還要簡單。
-
Jackson :
這三個框架中實現對於 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:
- 記一次批量更新整型型別的列 → 探究 UPDATE 的使用細節
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 執行緒池底層原理詳解與原始碼分析
- 30分鐘掌握 Webpack
- 線性迴歸大結局(嶺(Ridge)、 Lasso迴歸原理、公式推導),你想要的這裡都有
- Django 之路由層
- 【前端必會】webpack loader 到底是什麼
- day42-反射01
- 中心化決議管理——雲端分析
- HashMap底層原理及jdk1.8原始碼解讀
- 詳解JS中 call 方法的實現
- 列印 Logger 日誌時,需不需要再封裝一下工具類?
- 初識設計模式 - 代理模式
- 設計模式---享元模式
- 密碼學奇妙之旅、01 CFB密文反饋模式、AES標準、Golang程式碼
- [ML從入門到入門] 支援向量機:從SVM的推導過程到SMO的收斂性討論
- 從應用訪問Pod元資料-DownwardApi的應用
- Springboot之 Mybatis 多資料來源實現
- Java 泛型程式設計
- CAS核心思想、底層實現