< prev index next >

src/java.base/share/classes/java/io/ObjectStreamClass.java

Print this page
@@ -1,7 +1,7 @@
  /*
-  * 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

@@ -27,10 +27,11 @@
  
  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;

@@ -40,19 +41,23 @@
  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.

@@ -115,10 +120,13 @@
      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 */

@@ -180,14 +188,18 @@
      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;

@@ -336,10 +348,11 @@
          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;

@@ -362,11 +375,21 @@
                      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",

@@ -397,11 +420,11 @@
          }
  
          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");

@@ -534,24 +557,26 @@
          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();

@@ -814,10 +839,28 @@
      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() {

@@ -1292,10 +1335,76 @@
              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.
       */

@@ -1787,13 +1896,15 @@
          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

@@ -1805,10 +1916,11 @@
              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<>();
  
  

@@ -1819,10 +1931,11 @@
                      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);
                  }
              }

@@ -1913,11 +2026,14 @@
               * 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();
                  };
              }
          }
  

@@ -1963,12 +2079,17 @@
                                  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();
                  }
              }
          }

@@ -2097,20 +2218,20 @@
  
      /**
       * 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);
          }
  

@@ -2206,43 +2327,87 @@
                  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)

@@ -2250,14 +2415,11 @@
                  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 >