Skip to content

Commit d40a35b

Browse files
committed
Support implicit attribute aliases with @AliasFor
Spring Framework 4.2 introduced support for aliases between annotation attributes that fall into the following two categories. 1) Alias pairs: two attributes in the same annotation that use @AliasFor to declare that they are explicit aliases for each other. 2) Meta-annotation attribute overrides: an attribute in one annotation uses @AliasFor to declare that it is an explicit override of an attribute in a meta-annotation. However, the existing functionality fails to support the case where two attributes in the same annotation both use @AliasFor to declare that they are both explicit overrides of the same attribute in the same meta-annotation. In such scenarios, one would intuitively assume that two such attributes would be treated as "implicit" aliases for each other, analogous to the existing support for explicit alias pairs. Furthermore, an annotation may potentially declare multiple aliases that are effectively a set of implicit aliases for each other. This commit introduces support for implicit aliases configured via @AliasFor through an extensive overhaul of the support for alias lookups, validation, etc. Specifically, this commit includes the following. - Introduced isAnnotationMetaPresent() in AnnotationUtils. - Introduced private AliasDescriptor class in AnnotationUtils in order to encapsulate the parsing, validation, and comparison of both explicit and implicit aliases configured via @AliasFor. - Switched from single values for alias names to lists of alias names. - Renamed getAliasedAttributeName() to getAliasedAttributeNames() in AnnotationUtils. - Converted alias map to contain lists of aliases in AnnotationUtils. - Refactored the following to support multiple implicit aliases: getRequiredAttributeWithAlias() in AnnotationAttributes, AbstractAliasAwareAnnotationAttributeExtractor, MapAnnotationAttributeExtractor, MergedAnnotationAttributesProcessor in AnnotatedElementUtils, and postProcessAnnotationAttributes() in AnnotationUtils. - Introduced numerous tests for implicit alias support, including AbstractAliasAwareAnnotationAttributeExtractorTestCase, DefaultAnnotationAttributeExtractorTests, and MapAnnotationAttributeExtractorTests. - Updated Javadoc in @AliasFor regarding implicit aliases and in AnnotationUtils regarding "meta-present". Issue: SPR-13345
1 parent ff9fb9a commit d40a35b

12 files changed

+1369
-379
lines changed

spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
22+
import java.util.List;
2223
import java.util.Map;
2324

2425
import org.springframework.util.Assert;
@@ -44,7 +45,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
4445

4546
private final S source;
4647

47-
private final Map<String, String> attributeAliasMap;
48+
private final Map<String, List<String>> attributeAliasMap;
4849

4950

