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:
@@ -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 {};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
|
||||
Reference in New Issue
Block a user