Support annotation attribute aliases and overrides via @AliasFor

This commit introduces first-class support for aliases for annotation
attributes. Specifically, this commit introduces a new @AliasFor
annotation that can be used to declare a pair of aliased attributes
within a single annotation or an alias from an attribute in a custom
composed annotation to an attribute in a meta-annotation.

To support @AliasFor within annotation instances, AnnotationUtils has
been overhauled to "synthesize" any annotations returned by "get" and
"find" searches. A SynthesizedAnnotation is an annotation that is
wrapped in a JDK dynamic proxy which provides run-time support for
@AliasFor semantics. SynthesizedAnnotationInvocationHandler is the
actual handler behind the proxy.

In addition, the contract for @AliasFor is fully validated, and an
AnnotationConfigurationException is thrown in case invalid
configuration is detected.

For example, @ContextConfiguration from the spring-test module is now
declared as follows:

    public @interface ContextConfiguration {

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

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

        // ...
    }

The following annotations and their related support classes have been
modified to use @AliasFor.

- @ManagedResource
- @ContextConfiguration
- @ActiveProfiles
- @TestExecutionListeners
- @TestPropertySource
- @Sql
- @ControllerAdvice
- @RequestMapping

Similarly, support for AnnotationAttributes has been reworked to
support @AliasFor as well. This allows for fine-grained control over
exactly which attributes are overridden within an annotation hierarchy.
In fact, it is now possible to declare an alias for the 'value'
attribute of a meta-annotation.

For example, given the revised declaration of @ContextConfiguration
above, one can now develop a composed annotation with a custom
attribute override as follows.

    @ContextConfiguration
    public @interface MyTestConfig {

        @AliasFor(
           annotation = ContextConfiguration.class,
           attribute = "locations"
        )
        String[] xmlFiles();

        // ...
    }

Consequently, the following are functionally equivalent.

- @MyTestConfig(xmlFiles = "test.xml")
- @ContextConfiguration("test.xml")
- @ContextConfiguration(locations = "test.xml").

