Java Record 的一些思考 - 序列化相關

乾貨滿滿張雜湊發表於2022-01-04

Java Record 序列化相關

Record 在設計之初,就是為了找尋一種純表示資料的型別載體。Java 的 class 現在經過不斷的迭代做功能加法,用法已經非常複雜,各種語法糖,各種多型構造器,各種繼承設計導致針對 Java 的序列化框架也做得非常複雜,要考慮的情況有很多很多。每次 Java 升級,如果對類結構有做改動或者加入了新特性,那麼序列化框架就都需要改來相容。這樣會阻礙 Java 的發展,於是設計出了 Record 這個專門用來儲存資料的型別。

經過上一節的分析我們知道,Record 型別宣告後就是 final 的,在編譯後,根據 Record 原始碼插入相關域與方法的位元組碼,包括:

  1. 自動生成的 private final field
  2. 自動生成的全屬性構造器
  3. 自動生成的 public getter 方法
  4. 自動生成的 hashCode(),equals(),toString() 方法:
  5. 從位元組碼可以看出,這三個方法的底層實現是 invokeDynamic 另一個方法
  6. 呼叫的是 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

相關文章