From 6ee3ef6af5e215b854b0901e6a43737e86318c31 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:55:39 +0200 Subject: [PATCH] Avoid unnecessarily synthesizing meta-annotations with attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In commit 622fc3edf7, I introduced a check in TypeMappedAnnotation#isSynthesizable() intended to force synthesis when an attribute value needs to be resolved from a different level of a multi-level annotation hierarchy whose root annotation does not redeclare the target attribute itself. That check tested if `resolvedMirrors.length > 0` for a meta-annotation; however, resolvedMirrors is always sized according to the number of attributes declared by the mapped annotation type, regardless of whether any of those attributes actually participate in mirroring or an @⁠AliasFor override. As a result, the check effectively synthesized any meta-annotation that declares at least one attribute, which reintroduced the unnecessary-synthesis behavior that commit d6768ccc18 had fixed, merely narrowed to meta-annotations with attributes. This commit replaces that overly broad check with a precise one in AnnotationTypeMapping#computeSynthesizableFlag(), which now also considers whether any attribute's value must be resolved from a different annotation in the meta-annotation hierarchy (tracked via annotationValueSource). This correctly identifies the original multi-level hierarchy scenario without over-matching on ordinary meta-annotations that have nothing to merge or override. See gh-28704 See gh-28716 Closes gh-37135 --- .../annotation/AnnotationTypeMapping.java | 11 ++++++ .../core/annotation/MergedAnnotation.java | 3 ++ .../core/annotation/TypeMappedAnnotation.java | 8 +---- .../annotation/MergedAnnotationsTests.java | 34 +++++++++++++++++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java index 563341654e1..b423301fbf4 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java @@ -275,6 +275,17 @@ final class AnnotationTypeMapping { return true; } + // Does an attribute value need to be resolved from a different annotation + // in the meta-annotation hierarchy? This can happen when an attribute is + // overridden via @AliasFor further up a multi-level annotation hierarchy + // whose root annotation does not redeclare the target attribute itself, + // in which case the override cannot be tracked via aliasMappings/aliasedBy. + for (int i = 0; i < this.annotationValueSource.length; i++) { + if (this.annotationValueSource[i] != null && this.annotationValueSource[i] != this) { + return true; + } + } + // Has nested annotations or arrays of annotations that are synthesizable? if (getAttributes().hasNestedAnnotation()) { AttributeMethods attributeMethods = getAttributes(); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java index bed8407fc9d..8ae3b01a5fc 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java @@ -513,6 +513,9 @@ public interface MergedAnnotation { * it has not already been synthesized and one of the following is true. * diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java index f28f7760582..67d181818fe 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -354,8 +354,7 @@ final class TypeMappedAnnotation extends AbstractMergedAnn /** * Determine if the supplied annotation has not already been synthesized - * and whether the mapped annotation is a composed annotation - * that needs to have its attributes merged or the mapped annotation is + * and whether the mapped annotation is * {@linkplain AnnotationTypeMapping#isSynthesizable() synthesizable} in general. * @param annotation the annotation to check * @since 5.3.22 @@ -365,11 +364,6 @@ final class TypeMappedAnnotation extends AbstractMergedAnn if (AnnotationUtils.isSynthesizedAnnotation(annotation)) { return false; } - // Is this a mapped annotation for a composed annotation, and are there - // annotation attributes (mirrors) that need to be merged? - if (getDistance() > 0 && this.resolvedMirrors.length > 0) { - return true; - } // Is the mapped annotation itself synthesizable? return this.mapping.isSynthesizable(); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 3c3b534a0f9..4aa1adff507 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -1565,6 +1565,22 @@ class MergedAnnotationsTests { assertNotSynthesized(enableGlobalAuthentication); } + /** + * A meta-annotation that declares an attribute should not be synthesized + * merely because it is meta-present rather than directly present, as long + * as none of its attributes are mirrored or overridden anywhere in the + * annotation hierarchy. + */ + @Test + void synthesizeShouldNotSynthesizeMetaAnnotationsWithNonOverriddenAttributes() { + MergedAnnotations mergedAnnotations = MergedAnnotations.from(ComponentWithPlainAttributeMetaAnnotation.class); + + PlainAttributeMetaAnnotation plainAttributeMetaAnnotation = + mergedAnnotations.get(PlainAttributeMetaAnnotation.class).synthesize(); + assertThat(plainAttributeMetaAnnotation.value()).isEqualTo("enigma"); + assertNotSynthesized(plainAttributeMetaAnnotation); + } + /** * If an attempt is made to synthesize an annotation from an annotation instance * that has already been synthesized, the original synthesized annotation should @@ -3278,6 +3294,24 @@ class MergedAnnotationsTests { static class SecurityConfig { } + /** + * Meta-annotation that declares an attribute which is never mirrored or + * overridden anywhere in the annotation hierarchy. + */ + @Retention(RUNTIME) + @interface PlainAttributeMetaAnnotation { + String value() default "enigma"; + } + + @PlainAttributeMetaAnnotation + @Retention(RUNTIME) + @interface ComposedPlainAttributeAnnotation { + } + + @ComposedPlainAttributeAnnotation + static class ComponentWithPlainAttributeMetaAnnotation { + } + @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @interface RootAnnotation {