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:
@@ -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<? 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 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<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")
|
||||
private boolean computeSynthesizableFlag(Set<Class<? extends Annotation>> 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}.
|
||||
|
||||
@@ -199,7 +199,7 @@ final class TypeMappedAnnotation<A extends Annotation> 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<A extends Annotation> extends AbstractMergedAnn
|
||||
|
||||
private <T> @Nullable T getValue(int attributeIndex, Class<T> 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<A extends Annotation> 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")
|
||||
|
||||
Reference in New Issue
Block a user