From e09cdcd92077e1c44772fe3d9fa1e1e638f59fe0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:15:14 +0100 Subject: [PATCH] Remove convention-based annotation attribute override support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit completely removes all support for convention-based annotation attribute overrides in Spring's annotation utilities and the MergedAnnotations infrastructure. Composed annotations must now use @⁠AliasFor to declare explicit overrides for attributes in meta-annotations. See gh-28760 Closes gh-28761 --- .../annotation/AnnotationTypeMapping.java | 135 +----------------- .../core/annotation/TypeMappedAnnotation.java | 12 +- .../AnnotatedElementUtilsTests.java | 87 ++++++----- .../AnnotationTypeMappingsTests.java | 38 +---- .../core/annotation/AnnotationUtilsTests.java | 5 +- .../annotation/MergedAnnotationsTests.java | 88 +++++------- .../annotation/TypeMappedAnnotationTests.java | 16 ++- .../core/type/AnnotationMetadataTests.java | 5 +- 8 files changed, 95 insertions(+), 291 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 f5e1a4ceaf..de9b411673 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,11 +28,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; @@ -51,22 +47,6 @@ import org.springframework.util.StringUtils; */ final class AnnotationTypeMapping { - private static final Log logger = LogFactory.getLog(AnnotationTypeMapping.class); - - private static final Predicate isBeanValidationConstraint = annotation -> - annotation.annotationType().getName().equals("jakarta.validation.Constraint"); - - /** - * Set used to track which convention-based annotation attribute overrides - * have already been checked. Each key is the combination of the fully - * qualified class name of a composed annotation and a meta-annotation - * that it is either present or meta-present on the composed annotation, - * separated by a dash. - * @since 6.0 - * @see #addConventionMappings() - */ - private static final Set conventionBasedOverrideCheckCache = ConcurrentHashMap.newKeySet(); - private static final MirrorSet[] EMPTY_MIRROR_SETS = new MirrorSet[0]; private static final int[] EMPTY_INT_ARRAY = new int[0]; @@ -90,8 +70,6 @@ final class AnnotationTypeMapping { private final int[] aliasMappings; - private final int[] conventionMappings; - private final int[] annotationValueMappings; private final AnnotationTypeMapping[] annotationValueSource; @@ -117,13 +95,10 @@ final class AnnotationTypeMapping { this.attributes = AttributeMethods.forAnnotationType(annotationType); this.mirrorSets = new MirrorSets(); this.aliasMappings = filledIntArray(this.attributes.size()); - this.conventionMappings = filledIntArray(this.attributes.size()); this.annotationValueMappings = filledIntArray(this.attributes.size()); this.annotationValueSource = new AnnotationTypeMapping[this.attributes.size()]; this.aliasedBy = resolveAliasedForTargets(); processAliases(); - addConventionMappings(); - addConventionAnnotationValues(); this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes); } @@ -284,95 +259,6 @@ final class AnnotationTypeMapping { return -1; } - private void addConventionMappings() { - if (this.distance == 0) { - return; - } - AttributeMethods rootAttributes = this.root.getAttributes(); - int[] mappings = this.conventionMappings; - Set conventionMappedAttributes = new HashSet<>(); - for (int i = 0; i < mappings.length; i++) { - String name = this.attributes.get(i).getName(); - int mapped = rootAttributes.indexOf(name); - if (!MergedAnnotation.VALUE.equals(name) && mapped != -1 && !isExplicitAttributeOverride(name)) { - conventionMappedAttributes.add(name); - mappings[i] = mapped; - MirrorSet mirrors = getMirrorSets().getAssigned(i); - if (mirrors != null) { - for (int j = 0; j < mirrors.size(); j++) { - mappings[mirrors.getAttributeIndex(j)] = mapped; - } - } - } - } - String rootAnnotationTypeName = this.root.annotationType.getName(); - String cacheKey = rootAnnotationTypeName + '-' + this.annotationType.getName(); - // We want to avoid duplicate log warnings as much as possible, without full synchronization, - // and we intentionally invoke add() before checking if any convention-based overrides were - // actually encountered in order to ensure that we add a "tracked" entry for the current cache - // key in any case. - // In addition, we do NOT want to log warnings for custom Java Bean Validation constraint - // annotations that are meta-annotated with other constraint annotations -- for example, - // @org.hibernate.validator.constraints.URL which overrides attributes in - // @jakarta.validation.constraints.Pattern. - if (conventionBasedOverrideCheckCache.add(cacheKey) && !conventionMappedAttributes.isEmpty() && - Arrays.stream(this.annotationType.getAnnotations()).noneMatch(isBeanValidationConstraint) && - logger.isWarnEnabled()) { - logger.warn(""" - Support for convention-based annotation attribute overrides is deprecated \ - and will be removed in Spring Framework 7.0. Please annotate the following \ - attributes in @%s with appropriate @AliasFor declarations: %s""" - .formatted(rootAnnotationTypeName, conventionMappedAttributes)); - } - } - - /** - * Determine if the given annotation attribute in the {@linkplain #getRoot() - * root annotation} is an explicit annotation attribute override for an - * attribute in a meta-annotation, explicit in the sense that the override - * is declared via {@link AliasFor @AliasFor}. - *

