diff --git a/src/java.base/share/classes/java/io/InvalidClassException.java b/src/java.base/share/classes/java/io/InvalidClassException.java
index be187597726..5045b0c4566 100644
--- a/src/java.base/share/classes/java/io/InvalidClassException.java
+++ b/src/java.base/share/classes/java/io/InvalidClassException.java
@@ -37,6 +37,9 @@
* an enum type
*
Other conditions given in the Java Object Serialization
* Specification
+ *
A {@linkplain Class#isValue()} value class implements {@linkplain Serializable}
+ * but does not delegate to a serialization proxy using {@code writeReplace()}.
+ *
Value classes are {@linkplain Serializable} through the use of the serialization proxy pattern.
+ * See {@linkplain ObjectOutputStream##valueclass-serialization value class serialization} for details.
+ * When the proxy is deserialized it re-constructs and returns the value object.
*
* @spec serialization/index.html Java Object Serialization Specification
* @author Mike Warres
@@ -264,6 +267,16 @@
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants
{
+ private static final String TRACE_DEST =
+ GetPropertyAction.privilegedGetProperty("TRACE");
+
+ static void TRACE(String format, Object... args) {
+ if (TRACE_DEST != null) {
+ var ps = "OUT".equals(TRACE_DEST.toUpperCase(Locale.ROOT)) ? System.out : System.err;
+ ps.println(("TRACE " + format).formatted(args));
+ }
+ }
+
/** handle value representing null */
private static final int NULL_HANDLE = -1;
@@ -473,6 +486,14 @@ protected ObjectInputStream() throws IOException, SecurityException {
* each object (regular or class) read to reconstruct the root object.
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
*
+ *
Serialization and deserialization of value classes is described in
+ * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+ *
+ * @implSpec
+ * When enabled with {@code --enable-preview}, serialization and deserialization of
+ * Core Library value classes migrated from pre-JEP 401 identity classes is
+ * implementation specific.
+ *
*
Exceptions are thrown for problems with the InputStream and for
* classes that should not be deserialized. All exceptions are fatal to
* the InputStream and leave it in an indeterminate state; it is up to the
@@ -606,6 +627,9 @@ protected Object readObjectOverride()
* each object (regular or class) read to reconstruct the root object.
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
*
+ *
Serialization and deserialization of value classes is described in
+ * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+ *
*
ObjectInputStream subclasses which override this method can only be
* constructed in security contexts possessing the
* "enableSubclassImplementation" SerializablePermission; any attempt to
@@ -2259,14 +2283,6 @@ private Object readOrdinaryObject(boolean unshared)
throw new InvalidClassException("invalid class descriptor");
}
- Object obj;
- try {
- obj = desc.isInstantiable() ? desc.newInstance() : null;
- } catch (Exception ex) {
- throw new InvalidClassException(desc.forClass().getName(),
- "unable to create instance", ex);
- }
-
// Assign the handle and initially set to null or the unsharedMarker
passHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
@@ -2274,72 +2290,123 @@ private Object readOrdinaryObject(boolean unshared)
handles.markException(passHandle, resolveEx);
}
- final boolean isRecord = desc.isRecord();
- if (isRecord) {
- assert obj == null;
- obj = readRecord(desc);
- if (!unshared)
- handles.setObject(passHandle, obj);
- } else if (desc.isExternalizable()) {
- if (desc.isValue()) {
- throw new InvalidClassException("Externalizable not valid for value class "
- + cl.getName());
- }
- if (!unshared)
- handles.setObject(passHandle, obj);
- readExternalData((Externalizable) obj, desc);
- } else if (desc.isValue()) {
- if (obj == null) {
- throw new InvalidClassException("Serializable not valid for value class "
- + cl.getName());
- }
- // For value objects, read the fields and finish the buffer before publishing the ref
- readSerialData(obj, desc);
- obj = desc.finishValue(obj);
- if (!unshared)
- handles.setObject(passHandle, obj);
- } else {
- // For all other objects, publish the ref and then read the data
- if (!unshared)
- handles.setObject(passHandle, obj);
- readSerialData(obj, desc);
- }
+ try {
+ // Dispatch on the factory mode to read an object from the stream.
+ Object obj = switch (desc.factoryMode()) {
+ case READ_OBJECT_DEFAULT -> readSerialDefaultObject(desc, unshared);
+ case READ_OBJECT_CUSTOM -> readSerialCustomData(desc, unshared);
+ case READ_RECORD -> readRecord(desc, unshared);
+ case READ_EXTERNALIZABLE -> readExternalObject(desc, unshared);
+ case READ_OBJECT_VALUE -> readObjectValue(desc, unshared);
+ case READ_NO_LOCAL_CLASS -> readAbsentLocalClass(desc, unshared);
+ case null -> throw new AssertionError("Unknown factoryMode for: " + desc.getName(),
+ resolveEx);
+ };
- handles.finish(passHandle);
+ handles.finish(passHandle);
- if (obj != null &&
- handles.lookupException(passHandle) == null &&
- desc.hasReadResolveMethod())
- {
- Object rep = desc.invokeReadResolve(obj);
- if (unshared && rep.getClass().isArray()) {
- rep = cloneArray(rep);
- }
- if (rep != obj) {
- // Filter the replacement object
- if (rep != null) {
- if (rep.getClass().isArray()) {
- filterCheck(rep.getClass(), Array.getLength(rep));
- } else {
- filterCheck(rep.getClass(), -1);
+ if (obj != null &&
+ handles.lookupException(passHandle) == null &&
+ desc.hasReadResolveMethod())
+ {
+ Object rep = desc.invokeReadResolve(obj);
+ if (unshared && rep.getClass().isArray()) {
+ rep = cloneArray(rep);
+ }
+ if (rep != obj) {
+ // Filter the replacement object
+ if (rep != null) {
+ if (rep.getClass().isArray()) {
+ filterCheck(rep.getClass(), Array.getLength(rep));
+ } else {
+ filterCheck(rep.getClass(), -1);
+ }
}
+ handles.setObject(passHandle, obj = rep);
}
- handles.setObject(passHandle, obj = rep);
}
+
+ return obj;
+ } catch (UncheckedIOException uioe) {
+ // Consistent re-throw for nested UncheckedIOExceptions
+ throw uioe.getCause();
}
+ }
- return obj;
+ /**
+ * {@return a value class instance by invoking its constructor with field values read from the stream.
+ * The fields of the class in the stream are matched to the local fields and applied to
+ * the constructor.
+ * If the stream contains superclasses with serializable fields,
+ * an InvalidClassException is thrown with an incompatible class change message.
+ *
+ * @param desc the class descriptor read from the stream, the local class is a value class
+ * @param unshared if the object is not to be shared
+ * @throws InvalidClassException if the stream contains a superclass with serializable fields.
+ * @throws IOException if there are I/O errors while reading from the
+ * underlying {@code InputStream}
+ */
+ private Object readObjectValue(ObjectStreamClass desc, boolean unshared) throws IOException {
+ final ObjectStreamClass localDesc = desc.getLocalDesc();
+ TRACE("readObjectValue: %s, local class: %s", desc.getName(), localDesc.getName());
+ // Check for un-expected fields in superclasses
+ List slots = desc.getClassDataLayout();
+ for (int i = 0; i < slots.size()-1; i++) {
+ ClassDataSlot slot = slots.get(i);
+ if (slot.hasData && slot.desc.getFields(false).length > 0) {
+ throw new InvalidClassException("incompatible class change to value class: " +
+ "stream class has non-empty super type: " + desc.getName());
+ }
+ }
+ // Read values for the value class fields
+ FieldValues fieldValues = new FieldValues(desc, true);
+
+ // Get value object constructor adapted to take primitive value buffer and object array.
+ MethodHandle consMH = ConstructorSupport.deserializationValueCons(desc);
+ try {
+ Object obj = (Object) consMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+ if (!unshared)
+ handles.setObject(passHandle, obj);
+ return obj;
+ } catch (Exception e) {
+ throw new InvalidObjectException(e.getMessage(), e);
+ } catch (Error e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new InvalidObjectException("ReflectiveOperationException " +
+ "during deserialization", t);
+ }
}
/**
- * If obj is non-null, reads externalizable data by invoking readExternal()
+ * Creates a new object and invokes its readExternal method to read its contents.
+ *
+ * If the class is instantiable, read externalizable data by invoking readExternal()
* method of obj; otherwise, attempts to skip over externalizable data.
* Expects that passHandle is set to obj's handle before this method is
- * called.
+ * called. The new object is entered in the handle table immediately,
+ * allowing it to leak before it is completely read.
*/
- private void readExternalData(Externalizable obj, ObjectStreamClass desc)
+ private Object readExternalObject(ObjectStreamClass desc, boolean unshared)
throws IOException
{
+ TRACE("readExternalObject: %s", desc.getName());
+
+ // For Externalizable objects,
+ // create the instance, publish the ref, and read the data
+ Externalizable obj = null;
+ try {
+ if (desc.isInstantiable()) {
+ obj = (Externalizable) desc.newInstance();
+ }
+ } catch (Exception ex) {
+ throw new InvalidClassException(desc.getName(),
+ "unable to create instance", ex);
+ }
+
+ if (!unshared)
+ handles.setObject(passHandle, obj);
+
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
@@ -2383,6 +2450,7 @@ private void readExternalData(Externalizable obj, ObjectStreamClass desc)
* externalizable data remains in the stream, a subsequent read will
* most likely throw a StreamCorruptedException.
*/
+ return obj;
}
/**
@@ -2391,14 +2459,15 @@ private void readExternalData(Externalizable obj, ObjectStreamClass desc)
* mechanism marks the record as having an exception.
* Null is returned from readRecord and later the exception is thrown at
* the exit of {@link #readObject(Class)}.
- **/
- private Object readRecord(ObjectStreamClass desc) throws IOException {
- ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
- if (slots.length != 1) {
+ */
+ private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOException {
+ TRACE("invoking readRecord: %s", desc.getName());
+ List slots = desc.getClassDataLayout();
+ if (slots.size() != 1) {
// skip any superclass stream field values
- for (int i = 0; i < slots.length-1; i++) {
- if (slots[i].hasData) {
- new FieldValues(slots[i].desc, true);
+ for (int i = 0; i < slots.size()-1; i++) {
+ if (slots.get(i).hasData) {
+ new FieldValues(slots.get(i).desc, true);
}
}
}
@@ -2412,10 +2481,13 @@ private Object readRecord(ObjectStreamClass desc) throws IOException {
// - byte[] primValues
// - Object[] objValues
// and return Object
- MethodHandle ctrMH = RecordSupport.deserializationCtr(desc);
+ MethodHandle ctrMH = ConstructorSupport.deserializationCtr(desc);
try {
- return (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+ Object obj = (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+ if (!unshared)
+ handles.setObject(passHandle, obj);
+ return obj;
} catch (Exception e) {
throw new InvalidObjectException(e.getMessage(), e);
} catch (Error e) {
@@ -2427,114 +2499,207 @@ private Object readRecord(ObjectStreamClass desc) throws IOException {
}
/**
- * Reads (or attempts to skip, if obj is null or is tagged with a
- * ClassNotFoundException) instance data for each serializable class of
- * object in stream, from superclass to subclass. Expects that passHandle
- * is set to obj's handle before this method is called.
+ * Construct an object from the stream for a class that has only default read object behaviors.
+ * For each object, the fields are read before any are assigned.
+ * The new instance is entered in the handle table if it is unshared,
+ * allowing it to escape before it is initialized.
+ * The `readObject` and `readObjectNoData` methods are not present and are not called.
+ *
+ * @param desc the class descriptor
+ * @param unshared true if the object should be shared
+ * @return the object constructed from the stream data
+ * @throws IOException if there are I/O errors while reading from the
+ * underlying {@code InputStream}
+ * @throws InvalidClassException if the instance creation fails
*/
- private void readSerialData(Object obj, ObjectStreamClass desc)
- throws IOException
- {
- ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
- // Best effort Failure Atomicity; slotValues will be non-null if field
- // values can be set after reading all field data in the hierarchy.
- // Field values can only be set after reading all data if there are no
- // user observable methods in the hierarchy, readObject(NoData). The
- // top most Serializable class in the hierarchy can be skipped.
- FieldValues[] slotValues = null;
-
- boolean hasSpecialReadMethod = false;
- for (int i = 1; i < slots.length; i++) {
- ObjectStreamClass slotDesc = slots[i].desc;
- if (slotDesc.hasReadObjectMethod()
- || slotDesc.hasReadObjectNoDataMethod()) {
- hasSpecialReadMethod = true;
- break;
+ private Object readSerialDefaultObject(ObjectStreamClass desc, boolean unshared)
+ throws IOException, InvalidClassException {
+ if (!desc.isInstantiable()) {
+ // No local class to create, read and discard
+ return readAbsentLocalClass(desc, unshared);
+ }
+ TRACE("readSerialDefaultObject: %s", desc.getName());
+ try {
+ final Object obj = desc.newInstance();
+ if (!unshared)
+ handles.setObject(passHandle, obj);
+
+ // Best effort Failure Atomicity; slotValues will be non-null if field
+ // values can be set after reading all field data in the hierarchy.
+ List slotValues = desc.getClassDataLayout().stream()
+ .filter(s -> s.hasData)
+ .map(s1 -> {
+ var values = new FieldValues(s1.desc, true);
+ finishBlockData(s1.desc);
+ return values;
+ })
+ .toList();
+
+ if (handles.lookupException(passHandle) != null) {
+ return null; // some exception for a class, do not return the object
}
+
+ // Check that the types are assignable for all slots before assigning.
+ slotValues.forEach(v -> v.defaultCheckFieldValues(obj));
+ slotValues.forEach(v -> v.defaultSetFieldValues(obj));
+ return obj;
+ } catch (InstantiationException | InvocationTargetException ex) {
+ throw new InvalidClassException(desc.forClass().getName(),
+ "unable to create instance", ex);
}
- // No special read methods, can store values and defer setting.
- if (!hasSpecialReadMethod)
- slotValues = new FieldValues[slots.length];
+ }
- for (int i = 0; i < slots.length; i++) {
- ObjectStreamClass slotDesc = slots[i].desc;
- if (slots[i].hasData) {
- if (obj == null || handles.lookupException(passHandle) != null) {
- // Read fields of the current descriptor into a new FieldValues and discard
- new FieldValues(slotDesc, true);
- } else if (slotDesc.hasReadObjectMethod()) {
- SerialCallbackContext oldContext = curContext;
- if (oldContext != null)
- oldContext.check();
- try {
- curContext = new SerialCallbackContext(obj, slotDesc);
+ /**
+ * Reads (or attempts to skip, if not instantiatable or is tagged with a
+ * ClassNotFoundException) instance data for each serializable class of
+ * object in stream, from superclass to subclass.
+ * Expects that passHandle is set to current handle before this method is called.
+ */
+ private Object readSerialCustomData(ObjectStreamClass desc, boolean unshared)
+ throws IOException
+ {
+ if (!desc.isInstantiable()) {
+ // No local class to create, read and discard
+ return readAbsentLocalClass(desc, unshared);
+ }
- bin.setBlockDataMode(true);
- slotDesc.invokeReadObject(obj, this);
- } catch (ClassNotFoundException ex) {
- /*
- * In most cases, the handle table has already
- * propagated a CNFException to passHandle at this
- * point; this mark call is included to address cases
- * where the custom readObject method has cons'ed and
- * thrown a new CNFException of its own.
- */
- handles.markException(passHandle, ex);
- } finally {
- curContext.setUsed();
- if (oldContext!= null)
- oldContext.check();
- curContext = oldContext;
- }
+ TRACE("readSerialCustomData: %s, ex: %s", desc.getName(), handles.lookupException(passHandle));
+ try {
+ Object obj = desc.newInstance();
+ if (!unshared)
+ handles.setObject(passHandle, obj);
+ // Read data into each of the slots for the class
+ return readSerialCustomSlots(obj, desc.getClassDataLayout());
+ } catch (InstantiationException | InvocationTargetException ex) {
+ throw new InvalidClassException(desc.forClass().getName(),
+ "unable to create instance", ex);
+ }
+ }
- /*
- * defaultDataEnd may have been set indirectly by custom
- * readObject() method when calling defaultReadObject() or
- * readFields(); clear it to restore normal read behavior.
- */
- defaultDataEnd = false;
+ /**
+ * Reads from the stream using custom or default readObject methods appropriate.
+ * For each slot, either the custom readObject method or the default reader of fields
+ * is invoked. Unused slot specific custom data is discarded.
+ * This function is used by {@link #readSerialCustomData}.
+ *
+ * @param obj the object to assign the values to
+ * @param slots a list of slots to read from the stream
+ * @return the object being initialized
+ * @throws IOException if there are I/O errors while reading from the
+ * underlying {@code InputStream}
+ */
+ private Object readSerialCustomSlots(Object obj, List slots) throws IOException {
+ TRACE(" readSerialCustomSlots: %s", slots);
+
+ for (ClassDataSlot slot : slots) {
+ ObjectStreamClass slotDesc = slot.desc;
+ if (slot.hasData) {
+ if (slotDesc.hasReadObjectMethod() &&
+ handles.lookupException(passHandle) == null) {
+ // Invoke slot custom readObject method
+ readSlotViaReadObject(obj, slotDesc);
} else {
// Read fields of the current descriptor into a new FieldValues
FieldValues values = new FieldValues(slotDesc, true);
- if (slotValues != null) {
- slotValues[i] = values;
- } else if (obj != null) {
- if (handles.lookupException(passHandle) == null) {
- // passHandle NOT marked with an exception; set field values
- values.defaultCheckFieldValues(obj);
- values.defaultSetFieldValues(obj);
- }
+ if (handles.lookupException(passHandle) == null) {
+ // Set the instance fields if no previous exception
+ values.defaultCheckFieldValues(obj);
+ values.defaultSetFieldValues(obj);
}
- }
-
- if (slotDesc.hasWriteObjectData()) {
- skipCustomData();
- } else {
- bin.setBlockDataMode(false);
+ finishBlockData(slotDesc);
}
} else {
- if (obj != null &&
- slotDesc.hasReadObjectNoDataMethod() &&
- handles.lookupException(passHandle) == null)
- {
+ if (slotDesc.hasReadObjectNoDataMethod() &&
+ handles.lookupException(passHandle) == null) {
slotDesc.invokeReadObjectNoData(obj);
}
}
}
+ return obj;
+ }
- if (obj != null && slotValues != null && handles.lookupException(passHandle) == null) {
- // passHandle NOT marked with an exception
- // Check that the non-primitive types are assignable for all slots
- // before assigning.
- for (int i = 0; i < slots.length; i++) {
- if (slotValues[i] != null)
- slotValues[i].defaultCheckFieldValues(obj);
- }
- for (int i = 0; i < slots.length; i++) {
- if (slotValues[i] != null)
- slotValues[i].defaultSetFieldValues(obj);
+ /**
+ * Invoke the readObject method of the class to read and store the state from the stream.
+ *
+ * @param obj an instance of the class being created, only partially initialized.
+ * @param slotDesc the ObjectStreamDescriptor for the current class
+ * @throws IOException if there are I/O errors while reading from the
+ * underlying {@code InputStream}
+ */
+ private void readSlotViaReadObject(Object obj, ObjectStreamClass slotDesc) throws IOException {
+ TRACE("readSlotViaReadObject: %s", slotDesc.getName());
+ assert obj != null : "readSlotViaReadObject called when obj == null";
+
+ SerialCallbackContext oldContext = curContext;
+ if (oldContext != null)
+ oldContext.check();
+ try {
+ curContext = new SerialCallbackContext(obj, slotDesc);
+
+ bin.setBlockDataMode(true);
+ slotDesc.invokeReadObject(obj, this);
+ } catch (ClassNotFoundException ex) {
+ /*
+ * In most cases, the handle table has already
+ * propagated a CNFException to passHandle at this
+ * point; this mark call is included to address cases
+ * where the custom readObject method has cons'ed and
+ * thrown a new CNFException of its own.
+ */
+ handles.markException(passHandle, ex);
+ } finally {
+ curContext.setUsed();
+ if (oldContext!= null)
+ oldContext.check();
+ curContext = oldContext;
+ }
+
+ /*
+ * defaultDataEnd may have been set indirectly by custom
+ * readObject() method when calling defaultReadObject() or
+ * readFields(); clear it to restore normal read behavior.
+ */
+ defaultDataEnd = false;
+
+ finishBlockData(slotDesc);
+ }
+
+
+ /**
+ * Read and discard an entire object, leaving a null reference in the HandleTable.
+ * The descriptor of the class in the stream is used to read the fields from the stream.
+ * There is no instance in which to store the field values.
+ * Custom data following the fields of any slot is read and discarded.
+ * References to nested objects are read and retained in the
+ * handle table using the regular mechanism.
+ * Handles later in the stream may refer to the nested objects.
+ *
+ * @param desc the stream class descriptor
+ * @param unshared the unshared flag, ignored since no object is created
+ * @return null, no object is created
+ * @throws IOException if there are I/O errors while reading from the
+ * underlying {@code InputStream}
+ */
+ private Object readAbsentLocalClass(ObjectStreamClass desc, boolean unshared)
+ throws IOException {
+ TRACE("readAbsentLocalClass: %s", desc.getName());
+ desc.getClassDataLayout().stream()
+ .filter(s -> s.hasData)
+ .forEach(s2 -> {new FieldValues(s2.desc, true); finishBlockData(s2.desc);});
+ return null;
+ }
+
+ // Finish handling of block data by skipping any remaining and setting BlockDataMode = false
+ private void finishBlockData(ObjectStreamClass slotDesc) throws UncheckedIOException {
+ try {
+ if (slotDesc.hasWriteObjectData()) {
+ skipCustomData();
+ } else {
+ bin.setBlockDataMode(false);
}
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(ioe);
}
}
@@ -2630,32 +2795,38 @@ private final class FieldValues extends GetField {
* @param desc the ObjectStreamClass to read
* @param recordDependencies if true, record the dependencies
* from current PassHandle and the object's read.
+ * @throws UncheckedIOException if any IOException occurs
*/
- FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws IOException {
- this.desc = desc;
+ FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws UncheckedIOException {
+ try {
+ this.desc = desc;
+ TRACE(" reading FieldValues: %s", desc.getName());
+ int primDataSize = desc.getPrimDataSize();
+ primValues = (primDataSize > 0) ? new byte[primDataSize] : null;
+ if (primDataSize > 0) {
+ bin.readFully(primValues, 0, primDataSize, false);
+ }
- int primDataSize = desc.getPrimDataSize();
- primValues = (primDataSize > 0) ? new byte[primDataSize] : null;
- if (primDataSize > 0) {
- bin.readFully(primValues, 0, primDataSize, false);
- }
- int numObjFields = desc.getNumObjFields();
- objValues = (numObjFields > 0) ? new Object[numObjFields] : null;
- objHandles = (numObjFields > 0) ? new int[numObjFields] : null;
- if (numObjFields > 0) {
- int objHandle = passHandle;
- ObjectStreamField[] fields = desc.getFields(false);
- int numPrimFields = fields.length - objValues.length;
- for (int i = 0; i < objValues.length; i++) {
- ObjectStreamField f = fields[numPrimFields + i];
- objValues[i] = readObject0(Object.class, f.isUnshared());
- objHandles[i] = passHandle;
- if (recordDependencies && f.getField() != null) {
- handles.markDependency(objHandle, passHandle);
+ int numObjFields = desc.getNumObjFields();
+ objValues = (numObjFields > 0) ? new Object[numObjFields] : null;
+ objHandles = (numObjFields > 0) ? new int[numObjFields] : null;
+ if (numObjFields > 0) {
+ int objHandle = passHandle;
+ ObjectStreamField[] fields = desc.getFields(false);
+ int numPrimFields = fields.length - objValues.length;
+ for (int i = 0; i < objValues.length; i++) {
+ ObjectStreamField f = fields[numPrimFields + i];
+ objValues[i] = readObject0(Object.class, f.isUnshared());
+ objHandles[i] = passHandle;
+ if (recordDependencies && f.getField() != null) {
+ handles.markDependency(objHandle, passHandle);
+ }
}
+ passHandle = objHandle;
}
- passHandle = objHandle;
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(ioe);
}
}
diff --git a/src/java.base/share/classes/java/io/ObjectOutputStream.java b/src/java.base/share/classes/java/io/ObjectOutputStream.java
index 6ce6a7a3694..65041a305a4 100644
--- a/src/java.base/share/classes/java/io/ObjectOutputStream.java
+++ b/src/java.base/share/classes/java/io/ObjectOutputStream.java
@@ -36,6 +36,8 @@
import jdk.internal.util.ByteArray;
import sun.reflect.misc.ReflectUtil;
+import static java.io.ObjectInputStream.TRACE;
+
/**
* An ObjectOutputStream writes primitive data types and graphs of Java objects
* to an OutputStream. The objects can be read (reconstituted) using an
@@ -158,9 +160,36 @@
* defaultWriteObject and writeFields initially terminate any existing
* block-data record.
*
+ *
*
Records are serialized differently than ordinary serializable or externalizable
* objects, see record serialization.
*
+ *
+ *
Value classes are {@linkplain Serializable} through the use of the serialization proxy pattern.
+ * The serialization protocol does not support a standard serialized form for value classes.
+ * The value class delegates to a serialization proxy by supplying an alternate
+ * record or object to be serialized instead of the value class.
+ * When the proxy is deserialized it re-constructs the value object and returns the value object.
+ * For example,
+ * {@snippet lang="java" :
+ * value class ZipCode implements Serializable { // @highlight substring="value class"
+ * private static final long serialVersionUID = 1L;
+ * private int zipCode;
+ * public ZipCode(int zip) { this.zipCode = zip; }
+ * public int zipCode() { return zipCode; }
+ *
+ * public Object writeReplace() { // @highlight substring="writeReplace"
+ * return new ZipCodeProxy(zipCode);
+ * }
+ *
+ * private record ZipCodeProxy(int zipCode) implements Serializable {
+ * public Object readResolve() { // @highlight substring="readResolve"
+ * return new ZipCode(zipCode);
+ * }
+ * }
+ * }
+ * }
+ *
* @spec serialization/index.html Java Object Serialization Specification
* @author Mike Warres
* @author Roger Riggs
@@ -344,6 +373,9 @@ public void useProtocolVersion(int version) throws IOException {
* object are written transitively so that a complete equivalent graph of
* objects can be reconstructed by an ObjectInputStream.
*
+ *
Serialization and deserialization of value classes is described in
+ * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+ *
*
Exceptions are thrown for problems with the OutputStream and for
* classes that should not be serialized. All exceptions are fatal to the
* OutputStream, which is left in an indeterminate state, and it is up to
@@ -413,6 +445,9 @@ protected void writeObjectOverride(Object obj) throws IOException {
* writeUnshared, and not to any transitively referenced sub-objects in the
* object graph to be serialized.
*
+ *
Serialization and deserialization of value classes is described in
+ * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+ *
*
ObjectOutputStream subclasses which override this method can only be
* constructed in security contexts possessing the
* "enableSubclassImplementation" SerializablePermission; any attempt to
@@ -1198,9 +1233,6 @@ private void writeObject0(Object obj, boolean unshared)
} else if (obj instanceof Enum) {
writeEnum((Enum>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
- if (cl.isValue() && !desc.isInstantiable()) {
- throw new NotSerializableException(cl.getName());
- }
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
@@ -1456,8 +1488,8 @@ private void writeOrdinaryObject(Object obj,
if (desc.isRecord()) {
writeRecordData(obj, desc);
} else if (desc.isExternalizable() && !desc.isProxy()) {
- if (desc.forClass().isValue())
- throw new NotSerializableException("Externalizable not valid for value class "
+ if (desc.isValue())
+ throw new InvalidClassException("Externalizable not valid for value class "
+ desc.forClass().getName());
writeExternalData((Externalizable) obj);
} else {
@@ -1507,10 +1539,10 @@ private void writeRecordData(Object obj, ObjectStreamClass desc)
throws IOException
{
assert obj.getClass().isRecord();
- ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
- if (slots.length != 1) {
+ List slots = desc.getClassDataLayout();
+ if (slots.size() != 1) {
throw new InvalidClassException(
- "expected a single record slot length, but found: " + slots.length);
+ "expected a single record slot length, but found: " + slots.size());
}
defaultWriteFields(obj, desc); // #### seems unnecessary to use the accessors
@@ -1523,9 +1555,9 @@ private void writeRecordData(Object obj, ObjectStreamClass desc)
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
- ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
- for (int i = 0; i < slots.length; i++) {
- ObjectStreamClass slotDesc = slots[i].desc;
+ List slots = desc.getClassDataLayout();
+ for (int i = 0; i < slots.size(); i++) {
+ ObjectStreamClass slotDesc = slots.get(i).desc;
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
diff --git a/src/java.base/share/classes/java/io/ObjectStreamClass.java b/src/java.base/share/classes/java/io/ObjectStreamClass.java
index ef84e36864c..05f045e5e6a 100644
--- a/src/java.base/share/classes/java/io/ObjectStreamClass.java
+++ b/src/java.base/share/classes/java/io/ObjectStreamClass.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -28,7 +28,6 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
-import java.lang.reflect.AccessFlag;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
@@ -54,9 +53,11 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
import jdk.internal.MigratedValueClass;
import jdk.internal.event.SerializationMisdeclarationEvent;
@@ -67,8 +68,11 @@
import jdk.internal.access.SharedSecrets;
import jdk.internal.access.JavaSecurityAccess;
import jdk.internal.util.ByteArray;
+import jdk.internal.value.DeserializeConstructor;
import sun.reflect.misc.ReflectUtil;
+import static java.io.ObjectInputStream.TRACE;
+
/**
* Serialization's descriptor for classes. It contains the name and
* serialVersionUID of the class. The ObjectStreamClass for a specific class
@@ -107,6 +111,71 @@ public final class ObjectStreamClass implements Serializable {
AccessController.doPrivileged(
new ReflectionFactory.GetReflectionFactoryAction());
+ /**
+ * The mode of deserialization for a class depending on its type and interfaces.
+ * The markers used are {@linkplain java.io.Serializable}, {@linkplain java.io.Externalizable},
+ * Class.isRecord(), Class.isValue(), constructors, and
+ * the presence of methods `readObject`, `writeObject`, `readObjectNoData`, `writeObject`.
+ * ObjectInputStream dispatches on the mode to construct objects from the stream.
+ */
+ enum DeserializationMode {
+ /**
+ * Construct an object from the stream for a class that has only default read object behaviors.
+ * All classes and superclasses use defaultReadObject; no custom readObject or readObjectNoData.
+ * The new instance is entered in the handle table if it is unshared,
+ * allowing it to escape before it is initialized.
+ * For each object, all the fields are read before any are assigned.
+ * The `readObject` and `readObjectNoData` methods are not present and are not called.
+ */
+ READ_OBJECT_DEFAULT,
+ /**
+ * Creates a new object and invokes its readExternal method to read its contents.
+ * If the class is instantiable, read externalizable data by invoking readExternal()
+ * method of obj; otherwise, attempts to skip over externalizable data.
+ * Expects that passHandle is set to obj's handle before this method is
+ * called. The new object is entered in the handle table immediately,
+ * allowing it to leak before it is completely read.
+ */
+ READ_EXTERNALIZABLE,
+ /**
+ * Read all the record fields and invoke its canonical constructor.
+ * Construct the record using its canonical constructor.
+ * The new record is entered in the handle table only after the constructor returns.
+ */
+ READ_RECORD,
+ /**
+ * Fully custom read from the stream to create an instance.
+ * If the class is not instantiatable or is tagged with ClassNotFoundException
+ * the data in the stream for the class is read and discarded. {@link #READ_NO_LOCAL_CLASS}
+ * The instance is created and set in the handle table, allowing it to leak before it is initialized.
+ * For each serializable class in the stream, from superclass to subclass the
+ * stream values are read by the `readObject` method, if present, or defaultReadObject.
+ * Custom inline data is discarded if not consumed by the class `readObject` method.
+ */
+ READ_OBJECT_CUSTOM,
+ /**
+ * Construct an object by reading the values of all fields and
+ * invoking a constructor or static factory method.
+ * The constructor or static factory method is selected by matching its parameters with the
+ * sequence of field types of the serializable fields of the local class and superclasses.
+ * Invoke the constructor with all the values from the stream, inserting
+ * defaults and dropping extra values as necessary.
+ * This is very similar to the reading of records, except for the identification of
+ * the constructor or static factory.
+ */
+ READ_OBJECT_VALUE,
+ /**
+ * Read and discard an entire object, leaving a null reference in the HandleTable.
+ * The descriptor of the class in the stream is used to read the fields from the stream.
+ * There is no instance in which to store the field values.
+ * Custom data following the fields of any slot is read and discarded.
+ * References to nested objects are read and retained in the
+ * handle table using the regular mechanism.
+ * Handles later in the stream may refer to the nested objects.
+ */
+ READ_NO_LOCAL_CLASS,
+ }
+
private static class Caches {
/** cache mapping local classes -> descriptors */
static final ClassCache localDescs =
@@ -142,6 +211,8 @@ protected Map computeValue(Class> type) {
private boolean isRecord;
/** true if represents a value class */
private boolean isValue;
+ /** The DeserializationMode for this class. */
+ private DeserializationMode factoryMode;
/** true if represented class implements Serializable */
private boolean serializable;
/** true if represented class implements Externalizable */
@@ -199,7 +270,7 @@ InvalidClassException newInvalidClassException() {
/** reflector for setting/getting serializable field values */
private FieldReflector fieldRefl;
/** data layout of serialized objects described by this class desc */
- private volatile ClassDataSlot[] dataLayout;
+ private volatile List dataLayout;
/** serialization-appropriate constructor, or null if none */
private Constructor> cons;
@@ -413,13 +484,34 @@ public Void run() {
}
if (isRecord) {
+ factoryMode = DeserializationMode.READ_RECORD;
canonicalCtr = canonicalRecordCtr(cl);
deserializationCtrs = new DeserializationConstructorsCache();
- } else if (isValue) {
- // Value object instance creation is specialized in newInstance()
- cons = null;
} else if (externalizable) {
- cons = getExternalizableConstructor(cl);
+ factoryMode = DeserializationMode.READ_EXTERNALIZABLE;
+ if (cl.isIdentity()) {
+ cons = getExternalizableConstructor(cl);
+ } else {
+ serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+ "Externalizable not valid for value class");
+ }
+ } else if (cl.isValue()) {
+ factoryMode = DeserializationMode.READ_OBJECT_VALUE;
+ if (!cl.isAnnotationPresent(MigratedValueClass.class)) {
+ serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+ "Value class serialization is only supported with `writeReplace`");
+ } else if (Modifier.isAbstract(cl.getModifiers())) {
+ serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+ "value class is abstract");
+ } else {
+ // Value classes should have constructor(s) annotated with {@link DeserializeConstructor}
+ canonicalCtr = getDeserializingValueCons(cl, fields);
+ deserializationCtrs = new DeserializationConstructorsCache(); factoryMode = DeserializationMode.READ_OBJECT_VALUE;
+ if (canonicalCtr == null) {
+ serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+ "no constructor or factory found for migrated value class");
+ }
+ }
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
@@ -431,6 +523,10 @@ public Void run() {
readObjectNoDataMethod = getPrivateMethod(
cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
+ factoryMode = ((superDesc == null || superDesc.factoryMode() == DeserializationMode.READ_OBJECT_DEFAULT)
+ && readObjectMethod == null && readObjectNoDataMethod == null)
+ ? DeserializationMode.READ_OBJECT_DEFAULT
+ : DeserializationMode.READ_OBJECT_CUSTOM;
}
domains = getProtectionDomains(cons, cl);
writeReplaceMethod = getInheritableMethod(
@@ -575,6 +671,9 @@ void initProxy(Class> cl,
deserializeEx = localDesc.deserializeEx;
domains = localDesc.domains;
cons = localDesc.cons;
+ factoryMode = localDesc.factoryMode;
+ } else {
+ factoryMode = DeserializationMode.READ_OBJECT_DEFAULT;
}
fieldRefl = getReflector(fields, localDesc);
initialized = true;
@@ -669,6 +768,12 @@ void initNonProxy(ObjectStreamClass model,
domains = localDesc.domains;
assert cl.isRecord() ? localDesc.cons == null : true;
cons = localDesc.cons;
+ factoryMode = localDesc.factoryMode;
+ } else {
+ // No local class, read data using only the schema from the stream
+ factoryMode = (externalizable)
+ ? DeserializationMode.READ_EXTERNALIZABLE
+ : DeserializationMode.READ_NO_LOCAL_CLASS;
}
fieldRefl = getReflector(fields, localDesc);
@@ -724,7 +829,7 @@ void readNonProxy(ObjectInputStream in)
String signature = ((tcode == 'L') || (tcode == '[')) ?
in.readTypeString() : String.valueOf(tcode);
try {
- fields[i] = new ObjectStreamField(fname, signature, false);
+ fields[i] = new ObjectStreamField(fname, signature, false, -1);
} catch (RuntimeException e) {
throw new InvalidClassException(name,
"invalid descriptor for field " +
@@ -943,6 +1048,14 @@ boolean isValue() {
return isValue;
}
+ /**
+ * {@return the factory mode for deserialization}
+ */
+ DeserializationMode factoryMode() {
+ requireInitialized();
+ return factoryMode;
+ }
+
/**
* Returns true if class descriptor represents externalizable class that
* has written its data in 1.2 (block data) format, false otherwise.
@@ -967,13 +1080,13 @@ boolean hasWriteObjectData() {
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, if it is
* non-externalizable and its first non-serializable superclass defines an
- * accessible no-arg constructor, or if the class is a migrated value class.
+ * accessible no-arg constructor, or if the class is a value class with a @DeserializeConstructor
+ * constructor or static factory.
* Otherwise, returns false.
*/
boolean isInstantiable() {
requireInitialized();
- return (cons != null |
- (isValue && cl != null && cl.isAnnotationPresent(jdk.internal.MigratedValueClass.class)));
+ return (cons != null || (isValue() && canonicalCtr != null));
}
/**
@@ -1084,23 +1197,11 @@ Object newInstance()
ex.initCause(err);
throw ex;
}
- } else if (isValue) {
- // Start with a buffered default value.
- return FieldReflector.newValueInstance(cl);
- } else {
+ } else {
throw new UnsupportedOperationException();
}
}
- /**
- * Finish the initialization of a value object.
- * @param obj an object (larval if a value object)
- * @return the finished object
- */
- Object finishValue(Object obj) {
- return (isValue) ? FieldReflector.finishValueInstance(obj) : obj;
- }
-
/**
* Invokes the writeObject method of the represented serializable class.
* Throws UnsupportedOperationException if this class descriptor is not
@@ -1271,23 +1372,18 @@ static class ClassDataSlot {
}
/**
- * Returns array of ClassDataSlot instances representing the data layout
+ * Returns a List of ClassDataSlot instances representing the data layout
* (including superclass data) for serialized objects described by this
* class descriptor. ClassDataSlots are ordered by inheritance with those
* containing "higher" superclasses appearing first. The final
* ClassDataSlot contains a reference to this descriptor.
*/
- ClassDataSlot[] getClassDataLayout() throws InvalidClassException {
+ List getClassDataLayout() throws InvalidClassException {
// REMIND: synchronize instead of relying on volatile?
- if (dataLayout == null) {
- dataLayout = getClassDataLayout0();
- }
- return dataLayout;
- }
+ List layout = dataLayout;
+ if (layout != null)
+ return layout;
- private ClassDataSlot[] getClassDataLayout0()
- throws InvalidClassException
- {
ArrayList slots = new ArrayList<>();
Class> start = cl, end = cl;
@@ -1336,7 +1432,8 @@ private ClassDataSlot[] getClassDataLayout0()
// order slots from superclass -> subclass
Collections.reverse(slots);
- return slots.toArray(new ClassDataSlot[slots.size()]);
+ dataLayout = slots;
+ return slots;
}
/**
@@ -1464,6 +1561,66 @@ private ObjectStreamClass getVariantFor(Class> cl)
return desc;
}
+ /**
+ * Return a method handle for the static method or constructor(s) that matches the
+ * serializable fields and annotated with {@link DeserializeConstructor}.
+ * The descriptor for the class is still being initialized, so is passed the fields needed.
+ * @param clazz The class to query
+ * @param fields the serializable fields of the class
+ * @return a MethodHandle, null if none found
+ */
+ @SuppressWarnings("unchecked")
+ private static MethodHandle getDeserializingValueCons(Class> clazz,
+ ObjectStreamField[] fields) {
+ // Search for annotated static factory in methods or constructors
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ MethodHandle mh = Stream.concat(
+ Arrays.stream(clazz.getDeclaredMethods()).filter(m -> Modifier.isStatic(m.getModifiers())),
+ Arrays.stream(clazz.getDeclaredConstructors()))
+ .filter(m -> m.isAnnotationPresent(DeserializeConstructor.class))
+ .map(m -> {
+ try {
+ m.setAccessible(true);
+ return (m instanceof Constructor> cons)
+ ? lookup.unreflectConstructor(cons)
+ : lookup.unreflect(((Method) m));
+ } catch (IllegalAccessException iae) {
+ throw new InternalError(iae); // should not occur after setAccessible
+ }})
+ .filter(m -> matchFactoryParamTypes(clazz, m, fields))
+ .findFirst().orElse(null);
+ TRACE("DeserializeConstructor for %s, mh: %s", clazz, mh);
+ return mh;
+ }
+
+ /**
+ * Check that the parameters of the factory method match the fields of this class.
+ *
+ * @param mh a MethodHandle for a constructor or factory
+ * @return true if all fields match the parameters, false if not
+ */
+ private static boolean matchFactoryParamTypes(Class> clazz,
+ MethodHandle mh,
+ ObjectStreamField[] fields) {
+ TRACE(" matchFactoryParams checking class: %s, mh: %s", clazz, mh);
+ var params = mh.type().parameterList();
+ if (params.size() != fields.length) {
+ TRACE(" matchFactoryParams %s, arg count mismatch %d params != %d fields",
+ clazz, params.size(), fields.length);
+ return false; // Mismatch in count of fields and parameters
+ }
+ for (ObjectStreamField field : fields) {
+ int argIndex = field.getArgIndex();
+ final Class> paramtype = params.get(argIndex);
+ if (!field.getType().equals(paramtype)) {
+ TRACE(" matchFactoryParams %s: argIndex: %d type mismatch field: %s != param: %s",
+ clazz, argIndex, field.getType(), paramtype);
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Returns public no-arg constructor of given class, or null if none found.
* Access checks are disabled on the returned constructor (if any), since
@@ -1710,14 +1867,12 @@ private static ObjectStreamField[] getDeclaredSerialFields(Class> cl)
if ((f.getType() == spf.getType()) &&
((f.getModifiers() & Modifier.STATIC) == 0))
{
- boundFields[i] =
- new ObjectStreamField(f, spf.isUnshared(), true);
+ boundFields[i] = new ObjectStreamField(f, spf.isUnshared(), true, i);
}
} catch (NoSuchFieldException ex) {
}
if (boundFields[i] == null) {
- boundFields[i] = new ObjectStreamField(
- fname, spf.getType(), spf.isUnshared());
+ boundFields[i] = new ObjectStreamField(fname, spf.getType(), spf.isUnshared(), i);
}
}
return boundFields;
@@ -1734,9 +1889,9 @@ private static ObjectStreamField[] getDefaultSerialFields(Class> cl) {
ArrayList list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
- for (int i = 0; i < clFields.length; i++) {
+ for (int i = 0, argIndex = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
- list.add(new ObjectStreamField(clFields[i], false, true));
+ list.add(new ObjectStreamField(clFields[i], false, true, argIndex++));
}
}
int size = list.size();
@@ -1968,33 +2123,6 @@ private static final class FieldReflector {
/** field types */
private final Class>[] types;
- /**
- * Return a new instance of the class using Unsafe.uninitializedDefaultValue
- * and buffer it.
- * @param clazz The value class
- * @return a buffered default value
- */
- static Object newValueInstance(Class> clazz) throws InstantiationException{
- var accessFlags = clazz.accessFlags();
- if (accessFlags.contains(AccessFlag.ABSTRACT) ||
- accessFlags.contains(AccessFlag.IDENTITY)) {
- throw new InstantiationException("Value class not instantiable: " + clazz.getName());
- }
- // may not be implicitly constructible; so allocate with Unsafe
- Object obj = UNSAFE.uninitializedDefaultValue(clazz);
- return UNSAFE.makePrivateBuffer(obj);
- }
-
- /**
- * Finish a value object, clear the larval state and returning the value object.
- * @param obj a buffered value object in a larval state
- * @return the finished value object
- */
- static Object finishValueInstance(Object obj) {
- assert (obj.getClass().isValue()) : "Should be a value class";
- return UNSAFE.finishPrivateBuffer(obj);
- }
-
/**
* Constructs FieldReflector capable of setting/getting values from the
* subset of fields whose ObjectStreamFields contain non-null
@@ -2147,7 +2275,7 @@ void setObjFieldValues(Object obj, Object[] vals) {
}
private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) {
- if (obj == null) {
+ if (obj == null && !dryRun) {
throw new NullPointerException();
}
for (int i = numPrimFields; i < fields.length; i++) {
@@ -2288,16 +2416,16 @@ private static ObjectStreamField[] matchFields(ObjectStreamField[] fields,
}
if (lf.getField() != null) {
m = new ObjectStreamField(
- lf.getField(), lf.isUnshared(), false);
+ lf.getField(), lf.isUnshared(), true, lf.getArgIndex()); // Don't hide type
} else {
m = new ObjectStreamField(
- lf.getName(), lf.getSignature(), lf.isUnshared());
+ lf.getName(), lf.getSignature(), lf.isUnshared(), lf.getArgIndex());
}
}
}
if (m == null) {
m = new ObjectStreamField(
- f.getName(), f.getSignature(), false);
+ f.getName(), f.getSignature(), false, -1);
}
m.setOffset(f.getOffset());
matches[i] = m;
@@ -2419,7 +2547,7 @@ static final class Impl extends Key {
}
/** Record specific support for retrieving and binding stream field values. */
- static final class RecordSupport {
+ static final class ConstructorSupport {
/**
* Returns canonical record constructor adapted to take two arguments:
* {@code (byte[] primValues, Object[] objValues)}
@@ -2476,6 +2604,65 @@ static MethodHandle deserializationCtr(ObjectStreamClass desc) {
desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
}
+ /**
+ * Returns value object constructor adapted to take two arguments:
+ * {@code (byte[] primValues, Object[] objValues)} and return {@code Object}
+ */
+ static MethodHandle deserializationValueCons(ObjectStreamClass desc) {
+ // check the cached value 1st
+ MethodHandle mh = desc.deserializationCtr;
+ if (mh != null) return mh;
+ mh = desc.deserializationCtrs.get(desc.getFields(false));
+ if (mh != null) return desc.deserializationCtr = mh;
+
+ // retrieve the selected constructor
+ // (T1, T2, ..., Tn):TR
+ ObjectStreamClass localDesc = desc.localDesc;
+ mh = localDesc.canonicalCtr;
+ MethodType mt = mh.type();
+
+ // change return type to Object
+ // (T1, T2, ..., Tn):TR -> (T1, T2, ..., Tn):Object
+ mh = mh.asType(mh.type().changeReturnType(Object.class));
+
+ // drop last 2 arguments representing primValues and objValues arrays
+ // (T1, T2, ..., Tn):Object -> (T1, T2, ..., Tn, byte[], Object[]):Object
+ mh = MethodHandles.dropArguments(mh, mh.type().parameterCount(), byte[].class, Object[].class);
+
+ Class>[] params = mt.parameterArray();
+ for (int i = params.length-1; i >= 0; i--) {
+ // Get the name from the local descriptor matching the argIndex
+ var field = getFieldForArgIndex(localDesc, i);
+ String name = (field == null) ? "" : field.getName(); // empty string to supply default
+ Class> type = params[i];
+ // obtain stream field extractor that extracts argument at
+ // position i (Ti+1) from primValues and objValues arrays
+ // (byte[], Object[]):Ti+1
+ MethodHandle combiner = streamFieldExtractor(name, type, desc);
+ // fold byte[] privValues and Object[] objValues into argument at position i (Ti+1)
+ // (..., Ti, Ti+1, byte[], Object[]):Object -> (..., Ti, byte[], Object[]):Object
+ mh = MethodHandles.foldArguments(mh, i, combiner);
+ }
+ // what we are left with is a MethodHandle taking just the primValues
+ // and objValues arrays and returning the constructed instance
+ // (byte[], Object[]):Object
+
+ // store it into cache and return the 1st value stored
+ return desc.deserializationCtr =
+ desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
+ }
+
+ // Find the ObjectStreamField for the argument index, otherwise null
+ private static ObjectStreamField getFieldForArgIndex(ObjectStreamClass desc, int argIndex) {
+ for (var field : desc.fields) {
+ if (field.getArgIndex() == argIndex)
+ return field;
+ }
+ TRACE("field for ArgIndex is null: %s, index: %d, fields: %s",
+ desc, argIndex, Arrays.toString(desc.fields));
+ return null;
+ }
+
/** Returns the number of primitive fields for the given descriptor. */
private static int numberPrimValues(ObjectStreamClass desc) {
ObjectStreamField[] fields = desc.getFields();
diff --git a/src/java.base/share/classes/java/io/ObjectStreamField.java b/src/java.base/share/classes/java/io/ObjectStreamField.java
index 75c955440c1..fd9fbc8728c 100644
--- a/src/java.base/share/classes/java/io/ObjectStreamField.java
+++ b/src/java.base/share/classes/java/io/ObjectStreamField.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -57,6 +57,8 @@ public class ObjectStreamField
private final Field field;
/** offset of field value in enclosing field group */
private int offset;
+ /** index of the field in the class, retain the declaration order of serializable fields */
+ private final int argIndex;
/**
* Create a Serializable field with the specified type. This field should
@@ -87,6 +89,11 @@ public ObjectStreamField(String name, Class> type) {
* @since 1.4
*/
public ObjectStreamField(String name, Class> type, boolean unshared) {
+ this(name, type, unshared, -1);
+ }
+
+ /* package-private */
+ ObjectStreamField(String name, Class> type, boolean unshared, int argIndex) {
if (name == null) {
throw new NullPointerException();
}
@@ -95,13 +102,14 @@ public ObjectStreamField(String name, Class> type, boolean unshared) {
this.unshared = unshared;
this.field = null;
this.signature = null;
+ this.argIndex = argIndex;
}
/**
* Creates an ObjectStreamField representing a field with the given name,
* signature and unshared setting.
*/
- ObjectStreamField(String name, String signature, boolean unshared) {
+ ObjectStreamField(String name, String signature, boolean unshared, int argIndex) {
if (name == null) {
throw new NullPointerException();
}
@@ -109,6 +117,7 @@ public ObjectStreamField(String name, Class> type, boolean unshared) {
this.signature = signature.intern();
this.unshared = unshared;
this.field = null;
+ this.argIndex = argIndex;
type = switch (signature.charAt(0)) {
case 'Z' -> Boolean.TYPE;
@@ -132,13 +141,14 @@ public ObjectStreamField(String name, Class> type, boolean unshared) {
* ObjectStreamField (if non-primitive) will return Object.class (as
* opposed to a more specific reference type).
*/
- ObjectStreamField(Field field, boolean unshared, boolean showType) {
+ ObjectStreamField(Field field, boolean unshared, boolean showType, int argIndex) {
this.field = field;
this.unshared = unshared;
name = field.getName();
Class> ftype = field.getType();
type = (showType || ftype.isPrimitive()) ? ftype : Object.class;
signature = ftype.descriptorString().intern();
+ this.argIndex = argIndex;
}
/**
@@ -227,6 +237,13 @@ protected void setOffset(int offset) {
this.offset = offset;
}
+ /**
+ * {@return Index of the field in the sequence of Serializable fields}
+ */
+ int getArgIndex() {
+ return argIndex;
+ }
+
/**
* Return true if this field has a primitive type.
*
diff --git a/src/java.base/share/classes/java/lang/Boolean.java b/src/java.base/share/classes/java/lang/Boolean.java
index 27211ec783e..149c704dddf 100644
--- a/src/java.base/share/classes/java/lang/Boolean.java
+++ b/src/java.base/share/classes/java/lang/Boolean.java
@@ -25,17 +25,14 @@
package java.lang;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import java.lang.constant.Constable;
-import java.lang.constant.ConstantDesc;
import java.lang.constant.ConstantDescs;
import java.lang.constant.DynamicConstantDesc;
import java.util.Optional;
-import static java.lang.constant.ConstantDescs.BSM_GET_STATIC_FINAL;
-import static java.lang.constant.ConstantDescs.CD_Boolean;
-
/**
* The Boolean class wraps a value of the primitive type
* {@code boolean} in an object. An object of type
@@ -176,6 +173,7 @@ public boolean booleanValue() {
* @since 1.4
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
diff --git a/src/java.base/share/classes/java/lang/Byte.java b/src/java.base/share/classes/java/lang/Byte.java
index f900f534690..d7ae473455b 100644
--- a/src/java.base/share/classes/java/lang/Byte.java
+++ b/src/java.base/share/classes/java/lang/Byte.java
@@ -26,6 +26,7 @@
package java.lang;
import jdk.internal.misc.CDS;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import jdk.internal.vm.annotation.Stable;
@@ -35,7 +36,6 @@
import static java.lang.constant.ConstantDescs.BSM_EXPLICIT_CAST;
import static java.lang.constant.ConstantDescs.CD_byte;
-import static java.lang.constant.ConstantDescs.CD_int;
import static java.lang.constant.ConstantDescs.DEFAULT_NAME;
/**
@@ -145,6 +145,7 @@ private ByteCache() {}
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
diff --git a/src/java.base/share/classes/java/lang/Character.java b/src/java.base/share/classes/java/lang/Character.java
index d55c30a0b11..b3299f03a5e 100644
--- a/src/java.base/share/classes/java/lang/Character.java
+++ b/src/java.base/share/classes/java/lang/Character.java
@@ -26,6 +26,7 @@
package java.lang;
import jdk.internal.misc.CDS;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import jdk.internal.vm.annotation.Stable;
@@ -9014,6 +9015,7 @@ private CharacterCache(){}
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
diff --git a/src/java.base/share/classes/java/lang/Double.java b/src/java.base/share/classes/java/lang/Double.java
index 5cd02386d9b..edca1de8c0d 100644
--- a/src/java.base/share/classes/java/lang/Double.java
+++ b/src/java.base/share/classes/java/lang/Double.java
@@ -33,6 +33,7 @@
import jdk.internal.math.FloatingDecimal;
import jdk.internal.math.DoubleConsts;
import jdk.internal.math.DoubleToDecimal;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.IntrinsicCandidate;
/**
@@ -923,6 +924,7 @@ public static Double valueOf(String s) throws NumberFormatException {
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Double valueOf(double d) {
return new Double(d);
}
diff --git a/src/java.base/share/classes/java/lang/Float.java b/src/java.base/share/classes/java/lang/Float.java
index 8b1b1be2748..1457b1218a1 100644
--- a/src/java.base/share/classes/java/lang/Float.java
+++ b/src/java.base/share/classes/java/lang/Float.java
@@ -33,6 +33,7 @@
import jdk.internal.math.FloatConsts;
import jdk.internal.math.FloatingDecimal;
import jdk.internal.math.FloatToDecimal;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.IntrinsicCandidate;
/**
@@ -550,6 +551,7 @@ public static Float valueOf(String s) throws NumberFormatException {
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Float valueOf(float f) {
return new Float(f);
}
diff --git a/src/java.base/share/classes/java/lang/Integer.java b/src/java.base/share/classes/java/lang/Integer.java
index 92d48c3d78b..4fee928497b 100644
--- a/src/java.base/share/classes/java/lang/Integer.java
+++ b/src/java.base/share/classes/java/lang/Integer.java
@@ -27,6 +27,7 @@
import jdk.internal.misc.CDS;
import jdk.internal.misc.VM;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import jdk.internal.vm.annotation.Stable;
@@ -1014,6 +1015,7 @@ private IntegerCache() {}
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
diff --git a/src/java.base/share/classes/java/lang/Long.java b/src/java.base/share/classes/java/lang/Long.java
index 2b5211dad7a..f4bb60fa973 100644
--- a/src/java.base/share/classes/java/lang/Long.java
+++ b/src/java.base/share/classes/java/lang/Long.java
@@ -34,6 +34,7 @@
import java.util.Optional;
import jdk.internal.misc.CDS;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import jdk.internal.vm.annotation.Stable;
@@ -1016,6 +1017,7 @@ private LongCache() {}
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
diff --git a/src/java.base/share/classes/java/lang/Number.java b/src/java.base/share/classes/java/lang/Number.java
index d0ebec608da..ae6537fad6e 100644
--- a/src/java.base/share/classes/java/lang/Number.java
+++ b/src/java.base/share/classes/java/lang/Number.java
@@ -24,7 +24,6 @@
*/
package java.lang;
-
/**
* The abstract class {@code Number} is the superclass of platform
* classes representing numeric values that are convertible to the
diff --git a/src/java.base/share/classes/java/lang/Short.java b/src/java.base/share/classes/java/lang/Short.java
index 6f26a08971a..d7f0d0be017 100644
--- a/src/java.base/share/classes/java/lang/Short.java
+++ b/src/java.base/share/classes/java/lang/Short.java
@@ -26,6 +26,7 @@
package java.lang;
import jdk.internal.misc.CDS;
+import jdk.internal.value.DeserializeConstructor;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import jdk.internal.vm.annotation.Stable;
@@ -34,7 +35,6 @@
import java.util.Optional;
import static java.lang.constant.ConstantDescs.BSM_EXPLICIT_CAST;
-import static java.lang.constant.ConstantDescs.CD_int;
import static java.lang.constant.ConstantDescs.CD_short;
import static java.lang.constant.ConstantDescs.DEFAULT_NAME;
@@ -274,6 +274,7 @@ private ShortCache() {}
* @since 1.5
*/
@IntrinsicCandidate
+ @DeserializeConstructor
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
diff --git a/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java b/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java
index 696a0b4a8a2..724d9fc7002 100644
--- a/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java
+++ b/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java
@@ -351,7 +351,7 @@ private boolean superHasAccessibleConstructor(Class> cl) {
*/
public final Constructor> newConstructorForSerialization(Class> cl) {
if (cl.isValue()) {
- throw new UnsupportedOperationException("newConstructorForSerialization does not support value classes");
+ throw new UnsupportedOperationException("newConstructorForSerialization does not support value classes: " + cl);
}
Class> initCl = cl;
diff --git a/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java b/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java
new file mode 100644
index 00000000000..f1acff12831
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.internal.value;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+
+/**
+ * Indicates the constructor or static factory to
+ * construct a value object during deserialization.
+ * The annotation is used by java.io.ObjectStreamClass to select the constructor
+ * or factory method to create objects from a stream.
+ *
+ * @since 24
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value={CONSTRUCTOR, METHOD})
+public @interface DeserializeConstructor {
+}
diff --git a/test/jdk/java/io/Serializable/valueObjects/SerializeAllValueClasses.java b/test/jdk/java/io/Serializable/valueObjects/SerializeAllValueClasses.java
new file mode 100644
index 00000000000..7a7b3d826e1
--- /dev/null
+++ b/test/jdk/java/io/Serializable/valueObjects/SerializeAllValueClasses.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules java.base/jdk.internal java.base/jdk.internal.misc
+ * @run junit/othervm --enable-preview SerializeAllValueClasses
+ * @run junit/othervm SerializeAllValueClasses
+ */
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Month;
+import java.time.ZonedDateTime;
+import java.time.chrono.HijrahDate;
+import java.time.chrono.JapaneseDate;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalUnit;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import jdk.internal.misc.PreviewFeatures;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Scans all classes in the JDK for those recognized as value classes
+ * or with the annotation jdk.internal.misc.ValueBasedClass.
+ *
+ * Scanning is done over the jrt: filesystem. Classes are matched using the
+ * following criteria:
+ *
+ * - serializable
+ * - is a public or protected class
+ * - has public or protected constructor
+ *
+ * This returns a list of class', which is convenient for the caller.
+ */
+
+public class SerializeAllValueClasses {
+ // Cache of instances of known classes suitable as arguments to constructors
+ // or factory methods.
+ private static final Map, Object> argumentForType = initInstances();
+
+ private static Map, Object> initInstances() {
+ Map, Object> map = new HashMap<>();
+ map.put(Integer.class, 12); map.put(int.class, 12);
+ map.put(Short.class, (short)3); map.put(short.class, (short)3);
+ map.put(Byte.class, (byte)4); map.put(byte.class, (byte)4);
+ map.put(Long.class, 5L); map.put(long.class, 5L);
+ map.put(Character.class, 'C'); map.put(char.class, 'C');
+ map.put(Float.class, 1.0f); map.put(float.class, 1.0f);
+ map.put(Double.class, 2.0d); map.put(double.class, 2.0d);
+ map.put(Duration.class, Duration.ofHours(1));
+ map.put(TemporalUnit.class, ChronoUnit.SECONDS);
+ map.put(LocalTime.class, LocalTime.of(12, 1));
+ map.put(LocalDate.class, LocalDate.of(2024, 1, 1));
+ map.put(LocalDateTime.class, LocalDateTime.of(2024, 2, 1, 12, 2));
+ map.put(TemporalAccessor.class, ZonedDateTime.now());
+ map.put(ZonedDateTime.class, ZonedDateTime.now());
+ map.put(Clock.class, Clock.systemUTC());
+ map.put(Month.class, Month.JANUARY);
+ map.put(Instant.class, Instant.now());
+ map.put(JapaneseDate.class, JapaneseDate.now());
+ map.put(HijrahDate.class, HijrahDate.now());
+ return map;
+ }
+
+
+ // Stream the value classes to the test
+ private static Stream classProvider() throws IOException, URISyntaxException {
+ return findAll().stream().map(c -> Arguments.of(c));
+ }
+
+ @Test
+ void info() {
+ var info = (PreviewFeatures.isEnabled()) ? " Checking preview classes declared as `value class`" :
+ " Checking identity classes with annotation `jdk.internal.ValueBased.class`";
+ System.err.println(info);
+ }
+
+ @ParameterizedTest
+ @MethodSource("classProvider")
+ void testValueClass(Class> clazz) {
+ boolean atLeastOne = false;
+
+ Object expected = argumentForType.get(clazz);
+ if (expected != null) {
+ serializeDeserialize(expected);
+ atLeastOne = true;
+ }
+ var cons = clazz.getConstructors();
+ for (Constructor> c : cons) {
+ Object[] args = makeArgs(c.getParameterTypes(), clazz);
+ if (args != null) {
+ try {
+ expected = c.newInstance(args);
+ serializeDeserialize(expected);
+ atLeastOne = true;
+ break; // one is enough
+ } catch (InvocationTargetException | InstantiationException |
+ IllegalAccessException e) {
+ // Ignore
+ System.err.printf("""
+ Ignoring constructor: %s
+ Generated arguments are invalid: %s
+ %s
+ """,
+ c, Arrays.toString(args), e.getCause());
+ }
+ }
+ }
+
+ // Scan for suitable factory methods
+ for (Method m : clazz.getMethods()) {
+ if (Modifier.isStatic(m.getModifiers()) &&
+ m.getReturnType().equals(clazz)) {
+ // static method returning itself
+ Object[] args = makeArgs(m.getParameterTypes(), clazz);
+ if (args != null) {
+ try {
+ expected = m.invoke(null, args);
+ serializeDeserialize(expected);
+ atLeastOne = true;
+ break; // one is enough
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ // Ignore
+ System.err.printf("""
+ Ignoring factory: %s
+ Generated arguments are invalid: %s
+ %s
+ """,
+ m, Arrays.toString(args), e.getCause());
+ }
+ }
+ }
+ }
+ assertTrue(atLeastOne, "No constructor or factory found for " + clazz);
+ }
+
+ /**
+ * {@return an array of instances matching the parameter types, or null}
+ *
+ * @param paramTypes an array of parameter types
+ * @param forClazz the owner class for which the parameters are being generated
+ */
+ private Object[] makeArgs(Class>[] paramTypes, Class> forClazz) {
+ Object[] args = Arrays.stream(paramTypes)
+ .map(t -> makeArg(t, forClazz))
+ .toArray();
+ for (Object arg : args) {
+ if (arg == null)
+ return null;
+ }
+ return args;
+ }
+
+ /**
+ * {@return an instance of the class, or null if not available}
+ * String values are customized by the requesting owner.
+ * For example, "true" is returned as a value when requested for "Boolean".
+ * @param paramType the parameter type
+ * @param forClass the owner class
+ */
+ private static Object makeArg(Class> paramType, Class> forClass) {
+ return (paramType == String.class || paramType == CharSequence.class)
+ ? makeStringArg(forClass)
+ : argumentForType.get(paramType);
+ }
+
+ /**
+ * {@return a string representation of an instance of class, or null}
+ * Mostly special cased for core value classes.
+ * @param forClass a Class
+ */
+ private static String makeStringArg(Class> forClass) {
+ if (forClass == Integer.class || forClass == int.class ||
+ forClass == Byte.class || forClass == byte.class ||
+ forClass == Short.class || forClass == short.class ||
+ forClass == Long.class || forClass == long.class) {
+ return "0";
+ } else if (forClass == Boolean.class || forClass == boolean.class) {
+ return "true";
+ } else if (forClass == Float.class || forClass == float.class ||
+ forClass == Double.class || forClass == double.class) {
+ return "1.0";
+ } else if (forClass == Duration.class) {
+ return "PT4H";
+ } else if (forClass == LocalDate.class) {
+ return LocalDate.of(2024, 1, 1).toString();
+ } else if (forClass == LocalDateTime.class) {
+ return LocalDateTime.of(2024, 1, 1, 12, 1).toString();
+ } else if (forClass == LocalTime.class) {
+ return LocalTime.of(12, 1).toString();
+ } else if (forClass == Instant.class) {
+ return Instant.ofEpochSecond(5_000_000, 1000).toString();
+ } else {
+ return null;
+ }
+ }
+
+ static final ClassLoader LOADER = SerializeAllValueClasses.class.getClassLoader();
+
+ private static Optional> findClass(String name) {
+ try {
+ Class> clazz = Class.forName(name, false, LOADER);
+ return Optional.of(clazz);
+ } catch (ClassNotFoundException | ExceptionInInitializerError |
+ NoClassDefFoundError | IllegalAccessError ex) {
+ return Optional.empty();
+ }
+ }
+
+ private static boolean isClass(Class> clazz) {
+ return !(clazz.isEnum() || clazz.isInterface());
+ }
+
+ private static boolean isNonAbstract(Class> clazz) {
+ return (clazz.getModifiers() & Modifier.ABSTRACT) == 0;
+ }
+
+ private static boolean isPublicOrProtected(Class> clazz) {
+ return (clazz.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0;
+ }
+
+ @SuppressWarnings("preview")
+ private static boolean isValueClass(Class> clazz) {
+ if (PreviewFeatures.isEnabled())
+ return clazz.isValue();
+ var a = clazz.getAnnotation(jdk.internal.ValueBased.class);
+ return a != null;
+ }
+
+ /**
+ * Scans classes in the JDK and returns matching classes.
+ *
+ * @return list of matching class
+ * @throws IOException if an unexpected exception occurs
+ * @throws URISyntaxException if an unexpected exception occurs
+ */
+ public static List> findAll() throws IOException, URISyntaxException {
+ FileSystem fs = FileSystems.getFileSystem(new URI("jrt:/"));
+ Path dir = fs.getPath("/modules");
+ try (final Stream paths = Files.walk(dir)) {
+ // each path is in the form: /modules////.../name.class
+ return paths.filter((path) -> path.getNameCount() > 2)
+ .map((path) -> path.subpath(2, path.getNameCount()))
+ .map(Path::toString)
+ .filter((name) -> name.endsWith(".class"))
+ .map((name) -> name.replaceFirst("\\.class$", ""))
+ .filter((name) -> !name.equals("module-info"))
+ .map((name) -> name.replaceAll("/", "."))
+ .flatMap((java.lang.String name) -> findClass(name).stream())
+ .filter(Serializable.class::isAssignableFrom)
+ .filter(SerializeAllValueClasses::isClass)
+ .filter(SerializeAllValueClasses::isNonAbstract)
+ .filter((klass) -> !klass.isSealed())
+ .filter(SerializeAllValueClasses::isValueClass)
+ .filter(SerializeAllValueClasses::isPublicOrProtected)
+ .collect(Collectors.toList());
+ }
+ }
+
+ private void serializeDeserialize(Object expected) {
+ try {
+ Object actual = deserialize(serialize(expected));
+ assertEquals(expected, actual, "round trip compare fail");
+ } catch (IOException | ClassNotFoundException e) {
+ fail("serialize/Deserialize", e);
+ }
+ }
+
+ /**
+ * Serialize an object into byte array.
+ */
+ private static byte[] serialize(Object obj) throws IOException {
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ try (ObjectOutputStream out = new ObjectOutputStream(bs)) {
+ out.writeObject(obj);
+ }
+ return bs.toByteArray();
+ }
+
+ /**
+ * Deserialize an object from byte array using the requested classloader.
+ */
+ private static Object deserialize(byte[] ba) throws IOException, ClassNotFoundException {
+ try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ba))) {
+ return in.readObject();
+ }
+ }
+
+}
diff --git a/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java b/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java
new file mode 100644
index 00000000000..de80da9ae4d
--- /dev/null
+++ b/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java
@@ -0,0 +1,1373 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import combo.ComboInstance;
+import combo.ComboParameter;
+import combo.ComboTask;
+import combo.ComboTestHelper;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InvalidClassException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.OptionalDataException;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+import static jdk.test.lib.Asserts.assertEquals;
+import static jdk.test.lib.Asserts.assertTrue;
+import static jdk.test.lib.Asserts.assertFalse;
+
+import jdk.test.lib.hexdump.HexPrinter;
+import jdk.test.lib.hexdump.ObjectStreamPrinter;
+
+/*
+ * @test
+ * @summary Deserialization Combo tests
+ * @library /test/langtools/tools/javac/lib /test/lib .
+ * @modules jdk.compiler/com.sun.tools.javac.api
+ * jdk.compiler/com.sun.tools.javac.code
+ * jdk.compiler/com.sun.tools.javac.comp
+ * jdk.compiler/com.sun.tools.javac.file
+ * jdk.compiler/com.sun.tools.javac.main
+ * jdk.compiler/com.sun.tools.javac.tree
+ * jdk.compiler/com.sun.tools.javac.util
+ * @build combo.ComboTestHelper SerializedObjectCombo
+ * @run main/othervm --enable-preview SerializedObjectCombo --everything --no-pre-filter
+ */
+
+
+public final class SerializedObjectCombo extends ComboInstance {
+ private static final Map LOADER_FOR_PATH = new ConcurrentHashMap<>();
+ private static final ParamSet KIND_SET = new ParamSet("KIND",
+ SerializationKind.values());
+ private static final ParamSet FIELD_SET = new ParamSet("FIELD",
+ 2, ArgumentValue.BASIC_VALUES);
+ private static final ParamSet CLASSACCESS_SET = new ParamSet("CLASSACCESS",
+ new ClassAccessKind[]{ClassAccessKind.PUBLIC});
+ private static final ParamSet SPECIAL_WRITE_METHODS_SET = new ParamSet("SPECIAL_WRITE_METHODS",
+ WriteObjectFragments.values());
+ private static final ParamSet SPECIAL_READ_METHODS_SET = new ParamSet("SPECIAL_READ_METHODS",
+ ReadObjectFragments.values());
+ private static final ParamSet EXTERNALIZABLE_METHODS_SET = new ParamSet("EXTERNALIZABLE_METHODS",
+ ExternalizableMethodFragments.values());
+ private static final ParamSet OBJECT_CONSTRUCTOR_SET = new ParamSet("OBJECT_CONSTRUCTOR",
+ ObjectConstructorFragment.ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT, ObjectConstructorFragment.NONE);
+ private static final ParamSet VALUE_SET = new ParamSet("VALUE",
+ ValueKind.values());
+ private static final ParamSet TESTNAME_EXTENDS_SET = new ParamSet("TESTNAME_EXTENDS",
+ TestNameExtendsFragments.NONE, TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT);
+ private static final ParamSet TOP_ABSTRACT_SET = new ParamSet("TOP_FRAGMENTS",
+ TopFragments.values());
+ /**
+ * The base template to generate all test classes.
+ * Each substitutable fragment is defined by an Enum of the alternatives.
+ * Giving each a name and an array of ComboParameters with the expansion value.
+ */
+ private static final String TEST_SOURCE_TEMPLATE = """
+ import java.io.*;
+ import java.util.*;
+ import jdk.internal.value.DeserializeConstructor;
+ import jdk.internal.MigratedValueClass;
+
+ #{TOP_FRAGMENTS}
+
+ @MigratedValueClass
+ #{CLASSACCESS} #{VALUE} class #{TESTNAME} #{TESTNAME_EXTENDS} #{KIND.IMPLEMENTS} {
+ #{FIELD[0]} f1;
+ #{FIELD[1]} f2;
+ #{FIELD_ADDITIONS}
+ #{CLASSACCESS} #{TESTNAME}() {
+ f1 = #{FIELD[0].RANDOM};
+ f2 = #{FIELD[1].RANDOM};
+ #{FIELD_CONSTRUCTOR_ADDITIONS}
+ }
+ #{OBJECT_CONSTRUCTOR}
+ @Override public boolean equals(Object obj) {
+ if (obj instanceof #{TESTNAME} other) {
+ if (#{FIELD[0]}.class.isPrimitive()) {
+ if (f1 != other.f1) return false;
+ } else {
+ if (!Objects.equals(f1, other.f1)) return false;
+ }
+ if (#{FIELD[1]}.class.isPrimitive()) {
+ if (f2 != other.f2) return false;
+ } else {
+ if (!Objects.equals(f2, other.f2)) return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ @Override public String toString() {
+ return "f1: " + String.valueOf(f1) +
+ ", f2: " + String.valueOf(f2)
+ #{FIELD_TOSTRING_ADDITIONS};
+ }
+ #{KIND.SPECIAL_METHODS}
+ private static final long serialVersionUID = 1L;
+ }
+ """;
+
+ // The unique number to qualify interface names, unique across multiple runs
+ private static int uniqueId = 0;
+ // Compilation errors prevent execution; set/cleared by checkCompile
+ private ComboTask.Result> compilationResult = null;
+ // The current set of parameters for the file being compiled and tested
+ private final Set currParams = new HashSet<>();
+
+ private static List focusKeys = null;
+
+ private enum CommandOption {
+ SHOW_SOURCE("--show-source", "show source files"),
+ VERBOSE("--verbose", "show extra information"),
+ SHOW_SERIAL_STREAM("--show-serial", "show and format the serialized stream"),
+ EVERYTHING("--everything", "run all tests"),
+ TRACE("--trace", "set TRACE system property of ObjectInputStream (temp)"),
+ MAX_COMBOS("--max-combo", "maximum number of values for each parameter", CommandOption::parseInt),
+ NO_PRE_FILTER("--no-pre-filter", "disable pre-filter checks"),
+ SELFTEST("--self-test", "run some self tests and exit"),
+ ;
+ private final String option;
+ private final String usage;
+ private final BiFunction parseArg;
+ private Optional