diff --git a/src/java.base/share/classes/java/io/ObjectInputStream.java b/src/java.base/share/classes/java/io/ObjectInputStream.java index e34224578ac..00b93c0c0a9 100644 --- a/src/java.base/share/classes/java/io/ObjectInputStream.java +++ b/src/java.base/share/classes/java/io/ObjectInputStream.java @@ -27,18 +27,14 @@ import java.io.ObjectInputFilter.Config; import java.io.ObjectStreamClass.ConstructorSupport; -import java.io.ObjectStreamClass.ClassDataSlot; import java.lang.System.Logger; import java.lang.invoke.MethodHandle; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Objects; import jdk.internal.access.JavaLangAccess; @@ -2131,127 +2127,67 @@ private Object readOrdinaryObject(boolean unshared) throw new InvalidClassException("invalid class descriptor"); } - // Assign the handle and initially set to null or the unsharedMarker - passHandle = handles.assign(unshared ? unsharedMarker : null); + Object obj; + try { + obj = desc.isInstantiable() ? desc.newInstance() : null; + } catch (Exception ex) { + throw new InvalidClassException(desc.forClass().getName(), + "unable to create instance", ex); + } + + passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); } - 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); - }; + final boolean isRecord = desc.isRecord(); + if (isRecord || desc.isDeserializeConstructor()) { + assert obj == null; + obj = readRecordOrValue(desc, isRecord); + if (!unshared) + handles.setObject(passHandle, obj); + } else if (desc.isExternalizable()) { + readExternalData((Externalizable) obj, desc); + } else { + readSerialData(obj, desc); + } - 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); } - } - - return obj; - } catch (UncheckedIOException uioe) { - // Consistent re-throw for nested UncheckedIOExceptions - throw uioe.getCause(); - } - } - - /** - * {@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(); - // 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()); + handles.setObject(passHandle, obj = rep); } } - // 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); - } + return obj; } /** - * Creates a new object and invokes its readExternal method to read its contents. - * - * If the class is instantiable, read externalizable data by invoking readExternal() + * If obj is non-null, reads 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. + * called. */ - private Object readExternalObject(ObjectStreamClass desc, boolean unshared) + private void readExternalData(Externalizable obj, ObjectStreamClass desc) throws IOException { - // 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(); @@ -2295,7 +2231,6 @@ private Object readExternalObject(ObjectStreamClass desc, boolean unshared) * externalizable data remains in the stream, a subsequent read will * most likely throw a StreamCorruptedException. */ - return obj; } /** @@ -2305,13 +2240,13 @@ private Object readExternalObject(ObjectStreamClass desc, boolean unshared) * Null is returned from readRecord and later the exception is thrown at * the exit of {@link #readObject(Class)}. */ - private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOException { - List slots = desc.getClassDataLayout(); - if (slots.size() != 1) { + private Object readRecordOrValue(ObjectStreamClass desc, boolean isRecord) throws IOException { + ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); + if (slots.length != 1) { // skip any superclass stream field values - for (int i = 0; i < slots.size()-1; i++) { - if (slots.get(i).hasData) { - new FieldValues(slots.get(i).desc, true); + for (int i = 0; i < slots.length-1; i++) { + if (slots[i].hasData) { + new FieldValues(slots[i].desc, true); } } } @@ -2325,13 +2260,10 @@ private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOExc // - byte[] primValues // - Object[] objValues // and return Object - MethodHandle ctrMH = ConstructorSupport.deserializationCtr(desc); + MethodHandle ctrMH = ConstructorSupport.deserializationCtr(desc, isRecord); try { - Object obj = (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues); - if (!unshared) - handles.setObject(passHandle, obj); - return obj; + return (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues); } catch (Exception e) { throw new InvalidObjectException(e.getMessage(), e); } catch (Error e) { @@ -2343,207 +2275,114 @@ private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOExc } /** - * 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 Object readSerialDefaultObject(ObjectStreamClass desc, boolean unshared) - throws IOException, InvalidClassException { - if (!desc.isInstantiable()) { - // No local class to create, read and discard - return readAbsentLocalClass(desc, unshared); - } - 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 slots = desc.getClassDataLayout(); - List slotValues = new ArrayList<>(slots.size()); - for (ClassDataSlot s : slots) { - if (s.hasData) { - var value = new FieldValues(s.desc, true); - finishBlockData(s.desc); - slotValues.add(value); - } - } - - 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. - for (FieldValues slotValue : slotValues) { - slotValue.defaultCheckFieldValues(obj); - } - for (FieldValues v : slotValues) { - v.defaultSetFieldValues(obj); - } - return obj; - } catch (InstantiationException | InvocationTargetException ex) { - throw new InvalidClassException(desc.forClass().getName(), - "unable to create instance", ex); - } - } - - - /** - * Reads (or attempts to skip, if not instantiatable or is tagged with a + * 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 current handle before this method is called. + * object in stream, from superclass to subclass. Expects that passHandle + * is set to obj's handle before this method is called. */ - private Object readSerialCustomData(ObjectStreamClass desc, boolean unshared) + private void readSerialData(Object obj, ObjectStreamClass desc) throws IOException { - if (!desc.isInstantiable()) { - // No local class to create, read and discard - return readAbsentLocalClass(desc, unshared); - } + 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; + } + } + // 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); - 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); - } - } + 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; + } - /** - * 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 { - - 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); + /* + * defaultDataEnd may have been set indirectly by custom + * readObject() method when calling defaultReadObject() or + * readFields(); clear it to restore normal read behavior. + */ + defaultDataEnd = false; } else { // Read fields of the current descriptor into a new FieldValues FieldValues values = new FieldValues(slotDesc, true); - if (handles.lookupException(passHandle) == null) { - // Set the instance fields if no previous exception - values.defaultCheckFieldValues(obj); - values.defaultSetFieldValues(obj); + 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); + } } - finishBlockData(slotDesc); + } + + if (slotDesc.hasWriteObjectData()) { + skipCustomData(); + } else { + bin.setBlockDataMode(false); } } else { - if (slotDesc.hasReadObjectNoDataMethod() && - handles.lookupException(passHandle) == null) { + if (obj != null && + slotDesc.hasReadObjectNoDataMethod() && + handles.lookupException(passHandle) == null) + { slotDesc.invokeReadObjectNoData(obj); } } } - return 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 { - 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 { - 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); + 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); } - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); } } @@ -2639,37 +2478,32 @@ 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 UncheckedIOException { - try { - this.desc = desc; - 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); - } + FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws IOException { + this.desc = desc; + + 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); } - passHandle = objHandle; } - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); + passHandle = objHandle; } } diff --git a/src/java.base/share/classes/java/io/ObjectOutputStream.java b/src/java.base/share/classes/java/io/ObjectOutputStream.java index 9d579d880ad..25ea52ac22e 100644 --- a/src/java.base/share/classes/java/io/ObjectOutputStream.java +++ b/src/java.base/share/classes/java/io/ObjectOutputStream.java @@ -1335,7 +1335,7 @@ private void writeOrdinaryObject(Object obj, if (desc.isRecord()) { writeRecordData(obj, desc); } else if (desc.isExternalizable() && !desc.isProxy()) { - if (desc.isValue()) + if (desc.isValueOrStrictInit()) throw new InvalidClassException("Externalizable not valid for value class " + desc.forClass().getName()); writeExternalData((Externalizable) obj); @@ -1386,10 +1386,10 @@ private void writeRecordData(Object obj, ObjectStreamClass desc) throws IOException { assert obj.getClass().isRecord(); - List slots = desc.getClassDataLayout(); - if (slots.size() != 1) { + ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); + if (slots.length != 1) { throw new InvalidClassException( - "expected a single record slot length, but found: " + slots.size()); + "expected a single record slot length, but found: " + slots.length); } defaultWriteFields(obj, desc); // #### seems unnecessary to use the accessors @@ -1402,9 +1402,9 @@ private void writeRecordData(Object obj, ObjectStreamClass desc) private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { - List slots = desc.getClassDataLayout(); - for (int i = 0; i < slots.size(); i++) { - ObjectStreamClass slotDesc = slots.get(i).desc; + ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); + for (int i = 0; i < slots.length; i++) { + ObjectStreamClass slotDesc = slots[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 63b044641d3..3a056f742d6 100644 --- a/src/java.base/share/classes/java/io/ObjectStreamClass.java +++ b/src/java.base/share/classes/java/io/ObjectStreamClass.java @@ -29,8 +29,8 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Field; -import java.lang.reflect.InaccessibleObjectException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.RecordComponent; import java.lang.reflect.Member; @@ -43,19 +43,19 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; 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; import jdk.internal.misc.Unsafe; import jdk.internal.reflect.ReflectionFactory; import jdk.internal.util.ByteArray; import jdk.internal.value.DeserializeConstructor; +import jdk.internal.value.ValueClass; /** * Serialization's descriptor for classes. It contains the name and @@ -89,71 +89,6 @@ public final class ObjectStreamClass implements Serializable { private static final ObjectStreamField[] serialPersistentFields = NO_FIELDS; - /** - * 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 = @@ -188,9 +123,7 @@ protected Map computeValue(Class type) { /** true if represents record type */ private boolean isRecord; /** true if represents a value class */ - private boolean isValue; - /** The DeserializationMode for this class. */ - private DeserializationMode factoryMode; + private boolean isValueOrStrictInit; /** true if represented class implements Serializable */ private boolean serializable; /** true if represented class implements Externalizable */ @@ -248,7 +181,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 List dataLayout; + private volatile ClassDataSlot[] dataLayout; /** serialization-appropriate constructor, or null if none */ private Constructor cons; @@ -260,6 +193,8 @@ InvalidClassException newInvalidClassException() { /** session-cache of record deserialization constructor * (in de-serialized OSC only), or null */ private MethodHandle deserializationCtr; + /** value deserialization method identified by {@link DeserializeConstructor}.*/ + private Executable deserializeConstructor; /** class-defined writeObject method, or null if none */ private Method writeObjectMethod; @@ -412,7 +347,7 @@ private ObjectStreamClass(final Class cl) { isProxy = Proxy.isProxyClass(cl); isEnum = Enum.class.isAssignableFrom(cl); isRecord = cl.isRecord(); - isValue = cl.isValue(); + isValueOrStrictInit = cl.isValue() || ValueClass.hasStrictInstanceField(cl); serializable = Serializable.class.isAssignableFrom(cl); externalizable = Externalizable.class.isAssignableFrom(cl); @@ -438,34 +373,19 @@ private ObjectStreamClass(final Class cl) { } if (isRecord) { - factoryMode = DeserializationMode.READ_RECORD; canonicalCtr = canonicalRecordCtr(cl); deserializationCtrs = new DeserializationConstructorsCache(); - } else if (externalizable) { - factoryMode = DeserializationMode.READ_EXTERNALIZABLE; - if (cl.isValue()) { - serializeEx = deserializeEx = new ExceptionInfo(cl.getName(), - "Externalizable not valid for value class"); - } else { - cons = getExternalizableConstructor(cl); + } else if (isValueOrStrictInit) { + if (!Modifier.isAbstract(cl.getModifiers())) { + // Serializable value classes should have constructor(s) annotated with {@link DeserializeConstructor} + deserializeConstructor = getDeserializingValueCons(cl, fields); } - } else if (cl.isValue()) { - factoryMode = DeserializationMode.READ_OBJECT_VALUE; - if (!cl.isAnnotationPresent(MigratedValueClass.class)) { + if (deserializeConstructor == null) { 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"); - } + "cannot serialize value class"); } + } else if (externalizable) { + cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject", @@ -477,10 +397,6 @@ private ObjectStreamClass(final Class cl) { 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; } writeReplaceMethod = getInheritableMethod( cl, "writeReplace", null, Object.class); @@ -502,7 +418,7 @@ private ObjectStreamClass(final Class cl) { if (deserializeEx == null) { if (isEnum) { deserializeEx = new ExceptionInfo(name, "enum type"); - } else if (cons == null && !(isRecord | isValue)) { + } else if (cons == null && !(isRecord || isValueOrStrictInit)) { deserializeEx = new ExceptionInfo(name, "no valid constructor"); } } @@ -561,9 +477,6 @@ void initProxy(Class cl, readResolveMethod = localDesc.readResolveMethod; deserializeEx = localDesc.deserializeEx; cons = localDesc.cons; - factoryMode = localDesc.factoryMode; - } else { - factoryMode = DeserializationMode.READ_OBJECT_DEFAULT; } fieldRefl = getReflector(fields, localDesc); initialized = true; @@ -642,7 +555,7 @@ void initNonProxy(ObjectStreamClass model, if (osc != null) { localDesc = osc; isRecord = localDesc.isRecord; - isValue = localDesc.isValue; + isValueOrStrictInit = localDesc.isValueOrStrictInit; // canonical record constructor is shared canonicalCtr = localDesc.canonicalCtr; // cache of deserialization constructors is shared @@ -657,12 +570,7 @@ void initNonProxy(ObjectStreamClass model, } 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; + deserializeConstructor = localDesc.deserializeConstructor; } fieldRefl = getReflector(fields, localDesc); @@ -718,7 +626,7 @@ void readNonProxy(ObjectInputStream in) String signature = ((tcode == 'L') || (tcode == '[')) ? in.readTypeString() : String.valueOf(tcode); try { - fields[i] = new ObjectStreamField(fname, signature, false, -1); + fields[i] = new ObjectStreamField(fname, signature, false); } catch (RuntimeException e) { throw new InvalidClassException(name, "invalid descriptor for field " + @@ -932,17 +840,14 @@ boolean isSerializable() { /** * {@return {code true} if the class is a value class, {@code false} otherwise} */ - boolean isValue() { + boolean isValueOrStrictInit() { requireInitialized(); - return isValue; + return isValueOrStrictInit; } - /** - * {@return the factory mode for deserialization} - */ - DeserializationMode factoryMode() { + boolean isDeserializeConstructor() { requireInitialized(); - return factoryMode; + return deserializeConstructor != null; } /** @@ -967,15 +872,13 @@ boolean hasWriteObjectData() { /** * Returns true if represented class is serializable/externalizable and can * be instantiated by the serialization runtime--i.e., if it is - * externalizable and defines a public no-arg constructor, if it is + * externalizable and defines a public no-arg constructor, or if it is * non-externalizable and its first non-serializable superclass defines an - * accessible no-arg constructor, or if the class is a value class with a @DeserializeConstructor - * constructor or static factory. - * Otherwise, returns false. + * accessible no-arg constructor. Otherwise, returns false. */ boolean isInstantiable() { requireInitialized(); - return (cons != null || (isValue() && canonicalCtr != null)); + return (cons != null); } /** @@ -1232,18 +1135,23 @@ static class ClassDataSlot { } /** - * Returns a List of ClassDataSlot instances representing the data layout + * Returns array 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. */ - List getClassDataLayout() throws InvalidClassException { + ClassDataSlot[] getClassDataLayout() throws InvalidClassException { // REMIND: synchronize instead of relying on volatile? - List layout = dataLayout; - if (layout != null) - return layout; + if (dataLayout == null) { + dataLayout = getClassDataLayout0(); + } + return dataLayout; + } + private ClassDataSlot[] getClassDataLayout0() + throws InvalidClassException + { ArrayList slots = new ArrayList<>(); Class start = cl, end = cl; @@ -1292,8 +1200,7 @@ List getClassDataLayout() throws InvalidClassException { // order slots from superclass -> subclass Collections.reverse(slots); - dataLayout = slots; - return slots; + return slots.toArray(new ClassDataSlot[slots.size()]); } /** @@ -1430,45 +1337,50 @@ private ObjectStreamClass getVariantFor(Class cl) * @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( + private static Executable getDeserializingValueCons(Class clazz, + ObjectStreamField[] fields) { + return 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)) + .mapMulti((exec, sink) -> { + if (exec.getParameterCount() != fields.length) + return; + var deserialzeConstructor = exec.getDeclaredAnnotation(DeserializeConstructor.class); + if (deserialzeConstructor == null) + return; + var names = deserialzeConstructor.value(); + if (!matchFactoryParamTypes(exec, names, fields)) + return; + exec.setAccessible(true); + sink.accept(exec); + }) .findFirst().orElse(null); - 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) { - var params = mh.type().parameterList(); - if (params.size() != fields.length) { - return false; // Mismatch in count of fields and parameters + private static boolean matchFactoryParamTypes(Executable exec, + String[] names, + ObjectStreamField[] fields) { + if (names.length != fields.length) { + return false; + } + var map = HashMap.newHashMap(names.length); + for (int i = 0; i < names.length; i++) { + if (map.put(names[i], i) != null) { + return false; // Duplicate names + } } + var params = exec.getParameterTypes(); for (ObjectStreamField field : fields) { - int argIndex = field.getArgIndex(); - final Class paramtype = params.get(argIndex); - if (!field.getType().equals(paramtype)) { + var i = map.get(field.getName()); + if (i == null) { + return false; // Name mismatch + } + if (!field.getType().equals(params[i])) { return false; } } @@ -1486,7 +1398,7 @@ private static Constructor getExternalizableConstructor(Class cl) { cons.setAccessible(true); return ((cons.getModifiers() & Modifier.PUBLIC) != 0) ? cons : null; - } catch (NoSuchMethodException | InaccessibleObjectException ex) { + } catch (NoSuchMethodException ex) { return null; } } @@ -1717,12 +1629,14 @@ private static ObjectStreamField[] getDeclaredSerialFields(Class cl) if ((f.getType() == spf.getType()) && ((f.getModifiers() & Modifier.STATIC) == 0)) { - boundFields[i] = new ObjectStreamField(f, spf.isUnshared(), true, i); + boundFields[i] = + new ObjectStreamField(f, spf.isUnshared(), true); } } catch (NoSuchFieldException ex) { } if (boundFields[i] == null) { - boundFields[i] = new ObjectStreamField(fname, spf.getType(), spf.isUnshared(), i); + boundFields[i] = new ObjectStreamField( + fname, spf.getType(), spf.isUnshared()); } } return boundFields; @@ -1739,9 +1653,9 @@ private static ObjectStreamField[] getDefaultSerialFields(Class cl) { ArrayList list = new ArrayList<>(); int mask = Modifier.STATIC | Modifier.TRANSIENT; - for (int i = 0, argIndex = 0; i < clFields.length; i++) { + for (int i = 0; i < clFields.length; i++) { if ((clFields[i].getModifiers() & mask) == 0) { - list.add(new ObjectStreamField(clFields[i], false, true, argIndex++)); + list.add(new ObjectStreamField(clFields[i], false, true)); } } int size = list.size(); @@ -2097,11 +2011,10 @@ void getObjFieldValues(Object obj, Object[] vals) { * in array should be equal to Unsafe.INVALID_FIELD_OFFSET. */ for (int i = numPrimFields; i < fields.length; i++) { - Field f = fields[i].getField(); vals[offsets[i]] = switch (typeCodes[i]) { case 'L', '[' -> layouts[i] != 0 - ? UNSAFE.getFlatValue(obj, readKeys[i], layouts[i], f.getType()) + ? UNSAFE.getFlatValue(obj, readKeys[i], layouts[i], types[i]) : UNSAFE.getReference(obj, readKeys[i]); default -> throw new InternalError(); }; @@ -2129,7 +2042,7 @@ void setObjFieldValues(Object obj, Object[] vals) { } private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) { - if (obj == null && !dryRun) { + if (obj == null) { throw new NullPointerException(); } for (int i = numPrimFields; i < fields.length; i++) { @@ -2139,11 +2052,11 @@ private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) { } switch (typeCodes[i]) { case 'L', '[' -> { - Field f = fields[i].getField(); Object val = vals[offsets[i]]; if (val != null && !types[i - numPrimFields].isInstance(val)) { + Field f = fields[i].getField(); throw new ClassCastException( "cannot assign instance of " + val.getClass().getName() + " to field " + @@ -2154,7 +2067,7 @@ private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) { } if (!dryRun) { if (layouts[i] != 0) { - UNSAFE.putFlatValue(obj, key, layouts[i], f.getType(), val); + UNSAFE.putFlatValue(obj, key, layouts[i], types[i], val); } else { UNSAFE.putReference(obj, key, val); } @@ -2270,16 +2183,16 @@ private static ObjectStreamField[] matchFields(ObjectStreamField[] fields, } if (lf.getField() != null) { m = new ObjectStreamField( - lf.getField(), lf.isUnshared(), true, lf.getArgIndex()); // Don't hide type + lf.getField(), lf.isUnshared(), false); } else { m = new ObjectStreamField( - lf.getName(), lf.getSignature(), lf.isUnshared(), lf.getArgIndex()); + lf.getName(), lf.getSignature(), lf.isUnshared()); } } } if (m == null) { m = new ObjectStreamField( - f.getName(), f.getSignature(), false, -1); + f.getName(), f.getSignature(), false); } m.setOffset(f.getOffset()); matches[i] = m; @@ -2408,64 +2321,40 @@ static final class ConstructorSupport { * and return * {@code Object} */ - static MethodHandle deserializationCtr(ObjectStreamClass desc) { + static MethodHandle deserializationCtr(ObjectStreamClass desc, boolean isRecord) { // 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; + if (isRecord) { + mh = desc.deserializationCtrs.get(desc.getFields(false)); + if (mh != null) return desc.deserializationCtr = mh; + } // retrieve record components - RecordComponent[] recordComponents = desc.forClass().getRecordComponents(); - - // retrieve the canonical constructor - // (T1, T2, ..., Tn):TR - mh = desc.getRecordConstructor(); - - // 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); - - for (int i = recordComponents.length-1; i >= 0; i--) { - String name = recordComponents[i].getName(); - Class type = recordComponents[i].getType(); - // 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); + Class[] types; + String[] names; + int count; + if (isRecord) { + var recordComponents = desc.forClass().getRecordComponents(); + types = Arrays.stream(recordComponents).map(RecordComponent::getType).toArray(Class[]::new); + names = Arrays.stream(recordComponents).map(RecordComponent::getName).toArray(String[]::new); + count = recordComponents.length; + // retrieve the canonical constructor + // (T1, T2, ..., Tn):TR + mh = desc.getRecordConstructor(); + } else { + var ctor = desc.deserializeConstructor; + types = ctor.getParameterTypes(); + names = ctor.getDeclaredAnnotation(DeserializeConstructor.class).value(); + count = types.length; + var lookup = MethodHandles.publicLookup(); + try { + mh = ctor instanceof Method m ? lookup.unreflect(m) + : lookup.unreflectConstructor((Constructor) ctor); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } } - // what we are left with is a MethodHandle taking just the primValues - // and objValues arrays and returning the constructed record instance - // (byte[], Object[]):Object - - // store it into cache and return the 1st value stored - return desc.deserializationCtr = - 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 @@ -2475,12 +2364,9 @@ static MethodHandle deserializationValueCons(ObjectStreamClass desc) { // (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]; + for (int i = count-1; i >= 0; i--) { + String name = names[i]; + Class type = types[i]; // obtain stream field extractor that extracts argument at // position i (Ti+1) from primValues and objValues arrays // (byte[], Object[]):Ti+1 @@ -2490,21 +2376,14 @@ static MethodHandle deserializationValueCons(ObjectStreamClass desc) { 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 + // and objValues arrays and returning the constructed record 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; + if (isRecord) { + mh = desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh); } - return null; + return desc.deserializationCtr = mh; } /** Returns the number of primitive fields for the given descriptor. */ diff --git a/src/java.base/share/classes/java/io/ObjectStreamField.java b/src/java.base/share/classes/java/io/ObjectStreamField.java index 962758054b8..465c29c101c 100644 --- a/src/java.base/share/classes/java/io/ObjectStreamField.java +++ b/src/java.base/share/classes/java/io/ObjectStreamField.java @@ -54,8 +54,6 @@ 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 @@ -86,11 +84,6 @@ 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(); } @@ -99,14 +92,13 @@ 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, int argIndex) { + ObjectStreamField(String name, String signature, boolean unshared) { if (name == null) { throw new NullPointerException(); } @@ -114,7 +106,6 @@ 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; @@ -138,14 +129,13 @@ 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, int argIndex) { + ObjectStreamField(Field field, boolean unshared, boolean showType) { 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; } /** @@ -226,13 +216,6 @@ 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 8e471963a24..12b1a922d62 100644 --- a/src/java.base/share/classes/java/lang/Boolean.java +++ b/src/java.base/share/classes/java/lang/Boolean.java @@ -111,7 +111,7 @@ public final class Boolean implements java.io.Serializable, * if possible. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Boolean(boolean value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Byte.java b/src/java.base/share/classes/java/lang/Byte.java index af44ca334ca..64faf133e32 100644 --- a/src/java.base/share/classes/java/lang/Byte.java +++ b/src/java.base/share/classes/java/lang/Byte.java @@ -378,7 +378,7 @@ public static Byte decode(String nm) throws NumberFormatException { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Byte(byte value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Character.java b/src/java.base/share/classes/java/lang/Character.java index 1dddbd5948e..55dd607d8bb 100644 --- a/src/java.base/share/classes/java/lang/Character.java +++ b/src/java.base/share/classes/java/lang/Character.java @@ -9423,7 +9423,7 @@ public static final UnicodeScript forName(String scriptName) { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Character(char value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Double.java b/src/java.base/share/classes/java/lang/Double.java index 29a84d54d61..f029a6e3825 100644 --- a/src/java.base/share/classes/java/lang/Double.java +++ b/src/java.base/share/classes/java/lang/Double.java @@ -1085,7 +1085,7 @@ public static boolean isFinite(double d) { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Double(double value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Float.java b/src/java.base/share/classes/java/lang/Float.java index 402688f058a..299dfa0af7e 100644 --- a/src/java.base/share/classes/java/lang/Float.java +++ b/src/java.base/share/classes/java/lang/Float.java @@ -688,7 +688,7 @@ public static boolean isFinite(float f) { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Float(float value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Integer.java b/src/java.base/share/classes/java/lang/Integer.java index a31ed449e70..5e2a608ce43 100644 --- a/src/java.base/share/classes/java/lang/Integer.java +++ b/src/java.base/share/classes/java/lang/Integer.java @@ -1043,7 +1043,7 @@ public static Integer valueOf(int i) { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Integer(int value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Long.java b/src/java.base/share/classes/java/lang/Long.java index 198d743c093..5c0f4a2719e 100644 --- a/src/java.base/share/classes/java/lang/Long.java +++ b/src/java.base/share/classes/java/lang/Long.java @@ -1105,7 +1105,7 @@ else if (nm.startsWith("0", index) && nm.length() > 1 + index) { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Long(long value) { this.value = value; } diff --git a/src/java.base/share/classes/java/lang/Short.java b/src/java.base/share/classes/java/lang/Short.java index f08a64e0a04..31904609ecc 100644 --- a/src/java.base/share/classes/java/lang/Short.java +++ b/src/java.base/share/classes/java/lang/Short.java @@ -383,7 +383,7 @@ public static Short decode(String nm) throws NumberFormatException { * likely to yield significantly better space and time performance. */ @Deprecated(since="9") - @DeserializeConstructor + @DeserializeConstructor("value") public Short(short value) { this.value = value; } diff --git a/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java b/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java index f1acff12831..f5d85a2c4cd 100644 --- a/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java +++ b/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java @@ -35,9 +35,12 @@ * The annotation is used by java.io.ObjectStreamClass to select the constructor * or factory method to create objects from a stream. * - * @since 24 + * @since Valhalla */ @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, METHOD}) public @interface DeserializeConstructor { + /// Identifies the serial field names for the method parameters. + /// The serial field type are the corresponding parameter types. + String[] value(); } diff --git a/src/java.base/share/classes/jdk/internal/value/ValueClass.java b/src/java.base/share/classes/jdk/internal/value/ValueClass.java index 50756fbcaec..723befc9d4b 100644 --- a/src/java.base/share/classes/jdk/internal/value/ValueClass.java +++ b/src/java.base/share/classes/jdk/internal/value/ValueClass.java @@ -34,6 +34,9 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import static java.lang.classfile.ClassFile.ACC_STATIC; +import static java.lang.classfile.ClassFile.ACC_STRICT_INIT; + /** * Utilities to access package private methods of java.lang.Class and related reflection classes. */ @@ -139,4 +142,31 @@ public static Object[] copyOfRangeSpecialArray(Object[] array, int from, int to) */ @IntrinsicCandidate public static native boolean isAtomicArray(Object[] array); + + // This class also serves as a lazy holder of its singleton instance + private static final class StrictInstanceFieldClassValue extends ClassValue { + private static final StrictInstanceFieldClassValue INSTANCE = new StrictInstanceFieldClassValue(); + + private StrictInstanceFieldClassValue() {} + + @Override + protected Boolean computeValue(Class type) { + if ((type.getModifiers() & (Modifier.ABSTRACT | Modifier.FINAL)) == (Modifier.ABSTRACT | Modifier.FINAL)) { + // Not class or interface + return false; + } + for (var field : type.getDeclaredFields()) { + // Reflection filters fields, hope the filtered classes don't declare strict fields + if ((field.getModifiers() & (ACC_STATIC | ACC_STRICT_INIT)) == ACC_STRICT_INIT) { + return true; + } + } + return false; + } + } + + /// Returns whether a class or interface declares strict instance fields. + public static boolean hasStrictInstanceField(Class cl) { + return StrictInstanceFieldClassValue.INSTANCE.get(cl); + } } diff --git a/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java b/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java index c706535fba1..cdd6b98a06e 100644 --- a/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java +++ b/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java @@ -1003,14 +1003,14 @@ public String expand(String optParameter) { enum ObjectConstructorFragment implements ComboParameter, CodeShapePredicate { NONE(""), ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT(""" - @DeserializeConstructor + @DeserializeConstructor({"f1", "f2"}) #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2) { this.f1 = f1; this.f2 = f2; #{FIELD_CONSTRUCTOR_ADDITIONS} } - @DeserializeConstructor + @DeserializeConstructor({"f1", "f2"}) #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2, int fExtra) { this.f1 = f1; this.f2 = f2; diff --git a/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java b/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java index fa07833f0c0..e9618b17ac1 100644 --- a/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java +++ b/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java @@ -318,7 +318,7 @@ static value class TreeV implements Tree, Serializable { private TreeV left; private TreeV right; - @DeserializeConstructor + @DeserializeConstructor({"left", "right"}) TreeV(TreeV left, TreeV right) { this.left = left; this.right = right; @@ -364,7 +364,7 @@ public String toString(int depth) { void testExternalizableNotSer() { var obj = new ValueExt(); var ex = Assertions.assertThrows(InvalidClassException.class, () -> serialize(obj)); - Assertions.assertEquals("SimpleValueGraphs$ValueExt; Externalizable not valid for value class", ex.getMessage()); + Assertions.assertEquals("SimpleValueGraphs$ValueExt; cannot serialize value class", ex.getMessage()); } @Test @@ -373,7 +373,7 @@ void testExternalizableNotDeser() throws IOException { byte[] bytes = serialize(obj); byte[] newBytes = patchBytes(bytes, "IdentExt", "ValueExt"); var ex = Assertions.assertThrows(InvalidClassException.class, () -> deserialize(newBytes)); - Assertions.assertTrue(ex.getMessage().contains("Externalizable not valid for value class")); + Assertions.assertTrue(ex.getMessage().contains("cannot serialize value class")); } // Exception trying to serialize diff --git a/test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java b/test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java index eb390551ba3..6efcfe840a8 100644 --- a/test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java +++ b/test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java @@ -26,7 +26,9 @@ * @summary Test serialization of value classes * @enablePreview * @modules java.base/jdk.internal java.base/jdk.internal.value + * @library /test/lib * @compile ValueSerializationTest.java + * @run driver jdk.test.lib.helpers.StrictProcessor ValueSerializationTest$NonSerializableStrictPoint * @run junit/othervm ValueSerializationTest */ @@ -53,14 +55,14 @@ import jdk.internal.MigratedValueClass; import jdk.internal.value.DeserializeConstructor; +import jdk.test.lib.helpers.StrictInit; import org.junit.jupiter.api.Assertions; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.*; + public class ValueSerializationTest { static final Class NSE = NotSerializableException.class; @@ -70,6 +72,7 @@ public static Stream doesNotImplementSerializable() { return Stream.of( Arguments.of( new NonSerializablePoint(10, 100), NSE), Arguments.of( new NonSerializablePointNoCons(10, 100), ICE), + Arguments.of( new NonSerializableStrictPoint(), ICE), // an array of Points Arguments.of( new NonSerializablePoint[] {new NonSerializablePoint(1, 5)}, NSE), Arguments.of( Arguments.of(new NonSerializablePoint(3, 7)), NSE), @@ -103,6 +106,24 @@ public NonSerializablePoint(int x, int y) { } } + public static class NonSerializableStrictPoint implements Serializable { + static { + for (var f : NonSerializableStrictPoint.class.getDeclaredFields()) { + assertTrue(f.isStrictInit(), f.getName()); + } + } + + @StrictInit + public int x; + @StrictInit + public int y; + public NonSerializableStrictPoint() { + x = 3; + y = 5; + super(); + } + } + /* Non-Serializable point, because it does not have an @DeserializeConstructor constructor. */ public static value class NonSerializablePointNoCons implements Serializable { public int x; @@ -162,7 +183,7 @@ public void implementSerializable(Object obj) throws IOException, ClassNotFoundE static value class SerializablePoint implements Serializable { public int x; public int y; - @DeserializeConstructor + @DeserializeConstructor({"x", "y"}) private SerializablePoint(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { @@ -173,7 +194,7 @@ static value class SerializablePoint implements Serializable { /* A Serializable Foo, with a serial proxy */ static value class SerializableFoo implements Serializable { public int x; - @DeserializeConstructor + @DeserializeConstructor("x") SerializableFoo(int x) { this.x = x; } @Serial Object writeReplace() throws ObjectStreamException {