Skip to content

Commit 3eacb83

Browse files
committed
Merge from sbrannen/SPR-13345
* SPR-13345: Support implicit attribute aliases with @AliasFor
2 parents ff9fb9a + d40a35b commit 3eacb83

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)