Some thoughts on Java Record - serialization related

Java Record serialization related

At the beginning of its design, Record was looking for a type carrier that purely represents data. Java's class is now doing functional addition through continuous iteration, and its usage has become very complex. Various syntactic sugars, various polymorphic constructors, and various inheritance designs have made the serialization framework for Java very complicated. The situation to be considered There are many, many. Every time Java is upgraded, if changes are made to the class structure or new features are added, the serialization framework needs to be changed to be compatible. This will hinder the development of Java, so the Record, a type dedicated to storing data, was designed.

After the analysis in the previous section, we know that the Record type is final after it is declared. After compiling, insert the bytecode of related fields and methods according to the Record source code, including:

  1. Automatically generated private final field
  2. Auto-generated full-property constructor
  3. Auto-generated public getter methods
  4. Automatically generated hashCode(), equals(), toString() methods:
  5. As can be seen from the bytecode, the underlying implementation of these three methods is another method of invokeDynamic
  6. The method in ObjectMethods.javathisbootstrap

All the elements inside are immutable, which is much more convenient for serialization, and omits many factors to be considered, such as field parent-child class inheritance and overwriting, etc. To serialize a Record, you only need to pay attention to the Record itself and read all the fields in it, and these fields are final . When deserializing, only the canonical constructor of the Record is used, that is, the constructor that assigns all attributes.

Next, let's look at the serialization difference between Record and ordinary classes through a simple example.

We use lombok to simplify the code here, assuming UserClass:

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

And it has the same field UserRecord:

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

Write code that uses Java's native serialization:

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));
		}
	}
}

Execute, write the two objects to the datafile , and then write code to read it from this file and output:

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());
		}
	}
}

After execution, you will see the output:

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

Constructor test

Next, we modify the source code and add the judgment that both id and age cannot be less than 1 in UserClass and UserRecord. And, an additional constructor is added to UserRecord to verify that the UserRecord full-property constructor is used for deserialization.

@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);
	}
}

Execute the code again DeSerializationTest, we will find that there is an error, but the UserClass is deserialized:

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

Compatibility test

Let's see what happens if we delete a field:

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

When executing the code, an error will be reported when reading UserClass , which is also expected, because this is said to be an incompatible modification in the deserialization description of ordinary class objects. Restore the fields of UserClass, re-execute the code, and find success:

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

That is, Record is by default compatible with deserialization of missing fields

Let's restore the field and see what happens with one more field:

@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 {
}

When executing the code, an error will be reported when reading UserClass , which is also expected. Restore the fields of UserClass, re-execute the code, and find success:

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

That is to say, Record is deserialized with more fields compatible by default

Finally, test if the field type of the Record has changed:

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

Executing code discovery fails because the types don't match (not even the wrapper class):

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)

Compatibility with some mainstream serialization frameworks

Since Record restricts the only way of serialization and deserialization, it is actually very simple to be compatible. Compared with changing the structure of Java Class, it is simpler to change the serialization framework caused by adding a feature.

The compatible ideas for Record in these three frameworks are very similar and relatively simple, namely:

  1. Implement a dedicated Serializer and Deserializer for Record.
  2. Verify whether the current version of Java supports Record through reflection (Java Reflection) or handle (Java MethodHandle), and obtain the canonical constructor of Record and the getter of various fields for deserialization and serialization. For your reference, two tool classes are implemented using reflection (Java Reflection) and handle (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() + ")");
        }
    }
}

Search "My Programming Meow" on WeChat, follow the official account, brush it every day, easily improve your technology, and gain various offers :

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324204444&siteId=291194637