Remove convention-based annotation attribute override support

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
This commit is contained in:
Sam Brannen 2025-03-06 16:15:14 +01:00
parent d722b9434e
commit e09cdcd920
8 changed files with 95 additions and 291 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
@ -51,22 +47,6 @@ import org.springframework.util.StringUtils;
*/ */
final class AnnotationTypeMapping { final class AnnotationTypeMapping {
private static final Log logger = LogFactory.getLog(AnnotationTypeMapping.class);
private static final Predicate<? super Annotation> 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<String> conventionBasedOverrideCheckCache = ConcurrentHashMap.newKeySet();
private static final MirrorSet[] EMPTY_MIRROR_SETS = new MirrorSet[0]; private static final MirrorSet[] EMPTY_MIRROR_SETS = new MirrorSet[0];
private static final int[] EMPTY_INT_ARRAY = new int[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[] aliasMappings;
private final int[] conventionMappings;
private final int[] annotationValueMappings; private final int[] annotationValueMappings;
private final AnnotationTypeMapping[] annotationValueSource; private final AnnotationTypeMapping[] annotationValueSource;
@ -117,13 +95,10 @@ final class AnnotationTypeMapping {
this.attributes = AttributeMethods.forAnnotationType(annotationType); this.attributes = AttributeMethods.forAnnotationType(annotationType);
this.mirrorSets = new MirrorSets(); this.mirrorSets = new MirrorSets();
this.aliasMappings = filledIntArray(this.attributes.size()); this.aliasMappings = filledIntArray(this.attributes.size());
this.conventionMappings = filledIntArray(this.attributes.size());
this.annotationValueMappings = filledIntArray(this.attributes.size()); this.annotationValueMappings = filledIntArray(this.attributes.size());
this.annotationValueSource = new AnnotationTypeMapping[this.attributes.size()]; this.annotationValueSource = new AnnotationTypeMapping[this.attributes.size()];
this.aliasedBy = resolveAliasedForTargets(); this.aliasedBy = resolveAliasedForTargets();
processAliases(); processAliases();
addConventionMappings();
addConventionAnnotationValues();
this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes); this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes);
} }
@ -284,95 +259,6 @@ final class AnnotationTypeMapping {
return -1; return -1;
} }
private void addConventionMappings() {
if (this.distance == 0) {
return;
}
AttributeMethods rootAttributes = this.root.getAttributes();
int[] mappings = this.conventionMappings;
Set<String> 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}.
* <p>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") @SuppressWarnings("unchecked")
private boolean computeSynthesizableFlag(Set<Class<? extends Annotation>> visitedAnnotationTypes) { private boolean computeSynthesizableFlag(Set<Class<? extends Annotation>> visitedAnnotationTypes) {
// Track that we have visited the current annotation type. // Track that we have visited the current annotation type.
@ -390,13 +276,6 @@ final class AnnotationTypeMapping {
return true; 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? // Has nested annotations or arrays of annotations that are synthesizable?
if (getAttributes().hasNestedAnnotation()) { if (getAttributes().hasNestedAnnotation()) {
AttributeMethods attributeMethods = getAttributes(); AttributeMethods attributeMethods = getAttributes();
@ -532,18 +411,6 @@ final class AnnotationTypeMapping {
return this.aliasMappings[attributeIndex]; 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 * Get a mapped attribute value from the most suitable
* {@link #getAnnotation() meta-annotation}. * {@link #getAnnotation() meta-annotation}.

View File

@ -199,7 +199,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
@Override @Override
public boolean hasDefaultValue(String attributeName) { public boolean hasDefaultValue(String attributeName) {
int attributeIndex = getAttributeIndex(attributeName, true); 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)); return (value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, this.valueExtractor));
} }
@ -377,20 +377,17 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
private <T> @Nullable T getValue(int attributeIndex, Class<T> type) { private <T> @Nullable T getValue(int attributeIndex, Class<T> type) {
Method attribute = this.mapping.getAttributes().get(attributeIndex); Method attribute = this.mapping.getAttributes().get(attributeIndex);
Object value = getValue(attributeIndex, true, false); Object value = getValue(attributeIndex, false);
if (value == null) { if (value == null) {
value = attribute.getDefaultValue(); value = attribute.getDefaultValue();
} }
return adapt(attribute, value, type); 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; AnnotationTypeMapping mapping = this.mapping;
if (this.useMergedValues) { if (this.useMergedValues) {
int mappedIndex = this.mapping.getAliasMapping(attributeIndex); int mappedIndex = this.mapping.getAliasMapping(attributeIndex);
if (mappedIndex == -1 && useConventionMapping) {
mappedIndex = this.mapping.getConventionMapping(attributeIndex);
}
if (mappedIndex != -1) { if (mappedIndex != -1) {
mapping = mapping.getRoot(); mapping = mapping.getRoot();
attributeIndex = mappedIndex; attributeIndex = mappedIndex;
@ -425,8 +422,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
private @Nullable Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) { private @Nullable Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) {
int attributeIndex = this.mapping.getAttributes().indexOf(attribute); int attributeIndex = this.mapping.getAttributes().indexOf(attribute);
boolean valueAttribute = VALUE.equals(attribute.getName()); return getValue(attributeIndex, true);
return getValue(attributeIndex, !valueAttribute, true);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 jakarta.annotation.Resource;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; 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.toList;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
import static org.assertj.core.api.Assertions.assertThat; 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.findAllMergedAnnotations;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
import static org.springframework.core.annotation.AnnotatedElementUtils.getAllAnnotationAttributes; import static org.springframework.core.annotation.AnnotatedElementUtils.getAllAnnotationAttributes;
@ -94,31 +92,18 @@ class AnnotatedElementUtilsTests {
AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name);
assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull();
assertThat(attributes.getStringArray("locations")).as("locations").containsExactly("explicitDeclaration"); // Convention-based annotation attribute overrides are no longer supported as of
assertThat(attributes.getStringArray("value")).as("value").containsExactly("explicitDeclaration"); // 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: // Verify contracts between utility methods:
assertThat(isAnnotated(element, name)).isTrue(); 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.
* <p>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 @Test
void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation() { void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotationV1() {
for (Class<?> clazz : asList(HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class, Class<?> clazz = HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class;
HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class)) {
getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(clazz);
}
}
private void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(Class<?> clazz) {
String name = ContextConfig.class.getName(); String name = ContextConfig.class.getName();
String simpleName = clazz.getSimpleName(); String simpleName = clazz.getSimpleName();
AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name); AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name);
@ -134,18 +119,27 @@ class AnnotatedElementUtilsTests {
} }
@Test @Test
void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotationV2() {
Class<?> element = InvalidConventionBasedComposedContextConfigClass.class; Class<?> clazz = HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class;
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> String name = ContextConfig.class.getName();
getMergedAnnotationAttributes(element, ContextConfig.class)) String simpleName = clazz.getSimpleName();
.withMessageContaining("Different @AliasFor mirror values for annotation") AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name);
.withMessageContaining("attribute 'locations' and its alias 'value'")
.withMessageContaining("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"); 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 @Test
void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() { 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 @Test
@ -157,12 +151,16 @@ class AnnotatedElementUtilsTests {
assertThat(contextConfig.locations()).as("locations for " + element).isEmpty(); assertThat(contextConfig.locations()).as("locations for " + element).isEmpty();
// 'value' in @SpringAppConfig should not override 'value' in @ContextConfig // 'value' in @SpringAppConfig should not override 'value' in @ContextConfig
assertThat(contextConfig.value()).as("value for " + element).isEmpty(); 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 @Test
void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throws Exception { 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 @Test
void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception {
assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"), "/test");
assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"), "/test");
} }
private void assertWebMapping(AnnotatedElement element) { private void assertWebMapping(AnnotatedElement element, String expectedPath) {
WebMapping webMapping = findMergedAnnotation(element, WebMapping.class); WebMapping webMapping = findMergedAnnotation(element, WebMapping.class);
assertThat(webMapping).isNotNull(); assertThat(webMapping).isNotNull();
assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray("/test")); assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray(expectedPath));
assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray("/test")); assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray(expectedPath));
} }
@Test @Test
@ -1090,8 +1088,7 @@ class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface ConventionBasedComposedContextConfig { @interface ConventionBasedComposedContextConfig {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ContextConfig.class)
String[] locations() default {}; String[] locations() default {};
} }
@ -1099,8 +1096,7 @@ class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface InvalidConventionBasedComposedContextConfig { @interface InvalidConventionBasedComposedContextConfig {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ContextConfig.class)
String[] locations(); String[] locations();
} }
@ -1258,13 +1254,11 @@ class AnnotatedElementUtilsTests {
@AliasFor(annotation = ContextConfig.class, attribute = "locations") @AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] locations() default {}; String[] locations() default {};
// Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 // Do NOT use @AliasFor(annotation = ...)
// @AliasFor(annotation = ContextConfig.class, attribute = "classes")
@AliasFor("value") @AliasFor("value")
Class<?>[] classes() default {}; Class<?>[] classes() default {};
// Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 // Do NOT use @AliasFor(annotation = ...)
// @AliasFor(annotation = ContextConfig.class, attribute = "classes")
@AliasFor("classes") @AliasFor("classes")
Class<?>[] value() default {}; Class<?>[] value() default {};
} }
@ -1303,8 +1297,7 @@ class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface ConventionBasedSinglePackageComponentScan { @interface ConventionBasedSinglePackageComponentScan {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ComponentScan.class)
String basePackages(); String basePackages();
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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")); 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 @Test
void getMirrorSetWhenAliasPairReturnsMirrors() { void getMirrorSetWhenAliasPairReturnsMirrors() {
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0);
@ -410,14 +404,6 @@ class AnnotationTypeMappingsTests {
assertThat(getAliasMapping(mappingsC, 1).getName()).isEqualTo("a1"); 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 @Test
void isEquivalentToDefaultValueWhenValueAndDefaultAreNullReturnsTrue() { void isEquivalentToDefaultValueWhenValueAndDefaultAreNullReturnsTrue() {
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValue.class).get(0); AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValue.class).get(0);
@ -481,11 +467,6 @@ class AnnotationTypeMappingsTests {
return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; 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, private AnnotationTypeMapping getMapping(AnnotationTypeMappings mappings,
Class<? extends Annotation> annotationType) { Class<? extends Annotation> annotationType) {
@ -890,23 +871,6 @@ class AnnotationTypeMappingsTests {
String a1() default ""; 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) @Retention(RetentionPolicy.RUNTIME)
@interface ClassValue { @interface ClassValue {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 = "") @WebMapping(method = RequestMethod.POST, name = "")
@interface Post { @interface Post {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = WebMapping.class)
String path() default ""; String path() default "";
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * Tests for {@link MergedAnnotations} and {@link MergedAnnotation}. These tests
* cover common usage scenarios and were mainly ported from the original * cover common usage scenarios and were mainly ported from the original tests in
* {@code AnnotationUtils} and {@code AnnotatedElementUtils} tests. * {@link AnnotationUtilsTests} and {@link AnnotatedElementUtilsTests}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Rod Johnson * @author Rod Johnson
@ -218,8 +218,10 @@ class MergedAnnotationsTests {
MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class, MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class,
SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class);
assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.isPresent()).isTrue();
assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); // Convention-based annotation attribute overrides are no longer supported as of
assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration".
assertThat(annotation.getStringArray("locations")).isEmpty();
assertThat(annotation.getStringArray("value")).isEmpty();
} }
@Test @Test
@ -244,16 +246,11 @@ class MergedAnnotationsTests {
assertThat(annotation.getStringArray("value")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty();
} }
@Test
void getWithInheritedAnnotationsFromInvalidConventionBasedComposedAnnotation() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotations.from(InvalidConventionBasedComposedContextConfigurationClass.class,
SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class));
}
@Test @Test
void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { 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 @Test
@ -263,12 +260,16 @@ class MergedAnnotationsTests {
.get(ContextConfiguration.class); .get(ContextConfiguration.class);
assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("locations")).isEmpty();
assertThat(annotation.getStringArray("value")).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 @Test
void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { 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 @Test
void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception {
testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"), "/test");
testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); 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) MergedAnnotation<?> annotation = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY)
.get(RequestMapping.class); .get(RequestMapping.class);
assertThat(annotation.getStringArray("value")).containsExactly("/test"); assertThat(annotation.getStringArray("value")).containsExactly(expectedPath);
assertThat(annotation.getStringArray("path")).containsExactly("/test"); assertThat(annotation.getStringArray("path")).containsExactly(expectedPath);
} }
@Test @Test
@ -2160,9 +2161,11 @@ class MergedAnnotationsTests {
@Test @Test
void getValueWhenHasDefaultOverride() { void getValueWhenHasDefaultOverride() {
MergedAnnotation<?> annotation = MergedAnnotations.from(DefaultOverrideClass.class) MergedAnnotation<?> annotation =
.get(DefaultOverrideRoot.class); MergedAnnotations.from(DefaultOverrideClass.class).get(DefaultOverrideRoot.class);
assertThat(annotation.getString("text")).isEqualTo("metameta"); // 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 @Test // gh-22654
@ -2370,24 +2373,13 @@ class MergedAnnotationsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface ConventionBasedComposedContextConfiguration { @interface ConventionBasedComposedContextConfiguration {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ContextConfiguration.class)
String[] locations() default {}; String[] locations() default {};
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ContextConfiguration.class)
Class<?>[] classes() default {}; 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 * This hybrid approach for annotation attribute overrides with transitive implicit
* aliases is unsupported. See SPR-13554 for details. * aliases is unsupported. See SPR-13554 for details.
@ -2396,8 +2388,7 @@ class MergedAnnotationsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface HalfConventionBasedAndHalfAliasedComposedContextConfiguration { @interface HalfConventionBasedAndHalfAliasedComposedContextConfiguration {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ContextConfiguration.class)
String[] locations() default {}; String[] locations() default {};
@AliasFor(annotation = ContextConfiguration.class, attribute = "locations") @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
@ -2519,13 +2510,11 @@ class MergedAnnotationsTests {
@AliasFor(annotation = ContextConfiguration.class, attribute = "locations") @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
String[] locations() default {}; String[] locations() default {};
// Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 // Do NOT use @AliasFor(annotation = ...)
// @AliasFor(annotation = ContextConfiguration.class, attribute = "classes")
@AliasFor("value") @AliasFor("value")
Class<?>[] classes() default {}; Class<?>[] classes() default {};
// Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 // Do NOT use @AliasFor(annotation = ...)
// @AliasFor(annotation = ContextConfiguration.class, attribute = "classes")
@AliasFor("classes") @AliasFor("classes")
Class<?>[] value() default {}; Class<?>[] value() default {};
} }
@ -2562,8 +2551,7 @@ class MergedAnnotationsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface ConventionBasedSinglePackageComponentScan { @interface ConventionBasedSinglePackageComponentScan {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ComponentScan.class)
String basePackages(); String basePackages();
} }
@ -2698,10 +2686,6 @@ class MergedAnnotationsTests {
static class ConventionBasedComposedContextConfigurationClass { static class ConventionBasedComposedContextConfigurationClass {
} }
@InvalidConventionBasedComposedContextConfiguration(locations = "requiredLocationsDeclaration")
static class InvalidConventionBasedComposedContextConfigurationClass {
}
@HalfConventionBasedAndHalfAliasedComposedContextConfiguration(xmlConfigFiles = "explicitDeclaration") @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(xmlConfigFiles = "explicitDeclaration")
static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1 { static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1 {
} }
@ -3153,8 +3137,7 @@ class MergedAnnotationsTests {
@RequestMapping(method = RequestMethod.POST, name = "") @RequestMapping(method = RequestMethod.POST, name = "")
@interface PostMapping { @interface PostMapping {
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = RequestMapping.class)
String path() default ""; String path() default "";
} }
@ -3645,13 +3628,11 @@ class MergedAnnotationsTests {
@interface DefaultOverrideRoot { @interface DefaultOverrideRoot {
String text() default "root"; String text() default "root";
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@DefaultOverrideRoot @DefaultOverrideRoot
@interface DefaultOverrideMeta { @interface DefaultOverrideMeta {
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -3659,18 +3640,15 @@ class MergedAnnotationsTests {
@interface DefaultOverrideMetaMeta { @interface DefaultOverrideMetaMeta {
String text() default "metameta"; String text() default "metameta";
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@DefaultOverrideMetaMeta @DefaultOverrideMetaMeta
@interface DefaultOverrideMetaMetaMeta { @interface DefaultOverrideMetaMetaMeta {
} }
@DefaultOverrideMetaMetaMeta @DefaultOverrideMetaMetaMeta
static class DefaultOverrideClass { static class DefaultOverrideClass {
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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. * for a much more extensive collection of tests.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
*/ */
class TypeMappedAnnotationTests { class TypeMappedAnnotationTests {
@ -62,11 +63,19 @@ class TypeMappedAnnotationTests {
@Test @Test
void mappingConventionAliasToMetaAnnotationReturnsMappedValues() { void mappingConventionAliasToMetaAnnotationReturnsMappedValues() {
TypeMappedAnnotation<?> annotation = getTypeMappedAnnotation( TypeMappedAnnotation<?> annotation = getTypeMappedAnnotation(
WithConventionAliasToMetaAnnotation.class,
ConventionAliasToMetaAnnotation.class);
assertThat(annotation.getString("value")).isEqualTo("value");
assertThat(annotation.getString("convention")).isEqualTo("convention");
annotation = getTypeMappedAnnotation(
WithConventionAliasToMetaAnnotation.class, WithConventionAliasToMetaAnnotation.class,
ConventionAliasToMetaAnnotation.class, ConventionAliasToMetaAnnotation.class,
ConventionAliasMetaAnnotationTarget.class); ConventionAliasMetaAnnotationTarget.class);
assertThat(annotation.getString("value")).isEmpty(); 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 @Test
@ -195,8 +204,7 @@ class TypeMappedAnnotationTests {
String value() default ""; String value() default "";
// Do NOT use @AliasFor here until Spring 6.1 // Do NOT use @AliasFor here
// @AliasFor(annotation = ConventionAliasMetaAnnotationTarget.class)
String convention() default ""; String convention() default "";
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -665,8 +665,7 @@ class AnnotationMetadataTests {
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface ComposedConfigurationWithAttributeOverrides { public @interface ComposedConfigurationWithAttributeOverrides {
// Do NOT use @AliasFor here until Spring 6.1 @AliasFor(annotation = TestComponentScan.class)
// @AliasFor(annotation = TestComponentScan.class)
String[] basePackages() default {}; String[] basePackages() default {};
} }