diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java index eeb582fb2a..5313f72776 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java @@ -40,6 +40,12 @@ public class CodecUtils { // Cache key includes configHash to distinguish between xlang and non-xlang modes private static final ConcurrentHashMap, Integer>, Class> graalvmSerializers = new ConcurrentHashMap<>(); + // Generated layer serializers need the original layer metadata to bootstrap their delegate + // synchronously in the constructor. + // This is a process-lifetime cache keyed by generated serializer class, which mirrors the + // existing codegen cache behavior and is populated only on the regular JVM path. + private static final ConcurrentHashMap, MetaSharedLayerCodecContext> + metaSharedLayerCodecContexts = new ConcurrentHashMap<>(); // TODO(chaokunyang) how to uninstall org.apache.fory.codegen/builder classes for graalvm build // time @@ -78,16 +84,26 @@ public static Class> loadOrGenMetaSharedCodecClass( public static Class> loadOrGenMetaSharedLayerCodecClass( Class cls, Fory fory, TypeDef layerTypeDef, Class layerMarkerClass) { Preconditions.checkNotNull(fory); - return loadSerializer( - "loadOrGenMetaSharedLayerCodecClass", - cls, - fory, - () -> - loadOrGenCodecClass( - cls, - fory, - new MetaSharedLayerCodecBuilder( - TypeRef.of(cls), fory, layerTypeDef, layerMarkerClass))); + Class> serializerClass = + loadSerializer( + "loadOrGenMetaSharedLayerCodecClass", + cls, + fory, + () -> + loadOrGenCodecClass( + cls, + fory, + new MetaSharedLayerCodecBuilder( + TypeRef.of(cls), fory, layerTypeDef, layerMarkerClass))); + if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + metaSharedLayerCodecContexts.putIfAbsent( + serializerClass, new MetaSharedLayerCodecContext(layerTypeDef, layerMarkerClass)); + } + return serializerClass; + } + + static MetaSharedLayerCodecContext getMetaSharedLayerCodecContext(Class serializerClass) { + return metaSharedLayerCodecContexts.get(serializerClass); } @SuppressWarnings("unchecked") @@ -171,4 +187,14 @@ private static Class> loadSerializer( throw new RuntimeException(e); } } + + static final class MetaSharedLayerCodecContext { + final TypeDef layerTypeDef; + final Class layerMarkerClass; + + MetaSharedLayerCodecContext(TypeDef layerTypeDef, Class layerMarkerClass) { + this.layerTypeDef = layerTypeDef; + this.layerMarkerClass = layerMarkerClass; + } + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedLayerCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedLayerCodecBuilder.java index 1c0d6faf84..7dc4dc1b61 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedLayerCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedLayerCodecBuilder.java @@ -32,10 +32,8 @@ import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.meta.TypeDef; import org.apache.fory.reflect.TypeRef; -import org.apache.fory.serializer.CodegenSerializer; import org.apache.fory.serializer.MetaSharedLayerSerializer; import org.apache.fory.serializer.MetaSharedLayerSerializerBase; -import org.apache.fory.serializer.Serializers; import org.apache.fory.type.Descriptor; import org.apache.fory.type.DescriptorGrouper; import org.apache.fory.util.ExceptionUtils; @@ -132,22 +130,27 @@ protected void addCommonImports() { @SuppressWarnings({"unchecked", "rawtypes"}) public static MetaSharedLayerSerializerBase setCodegenSerializer( Fory fory, Class cls, GeneratedMetaSharedLayerSerializer s) { - if (GraalvmSupport.isGraalRuntime()) { + // In native-image mode, including build time, do not rely on the cached layer bootstrap + // context. We only populate that cache on the regular JVM path, so native-image execution + // must resolve through the type resolver instead. + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { return (MetaSharedLayerSerializerBase) typeResolver(fory, r -> r.getSerializer(s.getType())); } - // This method hold jit lock, so create jit serializer async to avoid block serialization. - // Use MetaSharedLayerSerializer as fallback since it's compatible with - // MetaSharedLayerSerializerBase - Class serializerClass = - fory.getJITContext() - .registerSerializerJITCallback( - () -> MetaSharedLayerSerializer.class, - () -> CodegenSerializer.loadCodegenSerializer(fory, s.getType()), - c -> - s.serializer = - (MetaSharedLayerSerializerBase) - Serializers.newSerializer(fory, s.getType(), c)); - return (MetaSharedLayerSerializerBase) Serializers.newSerializer(fory, cls, serializerClass); + // Layer serializers don't have a generic no-arg/newSerializer construction path. The + // outer ObjectStreamSerializer JIT step already resolved the layer TypeDef and marker, so + // generated serializers look up that cached context here and bootstrap the interpreter + // delegate synchronously in their constructor. + CodecUtils.MetaSharedLayerCodecContext context = + CodecUtils.getMetaSharedLayerCodecContext(s.getClass()); + Preconditions.checkNotNull( + context, + "Missing layer codec context for generated serializer " + + s.getClass() + + "; loadOrGenMetaSharedLayerCodecClass should cache it before serializer" + + " instantiation."); + s.serializer = + new MetaSharedLayerSerializer(fory, cls, context.layerTypeDef, context.layerMarkerClass); + return s.serializer; } @Override diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java index 35e4b2625f..de3fd54adc 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/ObjectStreamSerializerTest.java @@ -39,14 +39,18 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import lombok.EqualsAndHashCode; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; +import org.apache.fory.builder.Generated; import org.apache.fory.config.CompatibleMode; import org.apache.fory.config.Language; import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.util.Preconditions; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -1136,6 +1140,107 @@ public void testNestedObjectSerialization(CompatibleMode compatible) { assertEquals(result.nestedList.get(1).nestedValue, "list2"); } + public static class AsyncTreeSetSubclass extends TreeSet { + public AsyncTreeSetSubclass() {} + } + + public static class AsyncTreeMapSubclass extends TreeMap { + public AsyncTreeMapSubclass() {} + } + + @EqualsAndHashCode + public static class AsyncLayerJitContainer implements Serializable { + private String name; + private AsyncTreeSetSubclass values; + private AsyncTreeMapSubclass attributes; + + public AsyncLayerJitContainer() {} + + public AsyncLayerJitContainer( + String name, AsyncTreeSetSubclass values, AsyncTreeMapSubclass attributes) { + this.name = name; + this.values = values; + this.attributes = attributes; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + } + } + + @Test(timeOut = 60000) + public void testAsyncCompilationNestedTreeCollectionsCompatibleMode() + throws InterruptedException { + Fory fory = newCompatibleAsyncObjectStreamFory(true); + fory.registerSerializer( + AsyncLayerJitContainer.class, + new ObjectStreamSerializer(fory, AsyncLayerJitContainer.class)); + fory.registerSerializer( + AsyncTreeSetSubclass.class, new ObjectStreamSerializer(fory, AsyncTreeSetSubclass.class)); + fory.registerSerializer( + AsyncTreeMapSubclass.class, new ObjectStreamSerializer(fory, AsyncTreeMapSubclass.class)); + + AsyncTreeSetSubclass values = new AsyncTreeSetSubclass(); + values.add("one"); + values.add("two"); + AsyncTreeMapSubclass attributes = new AsyncTreeMapSubclass(); + attributes.put("alpha", "A"); + attributes.put("beta", "B"); + AsyncLayerJitContainer obj = new AsyncLayerJitContainer("container", values, attributes); + + serDeCheckSerializer(fory, obj, "ObjectStreamSerializer"); + + waitForGeneratedLayerSerializer(fory, AsyncLayerJitContainer.class); + waitForGeneratedLayerSerializer(fory, AsyncTreeSetSubclass.class); + waitForGeneratedLayerSerializer(fory, AsyncTreeMapSubclass.class); + + serDeCheckSerializer(fory, obj, "ObjectStreamSerializer"); + } + + @Test(timeOut = 60000) + public void testAsyncCompilationTreeSetSubclassObjectStreamSerializer() + throws InterruptedException { + Fory fory = newCompatibleAsyncObjectStreamFory(true); + fory.registerSerializer( + AsyncTreeSetSubclass.class, new ObjectStreamSerializer(fory, AsyncTreeSetSubclass.class)); + + AsyncTreeSetSubclass values = new AsyncTreeSetSubclass(); + values.add("one"); + values.add("two"); + + serDeCheckSerializer(fory, values, "ObjectStreamSerializer"); + waitForGeneratedLayerSerializer(fory, AsyncTreeSetSubclass.class); + serDeCheckSerializer(fory, values, "ObjectStreamSerializer"); + } + + @Test + public void testTreeCollectionsStillWorkWithoutAsyncCompilation() { + Fory fory = newCompatibleAsyncObjectStreamFory(false); + fory.registerSerializer( + AsyncLayerJitContainer.class, + new ObjectStreamSerializer(fory, AsyncLayerJitContainer.class)); + fory.registerSerializer( + AsyncTreeSetSubclass.class, new ObjectStreamSerializer(fory, AsyncTreeSetSubclass.class)); + fory.registerSerializer( + AsyncTreeMapSubclass.class, new ObjectStreamSerializer(fory, AsyncTreeMapSubclass.class)); + + AsyncTreeSetSubclass values = new AsyncTreeSetSubclass(); + values.add("one"); + values.add("two"); + AsyncTreeMapSubclass attributes = new AsyncTreeMapSubclass(); + attributes.put("alpha", "A"); + attributes.put("beta", "B"); + + serDeCheckSerializer( + fory, + new AsyncLayerJitContainer("container", values, attributes), + "ObjectStreamSerializer"); + } + // ==================== Circular Reference in Custom Serialization ==================== /** Class with potential circular reference. */ @@ -1279,4 +1384,45 @@ public void testAllPrimitiveTypes(CompatibleMode compatible) { assertEquals(result.charVal, 'A'); assertEquals(result.boolVal, true); } + + private Fory newCompatibleAsyncObjectStreamFory(boolean asyncCompilation) { + return Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(true) + .withCodegen(true) + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withAsyncCompilation(asyncCompilation) + .build(); + } + + private void waitForGeneratedLayerSerializer(Fory fory, Class type) + throws InterruptedException { + long deadline = System.currentTimeMillis() + 30_000; + while (System.currentTimeMillis() < deadline) { + if (hasGeneratedLayerSerializer(fory, type)) { + return; + } + Thread.sleep(10); + } + Assert.fail("Timed out waiting for generated layer serializer for " + type.getName()); + } + + private boolean hasGeneratedLayerSerializer(Fory fory, Class type) { + Serializer serializer = fory.getTypeResolver().getSerializer(type); + if (!(serializer instanceof ObjectStreamSerializer)) { + return false; + } + Object[] slotsInfos = (Object[]) ReflectionUtils.getObjectFieldValue(serializer, "slotsInfos"); + if (slotsInfos.length == 0) { + return false; + } + for (Object slotsInfo : slotsInfos) { + Object slotsSerializer = ReflectionUtils.getObjectFieldValue(slotsInfo, "slotsSerializer"); + if (!(slotsSerializer instanceof Generated)) { + return false; + } + } + return true; + } }