Skip to content

Commit e09cdcd

Browse files
committed
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
1 parent d722b94 commit e09cdcd

File tree

8 files changed

+95
-291
lines changed

8 files changed

+95
-291
lines changed

Diff for: spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java

+1-134
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,11 +28,7 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.Set;
31-
import java.util.concurrent.ConcurrentHashMap;
32-
import java.util.function.Predicate;
3331

34-
import org.apache.commons.logging.Log;
35-
import org.apache.commons.logging.LogFactory;
3632
import org.jspecify.annotations.Nullable;
3733

3834
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
@@ -51,22 +47,6 @@
5147
*/
5248
final class AnnotationTypeMapping {
5349

54-
private static final Log logger = LogFactory.getLog(AnnotationTypeMapping.class);
55-
56-
private static final Predicate<? super Annotation> isBeanValidationConstraint = annotation ->
57-
annotation.annotationType().getName().equals("jakarta.validation.Constraint");
58-
59-
/**
60-
* Set used to track which convention-based annotation attribute overrides
61-
* have already been checked. Each key is the combination of the fully
62-
* qualified class name of a composed annotation and a meta-annotation
63-
* that it is either present or meta-present on the composed annotation,
64-
* separated by a dash.
65-
* @since 6.0
66-
* @see #addConventionMappings()
67-
*/
68-
private static final Set<String> conventionBasedOverrideCheckCache = ConcurrentHashMap.newKeySet();
69-
7050
private static final MirrorSet[] EMPTY_MIRROR_SETS = new MirrorSet[0];
7151

7252
private static final int[] EMPTY_INT_ARRAY = new int[0];
@@ -90,8 +70,6 @@ final class AnnotationTypeMapping {
9070

9171
private final int[] aliasMappings;
9272

93-
private final int[] conventionMappings;
94-
9573
private final int[] annotationValueMappings;
9674

9775
private final AnnotationTypeMapping[] annotationValueSource;
@@ -117,13 +95,10 @@ final class AnnotationTypeMapping {
11795
this.attributes = AttributeMethods.forAnnotationType(annotationType);
11896
this.mirrorSets = new MirrorSets();
11997
this.aliasMappings = filledIntArray(this.attributes.size());
120-
this.conventionMappings = filledIntArray(this.attributes.size());
12198
this.annotationValueMappings = filledIntArray(this.attributes.size());
12299
this.annotationValueSource = new AnnotationTypeMapping[this.attributes.size()];
123100
this.aliasedBy = resolveAliasedForTargets();
124101
processAliases();
125-
addConventionMappings();
126-
addConventionAnnotationValues();
127102
this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes);
128103
}
129104

@@ -284,95 +259,6 @@ private int getFirstRootAttributeIndex(Collection<Method> aliases) {
284259
return -1;
285260
}
286261

287-
private void addConventionMappings() {
288-
if (this.distance == 0) {
289-
return;
290-
}
291-
AttributeMethods rootAttributes = this.root.getAttributes();
292-
int[] mappings = this.conventionMappings;
293-
Set<String> conventionMappedAttributes = new HashSet<>();
294-
for (int i = 0; i < mappings.length; i++) {
295-
String name = this.attributes.get(i).getName();
296-
int mapped = rootAttributes.indexOf(name);
297-
if (!MergedAnnotation.VALUE.equals(name) && mapped != -1 && !isExplicitAttributeOverride(name)) {
298-
conventionMappedAttributes.add(name);
299-
mappings[i] = mapped;
300-
MirrorSet mirrors = getMirrorSets().getAssigned(i);
301-
if (mirrors != null) {
302-
for (int j = 0; j < mirrors.size(); j++) {
303-
mappings[mirrors.getAttributeIndex(j)] = mapped;
304-
}
305-
}
306-
}
307-
}
308-
String rootAnnotationTypeName = this.root.annotationType.getName();
309-
String cacheKey = rootAnnotationTypeName + '-' + this.annotationType.getName();
310-
// We want to avoid duplicate log warnings as much as possible, without full synchronization,
311-
// and we intentionally invoke add() before checking if any convention-based overrides were
312-
// actually encountered in order to ensure that we add a "tracked" entry for the current cache
313-
// key in any case.
314-
// In addition, we do NOT want to log warnings for custom Java Bean Validation constraint
315-
// annotations that are meta-annotated with other constraint annotations -- for example,
316-
// @org.hibernate.validator.constraints.URL which overrides attributes in
317-
// @jakarta.validation.constraints.Pattern.
318-
if (conventionBasedOverrideCheckCache.add(cacheKey) && !conventionMappedAttributes.isEmpty() &&
319-
Arrays.stream(this.annotationType.getAnnotations()).noneMatch(isBeanValidationConstraint) &&
320-
logger.isWarnEnabled()) {
321-
logger.warn("""
322-
Support for convention-based annotation attribute overrides is deprecated \
323-
and will be removed in Spring Framework 7.0. Please annotate the following \
324-
attributes in @%s with appropriate @AliasFor declarations: %s"""
325-
.formatted(rootAnnotationTypeName, conventionMappedAttributes));
326-
}
327-
}
328-
329-
/**
330-
* Determine if the given annotation attribute in the {@linkplain #getRoot()
331-
* root annotation} is an explicit annotation attribute override for an
332-
* attribute in a meta-annotation, explicit in the sense that the override
333-
* is declared via {@link AliasFor @AliasFor}.
334-
* <p>If the named attribute does not exist in the root annotation, this
335-
* method returns {@code false}.
336-
* @param name the name of the annotation attribute to check
337-
* @since 6.0
338-
*/
339-
private boolean isExplicitAttributeOverride(String name) {
340-
Method attribute = this.root.getAttributes().get(name);
341-
if (attribute != null) {
342-
AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class);
343-
return ((aliasFor != null) &&
344-
(aliasFor.annotation() != Annotation.class) &&
345-
(aliasFor.annotation() != this.root.annotationType));
346-
}
347-
return false;
348-
}
349-
350-
private void addConventionAnnotationValues() {
351-
for (int i = 0; i < this.attributes.size(); i++) {
352-
Method attribute = this.attributes.get(i);
353-
boolean isValueAttribute = MergedAnnotation.VALUE.equals(attribute.getName());
354-
AnnotationTypeMapping mapping = this;
355-
while (mapping != null && mapping.distance > 0) {
356-
int mapped = mapping.getAttributes().indexOf(attribute.getName());
357-
if (mapped != -1 && isBetterConventionAnnotationValue(i, isValueAttribute, mapping)) {
358-
this.annotationValueMappings[i] = mapped;
359-
this.annotationValueSource[i] = mapping;
360-
}
361-
mapping = mapping.source;
362-
}
363-
}
364-
}
365-
366-
private boolean isBetterConventionAnnotationValue(int index, boolean isValueAttribute,
367-
AnnotationTypeMapping mapping) {
368-
369-
if (this.annotationValueMappings[index] == -1) {
370-
return true;
371-
}
372-
int existingDistance = this.annotationValueSource[index].distance;
373-
return !isValueAttribute && existingDistance > mapping.distance;
374-
}
375-
376262
@SuppressWarnings("unchecked")
377263
private boolean computeSynthesizableFlag(Set<Class<? extends Annotation>> visitedAnnotationTypes) {
378264
// Track that we have visited the current annotation type.
@@ -390,13 +276,6 @@ private boolean computeSynthesizableFlag(Set<Class<? extends Annotation>> visite
390276
return true;
391277
}
392278

393-
// Uses convention-based attribute overrides in meta-annotations?
394-
for (int index : this.conventionMappings) {
395-
if (index != -1) {
396-
return true;
397-
}
398-
}
399-
400279
// Has nested annotations or arrays of annotations that are synthesizable?
401280
if (getAttributes().hasNestedAnnotation()) {
402281
AttributeMethods attributeMethods = getAttributes();
@@ -532,18 +411,6 @@ int getAliasMapping(int attributeIndex) {
532411
return this.aliasMappings[attributeIndex];
533412
}
534413

535-
/**
536-
* Get the related index of a convention mapped attribute, or {@code -1}
537-
* if there is no mapping. The resulting value is the index of the attribute
538-
* on the root annotation that can be invoked in order to obtain the actual
539-
* value.
540-
* @param attributeIndex the attribute index of the source attribute
541-
* @return the mapped attribute index or {@code -1}
542-
*/
543-
int getConventionMapping(int attributeIndex) {
544-
return this.conventionMappings[attributeIndex];
545-
}
546-
547414
/**
548415
* Get a mapped attribute value from the most suitable
549416
* {@link #getAnnotation() meta-annotation}.

Diff for: spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java

+4-8
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public MergedAnnotation<?> getRoot() {
199199
@Override
200200
public boolean hasDefaultValue(String attributeName) {
201201
int attributeIndex = getAttributeIndex(attributeName, true);
202-
Object value = getValue(attributeIndex, true, false);
202+
Object value = getValue(attributeIndex, false);
203203
return (value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, this.valueExtractor));
204204
}
205205

@@ -377,20 +377,17 @@ private Object getRequiredValue(int attributeIndex, String attributeName) {
377377

378378
private <T> @Nullable T getValue(int attributeIndex, Class<T> type) {
379379
Method attribute = this.mapping.getAttributes().get(attributeIndex);
380-
Object value = getValue(attributeIndex, true, false);
380+
Object value = getValue(attributeIndex, false);
381381
if (value == null) {
382382
value = attribute.getDefaultValue();
383383
}
384384
return adapt(attribute, value, type);
385385
}
386386

387-
private @Nullable Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) {
387+
private @Nullable Object getValue(int attributeIndex, boolean forMirrorResolution) {
388388
AnnotationTypeMapping mapping = this.mapping;
389389
if (this.useMergedValues) {
390390
int mappedIndex = this.mapping.getAliasMapping(attributeIndex);
391-
if (mappedIndex == -1 && useConventionMapping) {
392-
mappedIndex = this.mapping.getConventionMapping(attributeIndex);
393-
}
394391
if (mappedIndex != -1) {
395392
mapping = mapping.getRoot();
396393
attributeIndex = mappedIndex;
@@ -425,8 +422,7 @@ private Object getRequiredValue(int attributeIndex, String attributeName) {
425422

426423
private @Nullable Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) {
427424
int attributeIndex = this.mapping.getAttributes().indexOf(attribute);
428-
boolean valueAttribute = VALUE.equals(attribute.getName());
429-
return getValue(attributeIndex, !valueAttribute, true);
425+
return getValue(attributeIndex, true);
430426
}
431427

432428
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)