Issue: SPR-11512, SPR-11513
This commit is contained in:
Sam Brannen
2015-05-14 23:32:30 +02:00
parent a87d5f8a63
commit ca66e076d1
31 changed files with 1582 additions and 336 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -23,6 +23,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* {@code ActiveProfiles} is a class-level annotation that is used to declare
* which <em>active bean definition profiles</em> should be used when loading
@@ -53,6 +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")
String[] value() default {};
/**
@@ -61,6 +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")
String[] profiles() default {};
/**

View File

@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AliasFor;
/**
* {@code @ContextConfiguration} defines class-level metadata that is used to determine
@@ -97,6 +98,7 @@ public @interface ContextConfiguration {
* @since 3.0
* @see #inheritLocations
*/
@AliasFor(attribute = "locations")
String[] value() default {};
/**
@@ -127,6 +129,7 @@ public @interface ContextConfiguration {
* @since 2.5
* @see #inheritLocations
*/
@AliasFor(attribute = "value")
String[] locations() default {};
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -68,7 +68,7 @@ public class ContextConfigurationAttributes {
* @param contextConfiguration the annotation from which to retrieve the attributes
*/
public ContextConfigurationAttributes(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(),
this(declaringClass, contextConfiguration.locations(), contextConfiguration.classes(),
contextConfiguration.inheritLocations(), contextConfiguration.initializers(),
contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader());
}
@@ -83,12 +83,9 @@ public class ContextConfigurationAttributes {
*/
@SuppressWarnings("unchecked")
public ContextConfigurationAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) {
this(declaringClass,
resolveLocations(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getStringArray("value")),
annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"),
(Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"),
annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"),
(Class<? extends ContextLoader>) annAttrs.getClass("loader"));
this(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"),
(Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"),
annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"), (Class<? extends ContextLoader>) annAttrs.getClass("loader"));
}
/**
@@ -159,37 +156,6 @@ public class ContextConfigurationAttributes {
}
/**
* Resolve resource locations from the {@link ContextConfiguration#locations() locations}
* and {@link ContextConfiguration#value() value} attributes of the supplied
* {@link ContextConfiguration} annotation.
* @throws IllegalStateException if both the locations and value attributes have been declared
*/
private static String[] resolveLocations(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
return resolveLocations(declaringClass, contextConfiguration.locations(), contextConfiguration.value());
}
/**
* Resolve resource locations from the supplied {@code locations} and
* {@code value} arrays, which correspond to attributes of the same names in
* the {@link ContextConfiguration} annotation.
* @throws IllegalStateException if both the locations and value attributes have been declared
*/
private static String[] resolveLocations(Class<?> declaringClass, String[] locations, String[] value) {
Assert.notNull(declaringClass, "declaringClass must not be null");
if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) {
throw new IllegalStateException(String.format("Test class [%s] has been configured with " +
"@ContextConfiguration's 'value' %s and 'locations' %s attributes. Only one declaration " +
"of resource locations is permitted per @ContextConfiguration annotation.",
declaringClass.getName(), ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations)));
}
else if (!ObjectUtils.isEmpty(value)) {
locations = value;
}
return locations;
}
/**
* Get the {@linkplain Class class} that declared the
* {@link ContextConfiguration @ContextConfiguration} annotation.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -23,6 +23,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* {@code TestExecutionListeners} defines class-level metadata for configuring
* which {@link TestExecutionListener TestExecutionListeners} should be
@@ -85,6 +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")
Class<? extends TestExecutionListener>[] value() default {};
/**
@@ -100,6 +103,7 @@ public @interface TestExecutionListeners {
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
* @see org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
*/
@AliasFor(attribute = "value")
Class<? extends TestExecutionListener>[] listeners() default {};
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -23,6 +23,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* {@code @TestPropertySource} is a class-level annotation that is used to
* configure the {@link #locations} of properties files and inlined
@@ -94,6 +96,7 @@ public @interface TestPropertySource {
*
* @see #locations
*/
@AliasFor(attribute = "locations")
String[] value() default {};
/**
@@ -141,6 +144,7 @@ public @interface TestPropertySource {
* @see #properties
* @see org.springframework.core.env.PropertySource
*/
@AliasFor(attribute = "value")
String[] locations() default {};
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -22,6 +22,8 @@ import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
@@ -93,6 +95,7 @@ public @interface Sql {
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #scripts}, but it may be used instead of {@link #scripts}.
*/
@AliasFor(attribute = "scripts")
String[] value() default {};
/**
@@ -126,6 +129,7 @@ public @interface Sql {
* {@code "classpath:com/example/MyTest.testMethod.sql"}.</li>
* </ul>
*/
@AliasFor(attribute = "value")
String[] scripts() default {};
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -255,24 +255,6 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
private String[] getScripts(Sql sql, TestContext testContext, boolean classLevel) {
String[] scripts = sql.scripts();
String[] value = sql.value();
boolean scriptsDeclared = !ObjectUtils.isEmpty(scripts);
boolean valueDeclared = !ObjectUtils.isEmpty(value);
if (valueDeclared && scriptsDeclared) {
String elementType = (classLevel ? "class" : "method");
String elementName = (classLevel ? testContext.getTestClass().getName()
: testContext.getTestMethod().toString());
String msg = String.format("Test %s [%s] has been configured with @Sql's 'value' [%s] "
+ "and 'scripts' [%s] attributes. Only one declaration of SQL script "
+ "paths is permitted per @Sql annotation.", elementType, elementName,
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(scripts));
logger.error(msg);
throw new IllegalStateException(msg);
}
if (valueDeclared) {
scripts = value;
}
if (ObjectUtils.isEmpty(scripts)) {
scripts = new String[] { detectDefaultScript(testContext, classLevel) };
}

View File

@@ -53,7 +53,6 @@ import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -147,18 +146,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
declaringClass.getName()));
}
Class<? extends TestExecutionListener>[] valueListenerClasses = (Class<? extends TestExecutionListener>[]) annAttrs.getClassArray("value");
Class<? extends TestExecutionListener>[] listenerClasses = (Class<? extends TestExecutionListener>[]) annAttrs.getClassArray("listeners");
if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) {
throw new IllegalStateException(String.format(
"Class [%s] configured with @TestExecutionListeners' "
+ "'value' [%s] and 'listeners' [%s] attributes. Use one or the other, but not both.",
declaringClass.getName(), ObjectUtils.nullSafeToString(valueListenerClasses),
ObjectUtils.nullSafeToString(listenerClasses)));
}
else if (!ObjectUtils.isEmpty(valueListenerClasses)) {
listenerClasses = valueListenerClasses;
}
boolean inheritListeners = annAttrs.getBoolean("inheritListeners");
AnnotationDescriptor<TestExecutionListeners> superDescriptor = MetaAnnotationUtils.findAnnotationDescriptor(

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -29,7 +29,6 @@ import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -94,7 +93,6 @@ abstract class ActiveProfilesUtils {
logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].",
annAttrs, declaringClass.getName()));
}
validateActiveProfilesConfiguration(declaringClass, annAttrs);
Class<? extends ActiveProfilesResolver> resolverClass = annAttrs.getClass("resolver");
if (ActiveProfilesResolver.class == resolverClass) {
@@ -134,20 +132,4 @@ abstract class ActiveProfilesUtils {
return StringUtils.toStringArray(activeProfiles);
}
private static void validateActiveProfilesConfiguration(Class<?> declaringClass, AnnotationAttributes annAttrs) {
String[] valueProfiles = annAttrs.getStringArray("value");
String[] profiles = annAttrs.getStringArray("profiles");
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
boolean profilesDeclared = !ObjectUtils.isEmpty(profiles);
if (valueDeclared && profilesDeclared) {
String msg = String.format("Class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
+ "and 'profiles' [%s] attributes. Only one declaration of active bean "
+ "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles));
logger.error(msg);
throw new IllegalStateException(msg);
}
}
}

View File

@@ -26,7 +26,6 @@ import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.springframework.test.util.MetaAnnotationUtils.*;
@@ -81,25 +80,7 @@ public class DefaultActiveProfilesResolver implements ActiveProfilesResolver {
annAttrs, declaringClass.getName()));
}
String[] profiles = annAttrs.getStringArray("profiles");
String[] valueProfiles = annAttrs.getStringArray("value");
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
boolean profilesDeclared = !ObjectUtils.isEmpty(profiles);
if (valueDeclared && profilesDeclared) {
String msg = String.format("Class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
+ "and 'profiles' [%s] attributes. Only one declaration of active bean "
+ "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles));
logger.error(msg);
throw new IllegalStateException(msg);
}
if (valueDeclared) {
profiles = valueProfiles;
}
for (String profile : profiles) {
for (String profile : annAttrs.getStringArray("profiles")) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -67,8 +67,7 @@ class TestPropertySourceAttributes {
* @param annAttrs the annotation attributes from which to retrieve the attributes
*/
TestPropertySourceAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) {
this(declaringClass, resolveLocations(declaringClass, annAttrs.getStringArray("locations"),
annAttrs.getStringArray("value")), annAttrs.getBoolean("inheritLocations"),
this(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getBoolean("inheritLocations"),
annAttrs.getStringArray("properties"), annAttrs.getBoolean("inheritProperties"));
}
@@ -156,31 +155,6 @@ class TestPropertySourceAttributes {
.toString();
}
/**
* Resolve resource locations from the supplied {@code locations} and
* {@code value} arrays, which correspond to attributes of the same names in
* the {@link TestPropertySource} annotation.
*
* @throws IllegalStateException if both the locations and value attributes have been declared
*/
private static String[] resolveLocations(Class<?> declaringClass, String[] locations, String[] value) {
Assert.notNull(declaringClass, "declaringClass must not be null");
if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) {
String msg = String.format("Class [%s] has been configured with @TestPropertySource's 'value' [%s] "
+ "and 'locations' [%s] attributes. Only one declaration of resource "
+ "locations is permitted per @TestPropertySource annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations));
logger.error(msg);
throw new IllegalStateException(msg);
}
else if (!ObjectUtils.isEmpty(value)) {
locations = value;
}
return locations;
}
/**
* Detect a default properties file for the supplied class, as specified
* in the class-level Javadoc for {@link TestPropertySource}.