5051
/**
@@ -83,29 +84,33 @@ public final S getSource() {
8384

8485
@Override
8586
public final Object getAttributeValue(Method attributeMethod) {
86-
String attributeName = attributeMethod.getName();
87+
final String attributeName = attributeMethod.getName();
8788
Object attributeValue = getRawAttributeValue(attributeMethod);
8889

89-
String aliasName = this.attributeAliasMap.get(attributeName);
90-
if (aliasName != null) {
91-
Object aliasValue = getRawAttributeValue(aliasName);
92-
Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
93-
94-
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
95-
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
96-
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
97-
String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element");
98-
throw new AnnotationConfigurationException(String.format(
99-
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
100-
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
101-
getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName,
102-
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
103-
}
104-
105-
// If the user didn't declare the annotation with an explicit value,
106-
// return the value of the alias.
107-
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
108-
attributeValue = aliasValue;
90+
List<String> aliasNames = this.attributeAliasMap.get(attributeName);
91+
if (aliasNames != null) {
92+
final Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
93+
for (String aliasName : aliasNames) {
94+
if (aliasName != null) {
95+
Object aliasValue = getRawAttributeValue(aliasName);
96+
97+
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
98+
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
99+
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
100+
String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element");
101+
throw new AnnotationConfigurationException(String.format(
102+
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
103+
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
104+
getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName,
105+
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
106+
}
107+
108+
// If the user didn't declare the annotation with an explicit value,
109+
// use the value of the alias instead.
110+
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
111+
attributeValue = aliasValue;
112+
}
113+
}
109114
}
110115
}
111116

spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,22 @@
2929
*
3030
* <h3>Usage Scenarios</h3>
3131
* <ul>
32-
* <li><strong>Aliases within an annotation</strong>: within a single
32+
* <li><strong>Explicit aliases within an annotation</strong>: within a single
3333
* annotation, {@code @AliasFor} can be declared on a pair of attributes to
3434
* signal that they are interchangeable aliases for each other.</li>
35-
* <li><strong>Alias for attribute in meta-annotation</strong>: if the
35+
* <li><strong>Explicit alias for attribute in meta-annotation</strong>: if the
3636
* {@link #annotation} attribute of {@code @AliasFor} is set to a different
3737
* annotation than the one that declares it, the {@link #attribute} is
3838
* interpreted as an alias for an attribute in a meta-annotation (i.e., an
3939
* explicit meta-annotation attribute override). This enables fine-grained
4040
* control over exactly which attributes are overridden within an annotation
4141
* hierarchy. In fact, with {@code @AliasFor} it is even possible to declare
4242
* an alias for the {@code value} attribute of a meta-annotation.</li>
43+
* <li><strong>Implicit aliases within an annotation</strong>: if one or
44+
* more attributes within an annotation are declared as explicit
45+
* meta-annotation attribute overrides for the same attribute in the
46+
* meta-annotation, those attributes will be treated as a set of <em>implicit</em>
47+
* aliases for each other, analogous to explicit aliases within an annotation.</li>
4348
* </ul>
4449
*
4550
* <h3>Usage Requirements</h3>
@@ -57,31 +62,44 @@
5762
*
5863
* <h3>Implementation Requirements</h3>
5964
* <ul>
60-
* <li><strong>Aliases within an annotation</strong>:
65+
* <li><strong>Explicit aliases within an annotation</strong>:
6166
* <ol>
6267
* <li>Each attribute that makes up an aliased pair must be annotated with
63-
* {@code @AliasFor}, and either the {@link #attribute} or the {@link #value}
64-
* attribute must reference the <em>other</em> attribute in the pair.</li>
68+
* {@code @AliasFor}, and either {@link #attribute} or {@link #value} must
69+
* reference the <em>other</em> attribute in the pair.</li>
6570
* <li>Aliased attributes must declare the same return type.</li>
6671
* <li>Aliased attributes must declare a default value.</li>
6772
* <li>Aliased attributes must declare the same default value.</li>
68-
* <li>The {@link #annotation} attribute should remain set to the default.</li>
73+
* <li>{@link #annotation} should not be declared.</li>
6974
* </ol>
7075
* </li>
71-
* <li><strong>Alias for attribute in meta-annotation</strong>:
76+
* <li><strong>Explicit alias for attribute in meta-annotation</strong>:
7277
* <ol>
7378
* <li>The attribute that is an alias for an attribute in a meta-annotation
74-
* must be annotated with {@code @AliasFor}, and the {@link #attribute} must
75-
* reference the aliased attribute in the meta-annotation.</li>
79+
* must be annotated with {@code @AliasFor}, and {@link #attribute} must
80+
* reference the attribute in the meta-annotation.</li>
7681
* <li>Aliased attributes must declare the same return type.</li>
77-
* <li>The {@link #annotation} must reference the meta-annotation.</li>
82+
* <li>{@link #annotation} must reference the meta-annotation.</li>
83+
* <li>The referenced meta-annotation must be <em>meta-present</em> on the
84+
* annotation class that declares {@code @AliasFor}.</li>
85+
* </ol>
86+
* </li>
87+
* <li><strong>Implicit aliases within an annotation</strong>:
88+
* <ol>
89+
* <li>Each attribute that belongs to the set of implicit aliases must be
90+
* annotated with {@code @AliasFor}, and {@link #attribute} must reference
91+
* the same attribute in the same meta-annotation.</li>
92+
* <li>Aliased attributes must declare the same return type.</li>
93+
* <li>Aliased attributes must declare a default value.</li>
94+
* <li>Aliased attributes must declare the same default value.</li>
95+
* <li>{@link #annotation} must reference the meta-annotation.</li>
7896
* <li>The referenced meta-annotation must be <em>meta-present</em> on the
7997
* annotation class that declares {@code @AliasFor}.</li>
8098
* </ol>
8199
* </li>
82100
* </ul>
83101
*
84-
* <h3>Example: Aliases within an Annotation</h3>
102+
* <h3>Example: Explicit Aliases within an Annotation</h3>
85103
* <pre class="code"> public &#064;interface ContextConfiguration {
86104
*
87105
* &#064;AliasFor("locations")
@@ -93,14 +111,28 @@
93111
* // ...
94112
* }</pre>
95113
*
96-
* <h3>Example: Alias for Attribute in Meta-annotation</h3>
114+
* <h3>Example: Explicit Alias for Attribute in Meta-annotation</h3>
97115
* <pre class="code"> &#064;ContextConfiguration
98116
* public &#064;interface MyTestConfig {
99117
*
100118
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
101119
* String[] xmlFiles();
102120
* }</pre>
103121
*
122+
* <h3>Example: Implicit Aliases within an Annotation</h3>
123+
* <pre class="code"> &#064;ContextConfiguration
124+
* public &#064;interface MyTestConfig {
125+
*
126+
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
127+
* String[] value() default {};
128+
*
129+
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
130+
* String[] groovyScripts() default {};
131+
*
132+
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
133+
* String[] xmlFiles() default {};
134+
* }</pre>
135+
*
104136
* <h3>Spring Annotations Supporting Attribute Aliases</h3>
105137
* <p>As of Spring Framework 4.2, several annotations within core Spring
106138
* have been updated to use {@code @AliasFor} to configure their internal

spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import org.springframework.util.Assert;
3232
import org.springframework.util.LinkedMultiValueMap;
3333
import org.springframework.util.MultiValueMap;
34-
import org.springframework.util.StringUtils;
3534

3635
/**
3736
* General utility methods for finding annotations and meta-annotations on
@@ -957,13 +956,21 @@ public void postProcess(AnnotatedElement element, Annotation annotation, Annotat
957956

958957
for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
959958
String attributeName = attributeMethod.getName();
960-
String aliasedAttributeName = AnnotationUtils.getAliasedAttributeName(attributeMethod,
961-
targetAnnotationType);
959+
List<String> aliases = AnnotationUtils.getAliasedAttributeNames(attributeMethod, targetAnnotationType);
962960

963961
// Explicit annotation attribute override declared via @AliasFor
964-
if (StringUtils.hasText(aliasedAttributeName) && attributes.containsKey(aliasedAttributeName)) {
965-
overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
962+
if (!aliases.isEmpty()) {
963+
if (aliases.size() != 1) {
964+
throw new IllegalStateException(String.format(
965+
"Alias list for annotation attribute [%s] must contain at most one element: %s",
966+
attributeMethod, aliases));
967+
}
968+
String aliasedAttributeName = aliases.get(0);
969+
if (attributes.containsKey(aliasedAttributeName)) {
970+
overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
971+
}
966972
}
973+
967974
// Implicit annotation attribute override based on convention
968975
else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
969976
overrideAttribute(element, annotation, attributes, attributeName, attributeName);

spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.lang.reflect.Array;
2222
import java.util.Iterator;
2323
import java.util.LinkedHashMap;
24+
import java.util.List;
2425
import java.util.Map;
2526

2627
import org.springframework.util.Assert;
@@ -422,26 +423,38 @@ private <T> T getRequiredAttributeWithAlias(String attributeName, Class<? extend
422423
Assert.notNull(expectedType, "expectedType must not be null");
423424

424425
T attributeValue = getAttribute(attributeName, expectedType);
425-
String aliasName = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
426-
T aliasValue = getAttribute(aliasName, expectedType);
427-
boolean attributeDeclared = !ObjectUtils.isEmpty(attributeValue);
428-
boolean aliasDeclared = !ObjectUtils.isEmpty(aliasValue);
429-
430-
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) && attributeDeclared && aliasDeclared) {
431-
String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString());
432-
String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] " +
433-
"are present with values of [%s] and [%s], but only one is permitted.",
434-
annotationType.getName(), elementName, attributeName, aliasName,
435-
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
436-
throw new AnnotationConfigurationException(msg);
437-
}
438426

439-
if (!attributeDeclared) {
440-
attributeValue = aliasValue;
427+
List<String> aliasNames = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
428+
if (aliasNames != null) {
429+
for (String aliasName : aliasNames) {
430+
T aliasValue = getAttribute(aliasName, expectedType);
431+
boolean attributeEmpty = ObjectUtils.isEmpty(attributeValue);
432+
boolean aliasEmpty = ObjectUtils.isEmpty(aliasValue);
433+
434+
if (!attributeEmpty && !aliasEmpty && !ObjectUtils.nullSafeEquals(attributeValue, aliasValue)) {
435+
String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString());
436+
String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] " +
437+
"are present with values of [%s] and [%s], but only one is permitted.",
438+
annotationType.getName(), elementName, attributeName, aliasName,
439+
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
440+
throw new AnnotationConfigurationException(msg);
441+
}
442+
443+
// If we expect an array and the current tracked value is null but the
444+
// current alias value is non-null, then replace the current null value
445+
// with the non-null value (which may be an empty array).
446+
if (expectedType.isArray() && attributeValue == null && aliasValue != null) {
447+
attributeValue = aliasValue;
448+
}
449+
// Else: if we're not expecting an array, we can rely on the behavior of
450+
// ObjectUtils.isEmpty().
451+
else if (attributeEmpty && !aliasEmpty) {
452+
attributeValue = aliasValue;
453+
}
454+
}
455+
assertAttributePresence(attributeName, aliasNames, attributeValue);
441456
}
442457

443-
assertAttributePresence(attributeName, aliasName, attributeValue);
444-
445458
return attributeValue;
446459
}
447460

@@ -473,11 +486,11 @@ private void assertAttributePresence(String attributeName, Object attributeValue
473486
}
474487
}
475488

476-
private void assertAttributePresence(String attributeName, String aliasName, Object attributeValue) {
489+
private void assertAttributePresence(String attributeName, List<String> aliases, Object attributeValue) {
477490
if (attributeValue == null) {
478491
throw new IllegalArgumentException(String.format(
479-
"Neither attribute '%s' nor its alias '%s' was found in attributes for annotation [%s]",
480-
attributeName, aliasName, this.displayName));
492+
"Neither attribute '%s' nor one of its aliases %s was found in attributes for annotation [%s]",
493+
attributeName, aliases, this.displayName));
481494
}
482495
}
483496

0 commit comments

Comments
 (0)