Introduce 'value' alias for 'attribute' in @AliasFor

SPR-11512 introduced support for annotation attribute aliases via
@AliasFor, requiring the explicit declaration of the 'attribute'
attribute. However, for aliases within an annotation, this explicit
declaration is unnecessary.

This commit improves the readability of alias pairs declared within an
annotation by introducing a 'value' attribute in @AliasFor that is an
alias for the existing 'attribute' attribute. This allows annotations
such as @ContextConfiguration from the spring-test module to declare
aliases as follows.

public @interface ContextConfiguration {

     @AliasFor("locations")
     String[] value() default {};

     @AliasFor("value")
     String[] locations() default {};

    // ...
}

Issue: SPR-13289
This commit is contained in:
Sam Brannen 2015-07-29 15:27:06 +02:00
parent 90493f49e6
commit 725292081e
35 changed files with 170 additions and 84 deletions

View File

@ -44,7 +44,7 @@ public @interface CacheEvict {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor(attribute = "cacheNames")
@AliasFor("cacheNames")
String[] value() default {};
/**
@ -55,7 +55,7 @@ public @interface CacheEvict {
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] cacheNames() default {};
/**

View File

@ -50,7 +50,7 @@ public @interface CachePut {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor(attribute = "cacheNames")
@AliasFor("cacheNames")
String[] value() default {};
/**
@ -61,7 +61,7 @@ public @interface CachePut {
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] cacheNames() default {};
/**

View File

@ -55,7 +55,7 @@ public @interface Cacheable {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor(attribute = "cacheNames")
@AliasFor("cacheNames")
String[] value() default {};
/**
@ -66,7 +66,7 @@ public @interface Cacheable {
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] cacheNames() default {};
/**

View File

@ -62,7 +62,7 @@ public @interface ComponentScan {
* are needed — for example, {@code @ComponentScan("org.my.pkg")}
* instead of {@code @ComponentScan(basePackages = "org.my.pkg")}.
*/
@AliasFor(attribute = "basePackages")
@AliasFor("basePackages")
String[] value() default {};
/**
@ -72,7 +72,7 @@ public @interface ComponentScan {
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
* String-based package names.
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] basePackages() default {};
/**
@ -166,7 +166,7 @@ public @interface ComponentScan {
* Alias for {@link #classes}.
* @see #classes
*/
@AliasFor(attribute = "classes")
@AliasFor("classes")
Class<?>[] value() default {};
/**
@ -190,7 +190,7 @@ public @interface ComponentScan {
* @see #value
* @see #type
*/
@AliasFor(attribute = "value")
@AliasFor("value")
Class<?>[] classes() default {};
/**

View File

@ -59,7 +59,7 @@ public @interface ImportResource {
* @see #locations
* @see #reader
*/
@AliasFor(attribute = "locations")
@AliasFor("locations")
String[] value() default {};
/**
@ -72,7 +72,7 @@ public @interface ImportResource {
* @see #value
* @see #reader
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] locations() default {};
/**

View File

@ -61,7 +61,7 @@ public @interface Scope {
* Alias for {@link #scopeName}.
* @see #scopeName
*/
@AliasFor(attribute = "scopeName")
@AliasFor("scopeName")
String value() default "";
/**
@ -75,7 +75,7 @@ public @interface Scope {
* @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
* @see #value
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String scopeName() default "";
/**

View File

@ -69,7 +69,7 @@ public @interface EventListener {
/**
* Alias for {@link #classes}.
*/
@AliasFor(attribute = "classes")
@AliasFor("classes")
Class<?>[] value() default {};
/**
@ -79,7 +79,7 @@ public @interface EventListener {
* attribute is specified with multiple values, the annotated method
* must <em>not</em> declare any parameters.
*/
@AliasFor(attribute = "value")
@AliasFor("value")
Class<?>[] classes() default {};
/**

View File

@ -49,10 +49,10 @@ public @interface ManagedResource {
/**
* Alias for the {@link #objectName} attribute, for simple default usage.
*/
@AliasFor(attribute = "objectName")
@AliasFor("objectName")
String value() default "";
@AliasFor(attribute = "value")
@AliasFor("value")
String objectName() default "";
String description() default "";

View File

@ -120,9 +120,21 @@ import java.lang.annotation.Target;
public @interface AliasFor {
/**
* The name of the attribute that <em>this</em> attribute is an alias for.
* Alias for {@link #attribute}.
* <p>Intended to be used instead of {@link #attribute} when {@link #annotation}
* is not declared &mdash; for example: {@code @AliasFor("value")} instead of
* {@code @AliasFor(attribute = "value")}.
*/
String attribute();
@AliasFor("attribute")
String value() default "";
/**
* The name of the attribute that <em>this</em> attribute is an alias for.
* @see #value
*/
@AliasFor("value")
String attribute() default "";
/**
* The type of annotation in which the aliased {@link #attribute} is declared.

View File

@ -1427,7 +1427,7 @@ public abstract class AnnotationUtils {
* @see #getAliasedAttributeName(Method, Class)
*/
static String getAliasedAttributeName(Method attribute) {
return getAliasedAttributeName(attribute, null);
return getAliasedAttributeName(attribute, (Class<? extends Annotation>) null);
}
/**
@ -1471,7 +1471,7 @@ public abstract class AnnotationUtils {
}
String attributeName = attribute.getName();
String aliasedAttributeName = aliasFor.attribute();
String aliasedAttributeName = getAliasedAttributeName(aliasFor, attribute);
if (!StringUtils.hasText(aliasedAttributeName)) {
String msg = String.format(
@ -1503,7 +1503,7 @@ public abstract class AnnotationUtils {
throw new AnnotationConfigurationException(msg);
}
String mirrorAliasedAttributeName = mirrorAliasFor.attribute();
String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor, aliasedAttribute);
if (!attributeName.equals(mirrorAliasedAttributeName)) {
String msg = String.format(
"Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].",
@ -1543,6 +1543,38 @@ public abstract class AnnotationUtils {
return aliasedAttributeName;
}
/**
* Get the name of the aliased attribute configured via the supplied
* {@link AliasFor @AliasFor} annotation on the supplied {@code attribute}.
* <p>This method returns the value of either the {@code attribute}
* or {@code value} attribute of {@code @AliasFor}, ensuring that only
* one of the attributes has been declared.
* @param aliasFor the {@code @AliasFor} annotation from which to retrieve
* the aliased attribute name
* @param attribute the attribute that is annotated with {@code @AliasFor},
* used solely for building an exception message
* @return the name of the aliased attribute, potentially an empty string
* @throws AnnotationConfigurationException if invalid configuration of
* {@code @AliasFor} is detected
* @since 4.2
* @see #getAliasedAttributeName(Method, Class)
*/
private static String getAliasedAttributeName(AliasFor aliasFor, Method attribute) {
String attributeName = aliasFor.attribute();
String value = aliasFor.value();
boolean attributeDeclared = StringUtils.hasText(attributeName);
boolean valueDeclared = StringUtils.hasText(value);
if (attributeDeclared && valueDeclared) {
throw new AnnotationConfigurationException(String.format(
"In @AliasFor declared on attribute [%s] in annotation [%s], attribute 'attribute' and its alias 'value' "
+ "are present with values of [%s] and [%s], but only one is permitted.",
attribute.getName(), attribute.getDeclaringClass().getName(), attributeName, value));
}
return (attributeDeclared ? attributeName : value);
}
/**
* Get all methods declared in the supplied {@code annotationType} that
* match Java's requirements for annotation <em>attributes</em>.

View File

@ -714,10 +714,30 @@ public class AnnotationUtilsTests {
assertEquals("actual value attribute: ", "/test", synthesizedAgainWebMapping.value());
}
@Test
public void synthesizeAnnotationWhereAliasForIsMissingAttributeDeclaration() throws Exception {
AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation(AliasForWithMissingAttributeDeclaration.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("@AliasFor declaration on attribute [foo] in annotation"));
exception.expectMessage(containsString(AliasForWithMissingAttributeDeclaration.class.getName()));
exception.expectMessage(containsString("is missing required 'attribute' value"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWhereAliasForHasDuplicateAttributeDeclaration() throws Exception {
AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation(AliasForWithDuplicateAttributeDeclaration.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("In @AliasFor declared on attribute [foo] in annotation"));
exception.expectMessage(containsString(AliasForWithDuplicateAttributeDeclaration.class.getName()));
exception.expectMessage(containsString("attribute 'attribute' and its alias 'value' are present with values of [baz] and [bar]"));
exception.expectMessage(containsString("but only one is permitted"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception {
AliasForNonexistentAttribute annotation =
AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class);
AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [foo] in"));
exception.expectMessage(containsString(AliasForNonexistentAttribute.class.getName()));
@ -1434,7 +1454,7 @@ public class AnnotationUtilsTests {
String name();
@AliasFor(attribute = "path")
@AliasFor("path")
String value() default "";
@AliasFor(attribute = "value")
@ -1472,10 +1492,10 @@ public class AnnotationUtilsTests {
@Retention(RetentionPolicy.RUNTIME)
@interface ContextConfig {
@AliasFor(attribute = "locations")
@AliasFor("locations")
String value() default "";
@AliasFor(attribute = "value")
@AliasFor("value")
String locations() default "";
}
@ -1483,10 +1503,10 @@ public class AnnotationUtilsTests {
@interface BrokenContextConfig {
// Intentionally missing:
// @AliasFor(attribute = "locations")
// @AliasFor("locations")
String value() default "";
@AliasFor(attribute = "value")
@AliasFor("value")
String locations() default "";
}
@ -1530,10 +1550,32 @@ public class AnnotationUtilsTests {
}
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForWithMissingAttributeDeclaration {
@AliasFor
String foo() default "";
}
@AliasForWithMissingAttributeDeclaration
static class AliasForWithMissingAttributeDeclarationClass {
}
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForWithDuplicateAttributeDeclaration {
@AliasFor(value = "bar", attribute = "baz")
String foo() default "";
}
@AliasForWithDuplicateAttributeDeclaration
static class AliasForWithDuplicateAttributeDeclarationClass {
}
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForNonexistentAttribute {
@AliasFor(attribute = "bar")
@AliasFor("bar")
String foo() default "";
}
@ -1544,7 +1586,7 @@ public class AnnotationUtilsTests {
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForWithoutMirroredAliasFor {
@AliasFor(attribute = "bar")
@AliasFor("bar")
String foo() default "";
String bar() default "";
@ -1571,10 +1613,10 @@ public class AnnotationUtilsTests {
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForAttributeOfDifferentType {
@AliasFor(attribute = "bar")
@AliasFor("bar")
String[] foo() default "";
@AliasFor(attribute = "foo")
@AliasFor("foo")
boolean bar() default true;
}
@ -1599,10 +1641,10 @@ public class AnnotationUtilsTests {
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForAttributeWithDifferentDefaultValue {
@AliasFor(attribute = "bar")
@AliasFor("bar")
String foo() default "X";
@AliasFor(attribute = "foo")
@AliasFor("foo")
String bar() default "Z";
}

View File

@ -32,9 +32,9 @@ import org.springframework.core.annotation.AliasFor;
String name();
@AliasFor(attribute = "path")
@AliasFor("path")
String value() default "";
@AliasFor(attribute = "value")
@AliasFor("value")
String path() default "";
}

View File

@ -39,14 +39,14 @@ public @interface Header {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
* The name of the request header to bind to.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -43,7 +43,7 @@ public @interface Payload {
/**
* Alias for {@link #expression}.
*/
@AliasFor(attribute = "expression")
@AliasFor("expression")
String value() default "";
/**
@ -54,7 +54,7 @@ public @interface Payload {
* <p>When processing STOMP over WebSocket messages this attribute is not supported.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String expression() default "";
/**

View File

@ -46,7 +46,7 @@ public @interface SendToUser {
* Alias for {@link #destinations}.
* @see #destinations
*/
@AliasFor(attribute = "destinations")
@AliasFor("destinations")
String[] value() default {};
/**
@ -57,7 +57,7 @@ public @interface SendToUser {
* @see #value
* @see org.springframework.messaging.simp.annotation.support.SendToMethodReturnValueHandler
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] destinations() default {};
/**

View File

@ -55,7 +55,7 @@ public @interface ActiveProfiles {
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #profiles}, but it may be used <em>instead</em> of {@link #profiles}.
*/
@AliasFor(attribute = "profiles")
@AliasFor("profiles")
String[] value() default {};
/**
@ -64,7 +64,7 @@ public @interface ActiveProfiles {
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #value}, but it may be used <em>instead</em> of {@link #value}.
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] profiles() default {};
/**

View File

@ -98,7 +98,7 @@ public @interface ContextConfiguration {
* @since 3.0
* @see #inheritLocations
*/
@AliasFor(attribute = "locations")
@AliasFor("locations")
String[] value() default {};
/**
@ -129,7 +129,7 @@ public @interface ContextConfiguration {
* @since 2.5
* @see #inheritLocations
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] locations() default {};
/**

View File

@ -87,7 +87,7 @@ public @interface TestExecutionListeners {
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #listeners}, but it may be used instead of {@link #listeners}.
*/
@AliasFor(attribute = "listeners")
@AliasFor("listeners")
Class<? extends TestExecutionListener>[] value() default {};
/**
@ -103,7 +103,7 @@ public @interface TestExecutionListeners {
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
* @see org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
*/
@AliasFor(attribute = "value")
@AliasFor("value")
Class<? extends TestExecutionListener>[] listeners() default {};
/**

View File

@ -96,7 +96,7 @@ public @interface TestPropertySource {
*
* @see #locations
*/
@AliasFor(attribute = "locations")
@AliasFor("locations")
String[] value() default {};
/**
@ -144,7 +144,7 @@ public @interface TestPropertySource {
* @see #properties
* @see org.springframework.core.env.PropertySource
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] locations() default {};
/**

View File

@ -98,7 +98,7 @@ public @interface Sql {
* @see #scripts
* @see #statements
*/
@AliasFor(attribute = "scripts")
@AliasFor("scripts")
String[] value() default {};
/**
@ -139,7 +139,7 @@ public @interface Sql {
* @see #value
* @see #statements
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] scripts() default {};
/**

View File

@ -60,7 +60,7 @@ public @interface Transactional {
* Alias for {@link #transactionManager}.
* @see #transactionManager
*/
@AliasFor(attribute = "transactionManager")
@AliasFor("transactionManager")
String value() default "";
/**
@ -72,7 +72,7 @@ public @interface Transactional {
* @since 4.2
* @see #value
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String transactionManager() default "";
/**

View File

@ -59,7 +59,7 @@ public @interface TransactionalEventListener {
/**
* Alias for {@link #classes}.
*/
@AliasFor(attribute = "classes")
@AliasFor("classes")
Class<?>[] value() default {};
/**
@ -68,7 +68,7 @@ public @interface TransactionalEventListener {
* may or may not be specified. When this attribute is specified with more
* than one value, the method must not have a parameter.
*/
@AliasFor(attribute = "value")
@AliasFor("value")
Class<?>[] classes() default {};
/**

View File

@ -68,7 +68,7 @@ public @interface ControllerAdvice {
* @since 4.0
* @see #basePackages()
*/
@AliasFor(attribute = "basePackages")
@AliasFor("basePackages")
String[] value() default {};
/**
@ -82,7 +82,7 @@ public @interface ControllerAdvice {
* alternative to String-based package names.
* @since 4.0
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] basePackages() default {};
/**

View File

@ -51,14 +51,14 @@ public @interface CookieValue {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
* The name of the cookie to bind to.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -51,7 +51,7 @@ public @interface CrossOrigin {
/**
* Alias for {@link #origins}.
*/
@AliasFor(attribute = "origins")
@AliasFor("origins")
String[] value() default {};
/**
@ -62,7 +62,7 @@ public @interface CrossOrigin {
* <p>If undefined, all origins are allowed.
* @see #value
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] origins() default {};
/**

View File

@ -50,7 +50,7 @@ public @interface MatrixVariable {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
@ -58,7 +58,7 @@ public @interface MatrixVariable {
* @since 4.2
* @see #value
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -51,14 +51,14 @@ public @interface RequestHeader {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
* The name of the request header to bind to.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -309,7 +309,7 @@ public @interface RequestMapping {
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
*/
@AliasFor(attribute = "path")
@AliasFor("path")
String[] value() default {};
/**
@ -324,7 +324,7 @@ public @interface RequestMapping {
* @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] path() default {};
/**

View File

@ -59,14 +59,14 @@ public @interface RequestParam {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
* The name of the request parameter to bind to.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -67,14 +67,14 @@ public @interface RequestPart {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
* The name of the part in the {@code "multipart/form-data"} request to bind to.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -45,7 +45,7 @@ public @interface ResponseStatus {
/**
* Alias for {@link #code}.
*/
@AliasFor(attribute = "code")
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
/**
@ -55,7 +55,7 @@ public @interface ResponseStatus {
* @since 4.2
* @see javax.servlet.http.HttpServletResponse#setStatus(int)
*/
@AliasFor(attribute = "value")
@AliasFor("value")
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
/**

View File

@ -64,7 +64,7 @@ public @interface SessionAttributes {
/**
* Alias for {@link #names}.
*/
@AliasFor(attribute = "names")
@AliasFor("names")
String[] value() default {};
/**
@ -76,7 +76,7 @@ public @interface SessionAttributes {
* names but rather operate on the model only.
* @since 4.2
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String[] names() default {};
/**

View File

@ -42,7 +42,7 @@ public @interface ActionMapping {
/**
* Alias for {@link #name}.
*/
@AliasFor(attribute = "name")
@AliasFor("name")
String value() default "";
/**
@ -57,7 +57,7 @@ public @interface ActionMapping {
* @see javax.portlet.ActionRequest#ACTION_NAME
* @see #value
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String name() default "";
/**

View File

@ -42,7 +42,7 @@ public @interface RenderMapping {
/**
* Alias for {@link #windowState}.
*/
@AliasFor(attribute = "windowState")
@AliasFor("windowState")
String value() default "";
/**
@ -56,7 +56,7 @@ public @interface RenderMapping {
* @see #value
* @see javax.portlet.PortletRequest#getWindowState()
*/
@AliasFor(attribute = "value")
@AliasFor("value")
String windowState() default "";
/**

View File

@ -429,10 +429,10 @@ method has been added.
----
public @interface ContextConfiguration {
@AliasFor(attribute = "locations")
@AliasFor("locations")
String[] value() default {};
@AliasFor(attribute = "value")
@AliasFor("value")
String[] locations() default {};
// ...