If the named attribute does not exist in the root annotation, this - * method returns {@code false}. - * @param name the name of the annotation attribute to check - * @since 6.0 - */ - private boolean isExplicitAttributeOverride(String name) { - Method attribute = this.root.getAttributes().get(name); - if (attribute != null) { - AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class); - return ((aliasFor != null) && - (aliasFor.annotation() != Annotation.class) && - (aliasFor.annotation() != this.root.annotationType)); - } - return false; - } - - private void addConventionAnnotationValues() { - for (int i = 0; i < this.attributes.size(); i++) { - Method attribute = this.attributes.get(i); - boolean isValueAttribute = MergedAnnotation.VALUE.equals(attribute.getName()); - AnnotationTypeMapping mapping = this; - while (mapping != null && mapping.distance > 0) { - int mapped = mapping.getAttributes().indexOf(attribute.getName()); - if (mapped != -1 && isBetterConventionAnnotationValue(i, isValueAttribute, mapping)) { - this.annotationValueMappings[i] = mapped; - this.annotationValueSource[i] = mapping; - } - mapping = mapping.source; - } - } - } - - private boolean isBetterConventionAnnotationValue(int index, boolean isValueAttribute, - AnnotationTypeMapping mapping) { - - if (this.annotationValueMappings[index] == -1) { - return true; - } - int existingDistance = this.annotationValueSource[index].distance; - return !isValueAttribute && existingDistance > mapping.distance; - } - @SuppressWarnings("unchecked") private boolean computeSynthesizableFlag(Set> visitedAnnotationTypes) { // Track that we have visited the current annotation type. @@ -390,13 +276,6 @@ final class AnnotationTypeMapping { return true; } - // Uses convention-based attribute overrides in meta-annotations? - for (int index : this.conventionMappings) { - if (index != -1) { - return true; - } - } - // Has nested annotations or arrays of annotations that are synthesizable? if (getAttributes().hasNestedAnnotation()) { AttributeMethods attributeMethods = getAttributes(); @@ -532,18 +411,6 @@ final class AnnotationTypeMapping { return this.aliasMappings[attributeIndex]; } - /** - * Get the related index of a convention mapped attribute, or {@code -1} - * if there is no mapping. The resulting value is the index of the attribute - * on the root annotation that can be invoked in order to obtain the actual - * value. - * @param attributeIndex the attribute index of the source attribute - * @return the mapped attribute index or {@code -1} - */ - int getConventionMapping(int attributeIndex) { - return this.conventionMappings[attributeIndex]; - } - /** * Get a mapped attribute value from the most suitable * {@link #getAnnotation() meta-annotation}. 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 bea8dd41ed..dbcf80face 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 @@ -199,7 +199,7 @@ final class TypeMappedAnnotation extends AbstractMergedAnn @Override public boolean hasDefaultValue(String attributeName) { int attributeIndex = getAttributeIndex(attributeName, true); - Object value = getValue(attributeIndex, true, false); + Object value = getValue(attributeIndex, false); return (value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, this.valueExtractor)); } @@ -377,20 +377,17 @@ final class TypeMappedAnnotation extends AbstractMergedAnn private @Nullable T getValue(int attributeIndex, Class type) { Method attribute = this.mapping.getAttributes().get(attributeIndex); - Object value = getValue(attributeIndex, true, false); + Object value = getValue(attributeIndex, false); if (value == null) { value = attribute.getDefaultValue(); } return adapt(attribute, value, type); } - private @Nullable Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) { + private @Nullable Object getValue(int attributeIndex, boolean forMirrorResolution) { AnnotationTypeMapping mapping = this.mapping; if (this.useMergedValues) { int mappedIndex = this.mapping.getAliasMapping(attributeIndex); - if (mappedIndex == -1 && useConventionMapping) { - mappedIndex = this.mapping.getConventionMapping(attributeIndex); - } if (mappedIndex != -1) { mapping = mapping.getRoot(); attributeIndex = mappedIndex; @@ -425,8 +422,7 @@ final class TypeMappedAnnotation extends AbstractMergedAnn private @Nullable Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) { int attributeIndex = this.mapping.getAttributes().indexOf(attribute); - boolean valueAttribute = VALUE.equals(attribute.getName()); - return getValue(attributeIndex, !valueAttribute, true); + return getValue(attributeIndex, true); } @SuppressWarnings("unchecked") diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 211679161a..598ea31299 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,6 @@ import javax.annotation.meta.When; import jakarta.annotation.Resource; import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -54,7 +53,6 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.core.annotation.AnnotatedElementUtils.findAllMergedAnnotations; import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; import static org.springframework.core.annotation.AnnotatedElementUtils.getAllAnnotationAttributes; @@ -94,31 +92,18 @@ class AnnotatedElementUtilsTests { AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); - assertThat(attributes.getStringArray("locations")).as("locations").containsExactly("explicitDeclaration"); - assertThat(attributes.getStringArray("value")).as("value").containsExactly("explicitDeclaration"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration". + assertThat(attributes.getStringArray("locations")).as("locations").isEmpty(); + assertThat(attributes.getStringArray("value")).as("value").isEmpty(); // Verify contracts between utility methods: assertThat(isAnnotated(element, name)).isTrue(); } - /** - * This test should never pass, simply because Spring does not support a hybrid - * approach for annotation attribute overrides with transitive implicit aliases. - * See SPR-13554 for details. - *

Furthermore, if you choose to execute this test, it can fail for either - * the first test class or the second one (with different exceptions), depending - * on the order in which the JVM returns the attribute methods via reflection. - */ - @Disabled("Permanently disabled but left in place for illustrative purposes") @Test - void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation() { - for (Class clazz : asList(HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class, - HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class)) { - getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(clazz); - } - } - - private void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(Class clazz) { + void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotationV1() { + Class clazz = HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class; String name = ContextConfig.class.getName(); String simpleName = clazz.getSimpleName(); AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name); @@ -134,18 +119,27 @@ class AnnotatedElementUtilsTests { } @Test - void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { - Class element = InvalidConventionBasedComposedContextConfigClass.class; - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - getMergedAnnotationAttributes(element, ContextConfig.class)) - .withMessageContaining("Different @AliasFor mirror values for annotation") - .withMessageContaining("attribute 'locations' and its alias 'value'") - .withMessageContaining("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"); + void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotationV2() { + Class clazz = HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class; + String name = ContextConfig.class.getName(); + String simpleName = clazz.getSimpleName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name); + + assertThat(attributes).as("Should find @ContextConfig on " + simpleName).isNotNull(); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration". + assertThat(attributes.getStringArray("locations")).as("locations for class [" + simpleName + "]").isEmpty(); + assertThat(attributes.getStringArray("value")).as("value for class [" + simpleName + "]").isEmpty(); + + // Verify contracts between utility methods: + assertThat(isAnnotated(clazz, name)).isTrue(); } @Test void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() { - assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "com.example.app.test". + assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class); } @Test @@ -157,12 +151,16 @@ class AnnotatedElementUtilsTests { assertThat(contextConfig.locations()).as("locations for " + element).isEmpty(); // 'value' in @SpringAppConfig should not override 'value' in @ContextConfig assertThat(contextConfig.value()).as("value for " + element).isEmpty(); - assertThat(contextConfig.classes()).as("classes for " + element).containsExactly(Number.class); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect Number.class. + assertThat(contextConfig.classes()).as("classes for " + element).isEmpty(); } @Test void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throws Exception { - assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "/test". + assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute"), ""); } } @@ -831,15 +829,15 @@ class AnnotatedElementUtilsTests { @Test void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); - assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); + assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"), "/test"); + assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"), "/test"); } - private void assertWebMapping(AnnotatedElement element) { + private void assertWebMapping(AnnotatedElement element, String expectedPath) { WebMapping webMapping = findMergedAnnotation(element, WebMapping.class); assertThat(webMapping).isNotNull(); - assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray("/test")); - assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray("/test")); + assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray(expectedPath)); + assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray(expectedPath)); } @Test @@ -1090,8 +1088,7 @@ class AnnotatedElementUtilsTests { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedComposedContextConfig { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class) + // Do NOT use @AliasFor here String[] locations() default {}; } @@ -1099,8 +1096,7 @@ class AnnotatedElementUtilsTests { @Retention(RetentionPolicy.RUNTIME) @interface InvalidConventionBasedComposedContextConfig { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class) + // Do NOT use @AliasFor here String[] locations(); } @@ -1258,13 +1254,11 @@ class AnnotatedElementUtilsTests { @AliasFor(annotation = ContextConfig.class, attribute = "locations") String[] locations() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("value") Class[] classes() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("classes") Class[] value() default {}; } @@ -1303,8 +1297,7 @@ class AnnotatedElementUtilsTests { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedSinglePackageComponentScan { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ComponentScan.class) + // Do NOT use @AliasFor here String basePackages(); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java index 1e99bc44f9..9f4fa08142 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -279,12 +279,6 @@ class AnnotationTypeMappingsTests { assertThat(getAliasMapping(mapping, 0)).isEqualTo(Mapped.class.getDeclaredMethod("alias")); } - @Test - void getConventionMappingReturnsAttributes() throws Exception { - AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(Mapped.class).get(1); - assertThat(getConventionMapping(mapping, 1)).isEqualTo(Mapped.class.getDeclaredMethod("convention")); - } - @Test void getMirrorSetWhenAliasPairReturnsMirrors() { AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); @@ -410,14 +404,6 @@ class AnnotationTypeMappingsTests { assertThat(getAliasMapping(mappingsC, 1).getName()).isEqualTo("a1"); } - @Test - void getConventionMappingWhenConventionToExplicitAliasesReturnsMappedAttributes() { - AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(ConventionToExplicitAliases.class); - AnnotationTypeMapping mapping = getMapping(mappings, ConventionToExplicitAliasesTarget.class); - assertThat(mapping.getConventionMapping(0)).isEqualTo(0); - assertThat(mapping.getConventionMapping(1)).isEqualTo(0); - } - @Test void isEquivalentToDefaultValueWhenValueAndDefaultAreNullReturnsTrue() { AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValue.class).get(0); @@ -481,11 +467,6 @@ class AnnotationTypeMappingsTests { return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; } - private @Nullable Method getConventionMapping(AnnotationTypeMapping mapping, int attributeIndex) { - int mapped = mapping.getConventionMapping(attributeIndex); - return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; - } - private AnnotationTypeMapping getMapping(AnnotationTypeMappings mappings, Class annotationType) { @@ -890,23 +871,6 @@ class AnnotationTypeMappingsTests { String a1() default ""; } - @Retention(RetentionPolicy.RUNTIME) - @interface ConventionToExplicitAliasesTarget { - - @AliasFor("test") - String value() default ""; - - @AliasFor("value") - String test() default ""; - } - - @Retention(RetentionPolicy.RUNTIME) - @ConventionToExplicitAliasesTarget - @interface ConventionToExplicitAliases { - - String test() default ""; - } - @Retention(RetentionPolicy.RUNTIME) @interface ClassValue { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index b127137cfc..0cf5b16225 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1362,8 +1362,7 @@ class AnnotationUtilsTests { @WebMapping(method = RequestMethod.POST, name = "") @interface Post { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = WebMapping.class) + // Do NOT use @AliasFor here String path() default ""; } 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 d0d2477e1f..b3576fb88d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,8 +63,8 @@ import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link MergedAnnotations} and {@link MergedAnnotation}. These tests - * cover common usage scenarios and were mainly ported from the original - * {@code AnnotationUtils} and {@code AnnotatedElementUtils} tests. + * cover common usage scenarios and were mainly ported from the original tests in + * {@link AnnotationUtilsTests} and {@link AnnotatedElementUtilsTests}. * * @author Phillip Webb * @author Rod Johnson @@ -218,8 +218,10 @@ class MergedAnnotationsTests { MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.isPresent()).isTrue(); - assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); - assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration". + assertThat(annotation.getStringArray("locations")).isEmpty(); + assertThat(annotation.getStringArray("value")).isEmpty(); } @Test @@ -244,16 +246,11 @@ class MergedAnnotationsTests { assertThat(annotation.getStringArray("value")).isEmpty(); } - @Test - void getWithInheritedAnnotationsFromInvalidConventionBasedComposedAnnotation() { - assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> MergedAnnotations.from(InvalidConventionBasedComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class)); - } - @Test void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { - testGetWithTypeHierarchy(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "com.example.app.test". + testGetWithTypeHierarchy(ConventionBasedSinglePackageComponentScanClass.class); } @Test @@ -263,12 +260,16 @@ class MergedAnnotationsTests { .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); - assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect Number.class. + assertThat(annotation.getClassArray("classes")).isEmpty(); } @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { - testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "/test". + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute"), ""); } } @@ -781,15 +782,15 @@ class MergedAnnotationsTests { @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); - testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"), "/test"); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"), "/test"); } - private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element) { + private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element, String expectedPath) { MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) .get(RequestMapping.class); - assertThat(annotation.getStringArray("value")).containsExactly("/test"); - assertThat(annotation.getStringArray("path")).containsExactly("/test"); + assertThat(annotation.getStringArray("value")).containsExactly(expectedPath); + assertThat(annotation.getStringArray("path")).containsExactly(expectedPath); } @Test @@ -2160,9 +2161,11 @@ class MergedAnnotationsTests { @Test void getValueWhenHasDefaultOverride() { - MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideClass.class) - .get(DefaultOverrideRoot.class); - assertThat(annotation.getString("text")).isEqualTo("metameta"); + MergedAnnotation annotation = + MergedAnnotations.from(DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "metameta". + assertThat(annotation.getString("text")).isEqualTo("root"); } @Test // gh-22654 @@ -2370,24 +2373,13 @@ class MergedAnnotationsTests { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedComposedContextConfiguration { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) + // Do NOT use @AliasFor here String[] locations() default {}; - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) + // Do NOT use @AliasFor here Class[] classes() default {}; } - @ContextConfiguration(value = "duplicateDeclaration") - @Retention(RetentionPolicy.RUNTIME) - @interface InvalidConventionBasedComposedContextConfiguration { - - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) - String[] locations(); - } - /** * This hybrid approach for annotation attribute overrides with transitive implicit * aliases is unsupported. See SPR-13554 for details. @@ -2396,8 +2388,7 @@ class MergedAnnotationsTests { @Retention(RetentionPolicy.RUNTIME) @interface HalfConventionBasedAndHalfAliasedComposedContextConfiguration { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) + // Do NOT use @AliasFor here String[] locations() default {}; @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") @@ -2519,13 +2510,11 @@ class MergedAnnotationsTests { @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] locations() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("value") Class[] classes() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("classes") Class[] value() default {}; } @@ -2562,8 +2551,7 @@ class MergedAnnotationsTests { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedSinglePackageComponentScan { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ComponentScan.class) + // Do NOT use @AliasFor here String basePackages(); } @@ -2698,10 +2686,6 @@ class MergedAnnotationsTests { static class ConventionBasedComposedContextConfigurationClass { } - @InvalidConventionBasedComposedContextConfiguration(locations = "requiredLocationsDeclaration") - static class InvalidConventionBasedComposedContextConfigurationClass { - } - @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(xmlConfigFiles = "explicitDeclaration") static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1 { } @@ -3153,8 +3137,7 @@ class MergedAnnotationsTests { @RequestMapping(method = RequestMethod.POST, name = "") @interface PostMapping { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = RequestMapping.class) + // Do NOT use @AliasFor here String path() default ""; } @@ -3645,13 +3628,11 @@ class MergedAnnotationsTests { @interface DefaultOverrideRoot { String text() default "root"; - } @Retention(RetentionPolicy.RUNTIME) @DefaultOverrideRoot @interface DefaultOverrideMeta { - } @Retention(RetentionPolicy.RUNTIME) @@ -3659,18 +3640,15 @@ class MergedAnnotationsTests { @interface DefaultOverrideMetaMeta { String text() default "metameta"; - } @Retention(RetentionPolicy.RUNTIME) @DefaultOverrideMetaMeta @interface DefaultOverrideMetaMetaMeta { - } @DefaultOverrideMetaMetaMeta static class DefaultOverrideClass { - } @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java index d04d16a015..7e99dcceb2 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; * for a much more extensive collection of tests. * * @author Phillip Webb + * @author Sam Brannen */ class TypeMappedAnnotationTests { @@ -62,11 +63,19 @@ class TypeMappedAnnotationTests { @Test void mappingConventionAliasToMetaAnnotationReturnsMappedValues() { TypeMappedAnnotation annotation = getTypeMappedAnnotation( + WithConventionAliasToMetaAnnotation.class, + ConventionAliasToMetaAnnotation.class); + assertThat(annotation.getString("value")).isEqualTo("value"); + assertThat(annotation.getString("convention")).isEqualTo("convention"); + + annotation = getTypeMappedAnnotation( WithConventionAliasToMetaAnnotation.class, ConventionAliasToMetaAnnotation.class, ConventionAliasMetaAnnotationTarget.class); assertThat(annotation.getString("value")).isEmpty(); - assertThat(annotation.getString("convention")).isEqualTo("convention"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "convention". + assertThat(annotation.getString("convention")).isEmpty(); } @Test @@ -195,8 +204,7 @@ class TypeMappedAnnotationTests { String value() default ""; - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ConventionAliasMetaAnnotationTarget.class) + // Do NOT use @AliasFor here String convention() default ""; } diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 6e829cddab..19266e7c2b 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -665,8 +665,7 @@ class AnnotationMetadataTests { @Target(ElementType.TYPE) public @interface ComposedConfigurationWithAttributeOverrides { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = TestComponentScan.class) + @AliasFor(annotation = TestComponentScan.class) String[] basePackages() default {}; }