chaokunyang commented on code in PR #3861:
URL: https://github.com/apache/fory/pull/3861#discussion_r3592419704
##########
java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java:
##########
@@ -442,6 +444,58 @@ public BufferCallback getBufferCallback() {
return bufferCallback;
}
+ /**
+ * If {@code obj} carries a populated {@link ForyExtraFields} sink whose
remote {@code TypeDef}
+ * differs from the local type, emits that remote schema header and replays
every field (matched
+ * fields from the bean, unmatched fields from the sink) so a downstream
peer with the full schema
+ * can recover them, excluded from writeRef(obj,typeinfo) because it is only
called by methods
+ * which pass collection objects, and from writeNonRef(obj,typeinfo) which
is gated on
+ * useDeclaredTypeInfo and !crossLanguage ie. the path for mononmorphic
elements
+ */
+ public boolean tryWriteExtraFieldsSchema(TypeResolver resolver, TypeInfo
typeInfo, Object obj) {
+ FieldAccessor sinkAccessor = typeInfo.getExtraFieldsSinkAccessor();
+ if (sinkAccessor == null) {
+ return false;
+ }
+ ForyExtraFields extraField = (ForyExtraFields) sinkAccessor.getObject(obj);
+ if (extraField == null || extraField.getTypeDef() == null) {
+ return false;
+ }
+ long sinkTypeDefId = extraField.getTypeDef().getId();
+ // The object was deserialized under a different (fuller) remote schema.
Emit that remote schema
+ // header (from the writer-class TypeInfo) and replay all fields through
the reader-class
+ // serializer that captured them, whose field order follows the remote
schema.
+ TypeInfo headerTypeInfo = resolver.getTypeInfoByTypeDefId(sinkTypeDefId);
+ if (headerTypeInfo == null) {
+ throw new IllegalStateException(
+ "Cannot re-serialize "
+ + obj.getClass().getName()
+ + " with captured extra fields: this Fory instance does not have
the object's original "
+ + "remote schema (TypeDef id "
+ + sinkTypeDefId
+ + ") cached, so it cannot emit the schema header needed to
preserve those fields. "
+ + "Replay needs a Fory that has deserialized a payload carrying
that schema and still "
+ + "has it cached, ie the instance that produced this object.
Each instance "
+ + "retains only a bounded number of distinct remote schemas, so
under very high schema "
+ + "churn the schema may not be retained and the captured fields
cannot be replayed.");
+ }
+ Serializer<?> replaySerializer =
+ resolver.getExtraFieldsWriteSerializer(obj.getClass(), sinkTypeDefId);
+ if (replaySerializer == null) {
+ throw new IllegalStateException(
+ "Cannot replay captured extra fields on "
+ + obj.getClass()
+ + ": remote TypeDef "
+ + sinkTypeDefId
+ + " has not been read into this class on this Fory instance.");
+ }
+ resolver.writeTypeInfo(this, headerTypeInfo);
Review Comment:
Preserve the original type header when the writer class is unavailable
When the intermediary cannot load the original writer class,
`headerTypeInfo.type` is `UnknownStruct` with `NONEXISTENT_META_SHARED_ID`.
Calling `writeTypeInfo` here emits that placeholder and then writes a
target-class replay body. The normal unknown-class path must go through
`UnknownStructSerializer.write`, which rewrites the placeholder to the original
compatible type id/user id and emits the remote TypeDef. Without that rewrite,
the forwarded stream is undecodable in the rolling-deployment case this feature
is intended to support. Please route this header through a schema-aware writer
and cover it with isolated classloaders.
##########
java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java:
##########
@@ -442,6 +444,58 @@ public BufferCallback getBufferCallback() {
return bufferCallback;
}
+ /**
+ * If {@code obj} carries a populated {@link ForyExtraFields} sink whose
remote {@code TypeDef}
+ * differs from the local type, emits that remote schema header and replays
every field (matched
+ * fields from the bean, unmatched fields from the sink) so a downstream
peer with the full schema
+ * can recover them, excluded from writeRef(obj,typeinfo) because it is only
called by methods
+ * which pass collection objects, and from writeNonRef(obj,typeinfo) which
is gated on
+ * useDeclaredTypeInfo and !crossLanguage ie. the path for mononmorphic
elements
+ */
+ public boolean tryWriteExtraFieldsSchema(TypeResolver resolver, TypeInfo
typeInfo, Object obj) {
+ FieldAccessor sinkAccessor = typeInfo.getExtraFieldsSinkAccessor();
+ if (sinkAccessor == null) {
+ return false;
+ }
+ ForyExtraFields extraField = (ForyExtraFields) sinkAccessor.getObject(obj);
+ if (extraField == null || extraField.getTypeDef() == null) {
+ return false;
+ }
+ long sinkTypeDefId = extraField.getTypeDef().getId();
+ // The object was deserialized under a different (fuller) remote schema.
Emit that remote schema
+ // header (from the writer-class TypeInfo) and replay all fields through
the reader-class
+ // serializer that captured them, whose field order follows the remote
schema.
+ TypeInfo headerTypeInfo = resolver.getTypeInfoByTypeDefId(sinkTypeDefId);
+ if (headerTypeInfo == null) {
+ throw new IllegalStateException(
+ "Cannot re-serialize "
+ + obj.getClass().getName()
+ + " with captured extra fields: this Fory instance does not have
the object's original "
+ + "remote schema (TypeDef id "
+ + sinkTypeDefId
+ + ") cached, so it cannot emit the schema header needed to
preserve those fields. "
+ + "Replay needs a Fory that has deserialized a payload carrying
that schema and still "
+ + "has it cached, ie the instance that produced this object.
Each instance "
+ + "retains only a bounded number of distinct remote schemas, so
under very high schema "
+ + "churn the schema may not be retained and the captured fields
cannot be replayed.");
+ }
+ Serializer<?> replaySerializer =
+ resolver.getExtraFieldsWriteSerializer(obj.getClass(), sinkTypeDefId);
+ if (replaySerializer == null) {
+ throw new IllegalStateException(
+ "Cannot replay captured extra fields on "
+ + obj.getClass()
+ + ": remote TypeDef "
+ + sinkTypeDefId
+ + " has not been read into this class on this Fory instance.");
+ }
+ resolver.writeTypeInfo(this, headerTypeInfo);
Review Comment:
Key replay metadata by the remote TypeDef identity
`writeSharedClassMeta` deduplicates metadata by `typeInfo.type`
(`Class<?>`), but two remote versions of the same class have different TypeDef
ids. If captured objects from both versions are forwarded in one root graph,
the second header references the first TypeDef while its body is written by the
second replay serializer, causing field corruption or reader-index drift.
Replay metadata needs to be keyed by the checked TypeDef identity, with a
regression test containing two versions of the same class in one graph.
##########
java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleSerializer.java:
##########
@@ -225,6 +238,71 @@ public void write(WriteContext writeContext, T value) {
serializer.write(writeContext, value);
}
+ /**
+ * Writes all fields in remote-sorted order under the remote TypeDef schema:
matched fields are
+ * read from local getters, unmatched fields are read from the {@link
ForyExtraFields} map using
+ * fieldInfo.
+ */
+ private void writeFieldsIncludingExtraFields(
+ WriteContext writeContext, T value, ForyExtraFields extraField) {
+ RefWriter refWriter = writeContext.getRefWriter();
+ Generics generics = writeContext.getGenerics();
+ for (SerializationFieldInfo fieldInfo : allFields) {
+ writeFieldByCodecCategory(writeContext, value, refWriter, generics,
fieldInfo, extraField);
+ }
+ }
+
+ private void writeFieldByCodecCategory(
+ WriteContext writeContext,
+ T value,
+ RefWriter refWriter,
+ Generics generics,
+ SerializationFieldInfo fieldInfo,
+ ForyExtraFields extraField) {
+
+ MemoryBuffer buffer = writeContext.getBuffer();
+
+ boolean useExtraField = false;
+ Object fieldValue = null;
+
+ Object temp = extraField.getOrDefault(fieldInfo.descriptor.getName(),
NOT_FOUND);
+ if (temp != NOT_FOUND) {
+ useExtraField = true;
+ fieldValue = temp; // might be null, which is valid
+ }
+
+ switch (fieldInfo.codecCategory) {
+ case BUILD_IN:
+ if (useExtraField) {
+ AbstractObjectSerializer.writeBuildInFieldValue(
+ writeContext, typeResolver, refWriter, fieldInfo, buffer,
fieldValue);
+ } else {
+ AbstractObjectSerializer.writeBuildInField(
Review Comment:
Handle converted fields when replaying the remote schema
A compatible scalar conversion such as remote `int score` to local `long
score` produces a descriptor with a converter but no `fieldAccessor`. If any
other field populates the sink, this branch calls `writeBuildInField` with that
null accessor. The generated path has the same bug because it treats every
`descriptor.getField() == null` as an unmatched field and reads an absent sink
entry. Please define the reverse replay behavior for converter fields (or
preserve their remote value) and test interpreter and codegen with a converted
field plus an extra field.
##########
java/fory-core/src/main/java/org/apache/fory/serializer/ForyExtraFields.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.serializer;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.fory.collection.ClassValueCache;
+import org.apache.fory.meta.TypeDef;
+import org.apache.fory.reflect.FieldAccessor;
+
+/**
+ * Opt-in sink for compatible-mode extra fields. A class participates by
declaring a field of this
+ * type; the serialization framework detects it, excludes it from the normal
field set, and routes
+ * unmatched remote fields into it instead of discarding them.
+ *
+ * <p>The remote {@link TypeDef} is stored transiently so that on
re-serialization the framework can
+ * emit the remote schema header and replay all fields in their original
order, allowing a
+ * downstream peer with the full schema to recover them.
+ */
+public final class ForyExtraFields {
+
+ private final Map<String, Object> fields = new HashMap<>();
+
+ public Object get(String name) {
+ return fields.get(name);
+ }
+
+ public Object getOrDefault(String name, Object defaultValue) {
+ return fields.getOrDefault(name, defaultValue);
+ }
+
+ public boolean isEmpty() {
+ return fields.isEmpty();
+ }
+
+ public boolean containsKey(String name) {
+ return fields.containsKey(name);
+ }
+
+ Object put(String name, Object value) {
+ return fields.put(name, value);
+ }
+
+ /**
+ * GC-transparent class-keyed cache: on JVM this is backed by ClassValue so
entries do not prevent
+ * the associated Class (or its ClassLoader) from being collected. On
Android/GraalVM it falls
+ * back to a ConcurrentHashMap.
+ */
+ private static final ClassValueCache<Optional<FieldAccessor>> SINK_CACHE =
+ ClassValueCache.newClassKeyCache(16);
+
+ public static Field findSinkField(Class<?> cls) {
+ return SINK_CACHE
+ .get(cls, () -> scanForExtraField(cls))
+ .map(FieldAccessor::getField)
+ .orElse(null);
+ }
+
+ /** Returns a {@link FieldAccessor} for the {@link ForyExtraFields} sink
field on {@code cls}. */
+ public static FieldAccessor findSinkAccessor(Class<?> cls) {
+ return SINK_CACHE.get(cls, () -> scanForExtraField(cls)).orElse(null);
+ }
+
+ private static Optional<FieldAccessor> scanForExtraField(Class<?> cls) {
+ for (Class<?> c = cls; c != null && c != Object.class; c =
c.getSuperclass()) {
+ for (Field f : c.getDeclaredFields()) {
+ if (ForyExtraFields.class == f.getType()) {
+ return Optional.of(FieldAccessor.createAccessor(f));
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ // Excluded from Java serialization because TypeDef is runtime metadata used
+ // only for Fory serialization. If a ForyExtraFields instance is serialized
+ // with ObjectOutputStream, this field will be null after deserialization, so
+ // the captured extra fields cannot later be replayed by Fory.
+ private transient TypeDef typeDef;
+
+ public TypeDef getTypeDef() {
+ return typeDef;
+ }
+
+ public static void capture(
+ Object target, FieldAccessor sinkAccessor, TypeDef typeDef, String name,
Object value) {
+ ForyExtraFields extraField = (ForyExtraFields)
sinkAccessor.getObject(target);
+ if (extraField == null) {
Review Comment:
Attach the TypeDef when the sink is already initialized
This assigns `typeDef` only when the sink field was null. A common
declaration such as `final ForyExtraFields extraFields = new ForyExtraFields()`
captures map entries but leaves `typeDef` null, so `tryWriteExtraFieldsSchema`
silently skips replay and the downstream peer loses the unknown fields. On
first capture, an existing unbound sink also needs to bind the remote TypeDef,
and subsequent captures should verify that the schema identity is consistent.
##########
java/fory-core/src/main/java/org/apache/fory/serializer/ForyExtraFields.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.serializer;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.fory.collection.ClassValueCache;
+import org.apache.fory.meta.TypeDef;
+import org.apache.fory.reflect.FieldAccessor;
+
+/**
+ * Opt-in sink for compatible-mode extra fields. A class participates by
declaring a field of this
+ * type; the serialization framework detects it, excludes it from the normal
field set, and routes
+ * unmatched remote fields into it instead of discarding them.
+ *
+ * <p>The remote {@link TypeDef} is stored transiently so that on
re-serialization the framework can
+ * emit the remote schema header and replay all fields in their original
order, allowing a
+ * downstream peer with the full schema to recover them.
+ */
+public final class ForyExtraFields {
+
+ private final Map<String, Object> fields = new HashMap<>();
Review Comment:
Use the complete remote field identity as the sink key
A simple field name is not unique in a native Java TypeDef: superclass and
subclass layers may legally contain fields with the same name, distinguished by
declaring class and field id/name. Capturing both into this map overwrites one
value, and replay writes both slots from the surviving value. Tagged fields are
also decoded as `$tagN` when the writer class is unavailable, so the documented
lookup by original name cannot work. Please store a stable schema field
identity and expose an unambiguous lookup contract.
##########
java/fory-core/src/main/java/org/apache/fory/serializer/ForyExtraFields.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.serializer;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.fory.collection.ClassValueCache;
+import org.apache.fory.meta.TypeDef;
+import org.apache.fory.reflect.FieldAccessor;
+
+/**
+ * Opt-in sink for compatible-mode extra fields. A class participates by
declaring a field of this
+ * type; the serialization framework detects it, excludes it from the normal
field set, and routes
+ * unmatched remote fields into it instead of discarding them.
+ *
+ * <p>The remote {@link TypeDef} is stored transiently so that on
re-serialization the framework can
+ * emit the remote schema header and replay all fields in their original
order, allowing a
+ * downstream peer with the full schema to recover them.
+ */
+public final class ForyExtraFields {
+
+ private final Map<String, Object> fields = new HashMap<>();
+
+ public Object get(String name) {
+ return fields.get(name);
+ }
+
+ public Object getOrDefault(String name, Object defaultValue) {
+ return fields.getOrDefault(name, defaultValue);
+ }
+
+ public boolean isEmpty() {
+ return fields.isEmpty();
+ }
+
+ public boolean containsKey(String name) {
+ return fields.containsKey(name);
+ }
+
+ Object put(String name, Object value) {
+ return fields.put(name, value);
+ }
+
+ /**
+ * GC-transparent class-keyed cache: on JVM this is backed by ClassValue so
entries do not prevent
+ * the associated Class (or its ClassLoader) from being collected. On
Android/GraalVM it falls
+ * back to a ConcurrentHashMap.
+ */
+ private static final ClassValueCache<Optional<FieldAccessor>> SINK_CACHE =
+ ClassValueCache.newClassKeyCache(16);
+
+ public static Field findSinkField(Class<?> cls) {
+ return SINK_CACHE
+ .get(cls, () -> scanForExtraField(cls))
+ .map(FieldAccessor::getField)
+ .orElse(null);
+ }
+
+ /** Returns a {@link FieldAccessor} for the {@link ForyExtraFields} sink
field on {@code cls}. */
+ public static FieldAccessor findSinkAccessor(Class<?> cls) {
+ return SINK_CACHE.get(cls, () -> scanForExtraField(cls)).orElse(null);
+ }
+
+ private static Optional<FieldAccessor> scanForExtraField(Class<?> cls) {
+ for (Class<?> c = cls; c != null && c != Object.class; c =
c.getSuperclass()) {
+ for (Field f : c.getDeclaredFields()) {
+ if (ForyExtraFields.class == f.getType()) {
+ return Optional.of(FieldAccessor.createAccessor(f));
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ // Excluded from Java serialization because TypeDef is runtime metadata used
+ // only for Fory serialization. If a ForyExtraFields instance is serialized
+ // with ObjectOutputStream, this field will be null after deserialization, so
+ // the captured extra fields cannot later be replayed by Fory.
+ private transient TypeDef typeDef;
+
+ public TypeDef getTypeDef() {
+ return typeDef;
+ }
+
+ public static void capture(
+ Object target, FieldAccessor sinkAccessor, TypeDef typeDef, String name,
Object value) {
+ ForyExtraFields extraField = (ForyExtraFields)
sinkAccessor.getObject(target);
+ if (extraField == null) {
+ extraField = new ForyExtraFields();
Review Comment:
Charge retained sink owners to the graph-memory budget
The first capture retains a new `ForyExtraFields`, `HashMap`, backing/entry
storage, and potentially a boxed primitive, but neither interpreter nor
generated callers reserve graph memory for these owners. A valid list
containing many partial objects can therefore grow the retained result far
beyond `maxGraphMemoryBytes`. The compatible serializer/codegen owner should
reserve stable lower-bound costs before allocating or growing the sink, and the
budget rejection path needs a regression test.
##########
java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleSerializer.java:
##########
@@ -285,7 +373,8 @@ private void readFields(ReadContext readContext, Object[]
fields) {
RefReader refReader = readContext.getRefReader();
Generics generics = readContext.getGenerics();
for (SerializationFieldInfo fieldInfo : allFields) {
- fields[counter++] = readField(readContext, refReader, generics,
fieldInfo, buffer, null);
+ fields[counter++] =
Review Comment:
Do not opt records into a sink path that cannot capture fields
A record component of type `ForyExtraFields` is detected as a sink, but the
interpreter record path reads into an `Object[]` with `captureUnmatched=false`,
so unknown fields are still skipped. Generated code attempts to call `capture`
before the record exists, using the record collector/array rather than a record
instance, and fails on the accessor receiver. Please either implement
constructor-owned sink collection for records or reject record sinks explicitly
in code and documentation.
##########
java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java:
##########
@@ -386,22 +463,93 @@ private Expression skipField(Descriptor descriptor) {
remoteFieldInfo(descriptor));
}
+ @Override
+ protected Expression getFieldValue(Expression bean, Descriptor descriptor) {
+ if (extraFieldsSinkField != null && descriptor.getField() == null) {
+ if (extraFieldsSinkExpr == null || extraFieldsSinkExprBean != bean) {
+ extraFieldsSinkExprBean = bean;
+ extraFieldsSinkExpr =
+ tryInlineCast(
+ new Invoke(extraFieldsSinkAccessorExpr(), "getObject",
OBJECT_TYPE, bean),
+ TypeRef.of(ForyExtraFields.class));
+ }
+ Invoke raw =
+ new Invoke(
+ extraFieldsSinkExpr, "get", OBJECT_TYPE,
Literal.ofString(descriptor.getName()));
+ return tryInlineCast(raw, descriptor.getTypeRef());
+ }
+ return super.getFieldValue(bean, descriptor);
+ }
+
@Override
protected Expression setFieldValue(Expression bean, Descriptor descriptor,
Expression value) {
if (descriptor.getField() == null) {
FieldConverter<?> converter = descriptor.getFieldConverter();
if (converter != null) {
return setFieldConverterTargetValue(bean, descriptor, converter,
value);
}
- // Field doesn't exist in current class, skip set this field value.
+ // If Field doesn't exist in current class,
+ if (extraFieldsSinkField != null) {
+ return new StaticInvoke(
+ ForyExtraFields.class,
+ "capture",
+ TypeRef.of(void.class),
+ bean,
+ extraFieldsSinkAccessorExpr(),
+ typeDefFieldExpr(),
+ Literal.ofString(descriptor.getName()),
+ value);
+ }
// Note that the field value shouldn't be an inlined value, otherwise
field value read may
- // be ignored.
- // Add an ignored call here to make expression type to void.
+ // be ignored. Add an ignored call here to make expression type to void.
return new StaticInvoke(ExceptionUtils.class, "ignore", value);
}
return super.setFieldValue(bean, descriptor, value);
}
+ private String ensureLazyFieldGenerated(
+ String cachedFieldName, String nameConstant, TypeRef<?> fieldType,
Expression initializer) {
+ if (cachedFieldName == null) {
+ cachedFieldName = ctx.newName(nameConstant);
+ ctx.addField(false, true, ctx.type(fieldType), cachedFieldName,
initializer);
+ }
+ return cachedFieldName;
+ }
+
+ private Expression extraFieldsSinkAccessorExpr() {
+ extraFieldsSinkAccessorName =
+ ensureLazyFieldGenerated(
+ extraFieldsSinkAccessorName,
+ EXTRA_FIELDS_SINK_ACCESSOR_NAME,
+ TypeRef.of(FieldAccessor.class),
+ new StaticInvoke(
+ FieldAccessor.class,
+ "createAccessor",
+ TypeRef.of(FieldAccessor.class),
+ getOrCreateField(
Review Comment:
Keep the sink field's declaring class in generated accessors
The scanner returns the exact inherited sink `Field`, but this code discards
its declaring class and resolves only `beanClass + name`. If a subclass legally
hides the inherited sink with a different-typed field of the same name,
generated code binds the subclass field. That causes valid input to fail and
can write a `ForyExtraFields` reference into the wrong field on Unsafe-based
accessors. Please retain the exact `Field`/accessor or resolve it through the
original declaring class.
##########
java/fory-core/src/main/java/org/apache/fory/serializer/ForyExtraFields.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.serializer;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.fory.collection.ClassValueCache;
+import org.apache.fory.meta.TypeDef;
+import org.apache.fory.reflect.FieldAccessor;
+
+/**
+ * Opt-in sink for compatible-mode extra fields. A class participates by
declaring a field of this
+ * type; the serialization framework detects it, excludes it from the normal
field set, and routes
+ * unmatched remote fields into it instead of discarding them.
+ *
+ * <p>The remote {@link TypeDef} is stored transiently so that on
re-serialization the framework can
+ * emit the remote schema header and replay all fields in their original
order, allowing a
+ * downstream peer with the full schema to recover them.
+ */
+public final class ForyExtraFields {
+
+ private final Map<String, Object> fields = new HashMap<>();
+
+ public Object get(String name) {
+ return fields.get(name);
+ }
+
+ public Object getOrDefault(String name, Object defaultValue) {
+ return fields.getOrDefault(name, defaultValue);
+ }
+
+ public boolean isEmpty() {
+ return fields.isEmpty();
+ }
+
+ public boolean containsKey(String name) {
+ return fields.containsKey(name);
+ }
+
+ Object put(String name, Object value) {
+ return fields.put(name, value);
+ }
+
+ /**
+ * GC-transparent class-keyed cache: on JVM this is backed by ClassValue so
entries do not prevent
+ * the associated Class (or its ClassLoader) from being collected. On
Android/GraalVM it falls
+ * back to a ConcurrentHashMap.
+ */
+ private static final ClassValueCache<Optional<FieldAccessor>> SINK_CACHE =
+ ClassValueCache.newClassKeyCache(16);
+
+ public static Field findSinkField(Class<?> cls) {
+ return SINK_CACHE
+ .get(cls, () -> scanForExtraField(cls))
+ .map(FieldAccessor::getField)
+ .orElse(null);
+ }
+
+ /** Returns a {@link FieldAccessor} for the {@link ForyExtraFields} sink
field on {@code cls}. */
+ public static FieldAccessor findSinkAccessor(Class<?> cls) {
+ return SINK_CACHE.get(cls, () -> scanForExtraField(cls)).orElse(null);
+ }
+
+ private static Optional<FieldAccessor> scanForExtraField(Class<?> cls) {
+ for (Class<?> c = cls; c != null && c != Object.class; c =
c.getSuperclass()) {
+ for (Field f : c.getDeclaredFields()) {
+ if (ForyExtraFields.class == f.getType()) {
Review Comment:
Ignore static fields when discovering an extra-fields sink
Discovery checks only the field type, so a `static ForyExtraFields` constant
is selected even though `FieldAccessor.createAccessor` rejects static fields.
Serializer initialization then fails for an otherwise valid model, including
xlang or non-compatible configurations where this feature should be irrelevant.
Please restrict discovery to instance fields and add a static-field regression
test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]