< prev index next > src/java.base/share/classes/java/io/ObjectStreamClass.java
Print this page
/*
- * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2026, 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. Oracle designates this
import java.lang.invoke.MethodHandle;
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.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+ import java.util.stream.Stream;
import jdk.internal.event.SerializationMisdeclarationEvent;
import jdk.internal.misc.Unsafe;
import jdk.internal.reflect.ReflectionFactory;
import jdk.internal.util.ByteArray;
+ import jdk.internal.value.Deserializer;
+ import jdk.internal.value.ValueClass;
/**
* Serialization's descriptor for classes. It contains the name and
* serialVersionUID of the class. The ObjectStreamClass for a specific class
* loaded in this Java VM can be found/created using the lookup method.
private boolean isProxy;
/** true if represents enum type */
private boolean isEnum;
/** true if represents record type */
private boolean isRecord;
+ /** true if represented class cannot use allocate-and-fill deserialization,
+ * due to value class or strict field initialization restrictions. */
+ private boolean requiresDeserializer;
/** true if represented class implements Serializable */
private boolean serializable;
/** true if represented class implements Externalizable */
private boolean externalizable;
/** true if desc has data written by class-defined writeObject method */
private Constructor<?> cons;
/** record canonical constructor (shared among OSCs for same class), or null */
private MethodHandle canonicalCtr;
/** cache of record deserialization constructors per unique set of stream fields
* (shared among OSCs for same class), or null */
- private DeserializationConstructorsCache deserializationCtrs;
- /** session-cache of record deserialization constructor
+ private RecordConstructorsCache cachedRecordConstructors;
+ /** session-cache of deserialization factory
* (in de-serialized OSC only), or null */
- private MethodHandle deserializationCtr;
+ private MethodHandle cachedAlternativeFactory;
+ /** value deserialization factory method or constructor identified by
+ * {@link Deserializer}, used when regular deserialization is
+ * illegal but deserialization support is required. */
+ private Executable deserializer;
/** class-defined writeObject method, or null if none */
private Method writeObjectMethod;
/** class-defined readObject method, or null if none */
private Method readObjectMethod;
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
isRecord = cl.isRecord();
+ requiresDeserializer = cl.isValue() || ValueClass.hasStrictInstanceField(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
Class<?> superCl = cl.getSuperclass();
superDesc = (superCl != null) ? lookup(superCl, false) : null;
fields = NO_FIELDS;
}
if (isRecord) {
canonicalCtr = canonicalRecordCtr(cl);
- deserializationCtrs = new DeserializationConstructorsCache();
+ cachedRecordConstructors = new RecordConstructorsCache();
+ } else if (requiresDeserializer) {
+ if (!Modifier.isAbstract(cl.getModifiers())) {
+ // Serializable value classes that appear in streams should
+ // have a factory annotated with @Deserializer
+ deserializer = findDeserializer(cl, fields);
+ }
+ if (deserializer == null) {
+ serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+ "cannot serialize value class");
+ }
} else if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
}
if (deserializeEx == null) {
if (isEnum) {
deserializeEx = new ExceptionInfo(name, "enum type");
- } else if (cons == null && !isRecord) {
+ } else if (cons == null && !(isRecord || requiresDeserializer)) {
deserializeEx = new ExceptionInfo(name, "no valid constructor");
}
}
if (isRecord && canonicalCtr == null) {
deserializeEx = new ExceptionInfo(name, "record canonical constructor not found");
numObjFields = model.numObjFields;
if (osc != null) {
localDesc = osc;
isRecord = localDesc.isRecord;
+ requiresDeserializer = localDesc.requiresDeserializer;
// canonical record constructor is shared
canonicalCtr = localDesc.canonicalCtr;
// cache of deserialization constructors is shared
- deserializationCtrs = localDesc.deserializationCtrs;
+ cachedRecordConstructors = localDesc.cachedRecordConstructors;
writeObjectMethod = localDesc.writeObjectMethod;
readObjectMethod = localDesc.readObjectMethod;
readObjectNoDataMethod = localDesc.readObjectNoDataMethod;
writeReplaceMethod = localDesc.writeReplaceMethod;
readResolveMethod = localDesc.readResolveMethod;
if (deserializeEx == null) {
deserializeEx = localDesc.deserializeEx;
}
assert cl.isRecord() ? localDesc.cons == null : true;
cons = localDesc.cons;
+ deserializer = localDesc.deserializer;
}
fieldRefl = getReflector(fields, localDesc);
// reassign to matched fields so as to reflect local unshared settings
fields = fieldRefl.getFields();
boolean isSerializable() {
requireInitialized();
return serializable;
}
+ /**
+ * {@return whether this class must use a deserialize factory}
+ * Concrete value classes and classes declaring strict fields cannot use the
+ * standard allocate-and-fill deserialization process.
+ */
+ boolean requiresDeserializer() {
+ requireInitialized();
+ return requiresDeserializer;
+ }
+
+ /**
+ * {@return whether this class declares a deserialize factory}
+ */
+ boolean hasDeserializer() {
+ requireInitialized();
+ return deserializer != null;
+ }
+
/**
* Returns true if class descriptor represents externalizable class that
* has written its data in 1.2 (block data) format, false otherwise.
*/
boolean hasBlockExternalData() {
desc.initNonProxy(this, cl, null, superDesc);
}
return desc;
}
+ /**
+ * Return an Executable for the static method or constructor(s) that matches the
+ * serializable fields and annotated with {@link Deserializer}.
+ * 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 an Executable, null if none found
+ */
+ private static Executable findDeserializer(Class<?> clazz,
+ ObjectStreamField[] fields) {
+ return Stream.concat(
+ Arrays.stream(clazz.getDeclaredMethods()).filter(m -> Modifier.isStatic(m.getModifiers())),
+ Arrays.stream(clazz.getDeclaredConstructors()))
+ .<Executable>mapMulti((exec, sink) -> {
+ if (!isDeserializer(exec, fields))
+ return;
+ exec.setAccessible(true);
+ sink.accept(exec);
+ })
+ .findFirst().orElse(null);
+ }
+
+ /**
+ * Check that an executable is a valid deserializer declaration for
+ * this class. This checks parameters types of the executable and the
+ * names identified by the deserializer annotation against the fields
+ * of this class.
+ *
+ * @return true if exec is a valid deserializer
+ */
+ private static boolean isDeserializer(Executable exec,
+ ObjectStreamField[] fields) {
+ if (exec.getParameterCount() != fields.length) {
+ return false;
+ }
+
+ var deserializer = exec.getDeclaredAnnotation(Deserializer.class);
+ if (deserializer == null) {
+ return false;
+ }
+
+ String[] names = deserializer.value();
+ if (names.length != fields.length) {
+ return false;
+ }
+
+ Map<String, Integer> map = HashMap.newHashMap(names.length);
+ for (int i = 0; i < names.length; i++) {
+ if (map.put(names[i], i) != null) {
+ return false; // Duplicate names in the factory
+ }
+ }
+
+ var params = exec.getParameterTypes();
+ for (ObjectStreamField field : fields) {
+ Integer i = map.get(field.getName());
+ if (i == null) {
+ return false; // Name not accounted by the factory
+ }
+ if (!field.getType().equals(params[i])) {
+ return false; // Name match, type mismatch
+ }
+ }
+ 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
* the defining class may still be non-public.
*/
private final long[] readKeys;
/** unsafe fields keys for writing fields - no dupes */
private final long[] writeKeys;
/** field data offsets */
private final int[] offsets;
+ /** field layouts, only used by reference fields */
+ private final int[] layouts;
/** field type codes */
private final char[] typeCodes;
- /** field types */
+ /** reference field types, only fields.length - numPrimFields items */
private final Class<?>[] types;
/**
* Constructs FieldReflector capable of setting/getting values from the
* subset of fields whose ObjectStreamFields contain non-null
this.fields = fields;
int nfields = fields.length;
readKeys = new long[nfields];
writeKeys = new long[nfields];
offsets = new int[nfields];
+ layouts = new int[nfields];
typeCodes = new char[nfields];
ArrayList<Class<?>> typeList = new ArrayList<>();
Set<Long> usedKeys = new HashSet<>();
UNSAFE.objectFieldOffset(rf) : Unsafe.INVALID_FIELD_OFFSET;
readKeys[i] = key;
writeKeys[i] = usedKeys.add(key) ?
key : Unsafe.INVALID_FIELD_OFFSET;
offsets[i] = f.getOffset();
+ layouts[i] = rf != null && !f.isPrimitive() ? UNSAFE.fieldLayout(rf) : 0;
typeCodes[i] = f.getTypeCode();
if (!f.isPrimitive()) {
typeList.add((rf != null) ? rf.getType() : null);
}
}
* descriptor this FieldReflector was obtained from, no field keys
* in array should be equal to Unsafe.INVALID_FIELD_OFFSET.
*/
for (int i = numPrimFields; i < fields.length; i++) {
vals[offsets[i]] = switch (typeCodes[i]) {
- case 'L', '[' -> UNSAFE.getReference(obj, readKeys[i]);
+ case 'L', '[' ->
+ layouts[i] != 0
+ ? UNSAFE.getFlatValue(obj, readKeys[i], layouts[i], types[i - numPrimFields])
+ : UNSAFE.getReference(obj, readKeys[i]);
default -> throw new InternalError();
};
}
}
f.getDeclaringClass().getName() + "." +
f.getName() + " of type " +
f.getType().getName() + " in instance of " +
obj.getClass().getName());
}
- if (!dryRun)
- UNSAFE.putReference(obj, key, val);
+ if (!dryRun) {
+ if (layouts[i] != 0) {
+ UNSAFE.putFlatValue(obj, key, layouts[i], types[i - numPrimFields], val);
+ } else {
+ UNSAFE.putReference(obj, key, val);
+ }
+ }
}
default -> throw new InternalError();
}
}
}
/**
* A LRA cache of record deserialization constructors.
*/
@SuppressWarnings("serial")
- private static final class DeserializationConstructorsCache
- extends ConcurrentHashMap<DeserializationConstructorsCache.Key, MethodHandle> {
+ private static final class RecordConstructorsCache
+ extends ConcurrentHashMap<RecordConstructorsCache.Key, MethodHandle> {
// keep max. 10 cached entries - when the 11th element is inserted the oldest
// is removed and 10 remains - 11 is the biggest map size where internal
// table of 16 elements is sufficient (inserting 12th element would resize it to 32)
private static final int MAX_SIZE = 10;
private Key.Impl first, last; // first and last in FIFO queue
- DeserializationConstructorsCache() {
+ RecordConstructorsCache() {
// start small - if there is more than one shape of ObjectStreamClass
// deserialized, there will typically be two (current version and previous version)
super(2);
}
Class<?> fieldType(int i) { return fieldTypes[i]; }
}
}
}
- /** Record specific support for retrieving and binding stream field values. */
- static final class RecordSupport {
+ /** Support for retrieving and binding stream field values for alternative
+ * deserialization of record and factory-based value classes. */
+ static final class AlternativeDeserialization {
/**
- * Returns canonical record constructor adapted to take two arguments:
+ * Returns factory method handle adapted to take two arguments:
* {@code (byte[] primValues, Object[] objValues)}
* and return
* {@code Object}
*/
- static MethodHandle deserializationCtr(ObjectStreamClass desc) {
+ static MethodHandle getFactory(ObjectStreamClass desc) {
+ // check the cached value 1st
+ MethodHandle mh = desc.cachedAlternativeFactory;
+ if (mh != null) return mh;
+
+ mh = desc.isRecord() ? recordConstructor(desc) : deserializer(desc);
+
+ // store into cache
+ return desc.cachedAlternativeFactory = mh;
+ }
+
+ private static MethodHandle recordConstructor(ObjectStreamClass desc) {
// check the cached value 1st
- MethodHandle mh = desc.deserializationCtr;
+ MethodHandle mh = desc.cachedRecordConstructors.get(desc.getFields(false));
if (mh != null) return mh;
- mh = desc.deserializationCtrs.get(desc.getFields(false));
- if (mh != null) return desc.deserializationCtr = mh;
// retrieve record components
RecordComponent[] recordComponents = desc.forClass().getRecordComponents();
+ var types = Arrays.stream(recordComponents).map(RecordComponent::getType).toArray(Class<?>[]::new);
+ var names = Arrays.stream(recordComponents).map(RecordComponent::getName).toArray(String[]::new);
+ int count = recordComponents.length;
// retrieve the canonical constructor
// (T1, T2, ..., Tn):TR
mh = desc.getRecordConstructor();
+ mh = buildMethodHandle(desc, mh, types, names, count);
+
+ // store it into cache and return the 1st value stored
+ mh = desc.cachedRecordConstructors.putIfAbsentAndGet(desc.getFields(false), mh);
+
+ return mh;
+ }
+
+ private static MethodHandle deserializer(ObjectStreamClass desc) {
+ Executable deserializer = desc.deserializer;
+ var types = deserializer.getParameterTypes();
+ String[] names = deserializer.getDeclaredAnnotation(Deserializer.class).value();
+ int count = types.length;
+
+ MethodHandle mh;
+ var lookup = MethodHandles.publicLookup();
+ try {
+ mh = deserializer instanceof Method m ? lookup.unreflect(m)
+ : lookup.unreflectConstructor((Constructor<?>) deserializer);
+ } catch (ReflectiveOperationException e) {
+ throw new InternalError(e);
+ }
+
+ return buildMethodHandle(desc, mh, types, names, count);
+ }
+
+ private static MethodHandle buildMethodHandle(ObjectStreamClass desc,
+ MethodHandle mh,
+ Class<?>[] types,
+ String[] names,
+ int count) {
// 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();
+ 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
MethodHandle combiner = streamFieldExtractor(name, type, desc);
// fold byte[] privValues and Object[] objValues into argument at position i (Ti+1)
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 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);
+ return mh;
}
/** Returns the number of primitive fields for the given descriptor. */
private static int numberPrimValues(ObjectStreamClass desc) {
ObjectStreamField[] fields = desc.getFields();
< prev index next >