Restore serialization support for TypeDescriptor - #37109
Open
junhyeong9812 wants to merge 1 commit into
Open
Conversation
TypeDescriptor declares Serializable, but instances created from a Field, a MethodParameter or a Property fail to serialize since the annotation lookup has been deferred to a serializable lambda that captures those non-serializable reflection objects. Mark the supplier transient, resolve the annotations in writeObject() so that the already serializable AnnotatedElementAdapter carries them, and rebuild the supplier from that adapter in readObject(), folding an empty adapter back into the shared EMPTY instance. See spring-projectsgh-33948 Signed-off-by: junhyeong9812 <pickjog@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
TypeDescriptorhas declaredSerializablesince 3.0, but instances created from aField, aMethodParameteror aPropertycan no longer be serialized. Writing such an instance fails withNotSerializableException.This is a regression from gh-33948 (
1a573d6e3c7, "Lazily retrieve TypeDescriptor annotations on demand"). Serialization still works in 6.2.1 and fails in 6.2.13, and the current code on 7.0.x and main behaves the same as 6.2.13.Problem
That commit replaced the eagerly resolved
AnnotatedElementAdapterfield with a serializable lambda:AnnotatedElementSupplierextendsSerializable, so the lambda is written to the stream as aSerializedLambdaalong with everything it captures. The captured values are the very reflection objects the descriptor was created from, and none of them are serializable:TypeDescriptor(Field)java.lang.reflect.FieldTypeDescriptor(MethodParameter)MethodParameterTypeDescriptor(Property)PropertyTypeDescriptor(ResolvableType, Class, Annotation[])Annotation[]Reproduction:
The failure occurs even when the annotations have already been resolved, since the supplier field itself is neither
transientnor cleared once the adapter has been cached.The existing
TypeDescriptorTests.serializable()test did not catch this becauseTypeDescriptor.forObject("")routes to the(ResolvableType, Class, Annotation[])constructor with anullannotation array, which is the one path that still serializes.Descriptors reaching a serialization boundary are not exotic:
ConversionFailedExceptionandConverterNotFoundExceptionboth hold non-transientTypeDescriptorfields, and those exceptions are serializable by virtue of beingThrowable.Fix
Mark the supplier
transientand let the already serializableAnnotatedElementAdaptercarry the annotations across the stream instead:Notes on the two hooks:
writeObject()resolves the annotations only at serialization time, so the lazy retrieval introduced by Expression performance regression due to missing annotation types on context classes #33948 is preserved for every other code path. Without it, a descriptor that has never been asked for its annotations would write anulladapter and lose them.readObject()runs the restored adapter throughAnnotatedElementAdapter.from(...)so that an empty adapter is folded back into the sharedEMPTYinstance.AnnotatedElementAdapter.isEmpty()is an identity check and the class has noreadResolve(), so without this theisEmpty()shortcut inhasAnnotation()andgetAnnotation()would be lost after a round trip.Tests cover all four constructors, a descriptor without annotations, a derived descriptor, and the lazy-versus-serialization timing.
Note on impact
Making the field
private transientremoves it from the defaultserialVersionUIDcomputation, so the computed UID changes (-4882614078662365050to1724818276882560505, serialized fields 4 to 3). Streams written by an earlier version are therefore rejected withInvalidClassExceptionrather than being read.Pinning the previous UID was considered and deliberately not done: with a matching UID the old supplier field would be read and discarded,
annotatedElementwould benullbecause the old code never forced resolution, and the annotations would be lost silently.TypeDescriptordoes not declare aserialVersionUIDand carries@SuppressWarnings("serial"), so cross-version stream compatibility was never part of its contract, and failing loudly seems preferable to dropping annotations quietly. Note also that streams containing aField,MethodParameterorPropertybased descriptor cannot exist today, since writing them is exactly what fails.One behavioural detail worth flagging: annotation resolution can now surface during serialization, so a missing annotation class shows up as
TypeNotPresentExceptionfromwriteObject()where it previously surfaced asNotSerializableException. Both cases fail, and the former carries more information about the cause.Unrelated to this change,
TypeDescriptor.getElementTypeDescriptor()results still fail to serialize withNotSerializableException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl, before and after this fix. That one originates in theResolvableTypeheld by the descriptor rather than in the annotation supplier, so it is left alone here.