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
@@ -16,7 +16,6 @@
package org.springframework.core.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
@@ -26,11 +25,14 @@ import java.lang.reflect.Method;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import static org.hamcrest.Matchers.*;
import static java.util.Arrays.*;
import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotatedElementUtils.*;
@@ -46,9 +48,8 @@ public class AnnotatedElementUtilsTests {
private static final String TX_NAME = Transactional.class.getName();
private Set<String> names(Class<?>... classes) {
return stream(classes).map(clazz -> clazz.getName()).collect(Collectors.toSet());
}
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void getMetaAnnotationTypesOnNonAnnotatedClass() {
@@ -180,7 +181,8 @@ public class AnnotatedElementUtilsTests {
public void getAllAnnotationAttributesOnClassWithMultipleComposedAnnotations() {
MultiValueMap<String, Object> attributes = getAllAnnotationAttributes(TxFromMultipleComposedAnnotations.class, TX_NAME);
assertNotNull("Annotation attributes map for @Transactional on TxFromMultipleComposedAnnotations", attributes);
assertEquals("value for TxFromMultipleComposedAnnotations.", asList("TxComposed1", "TxComposed2"), attributes.get("value"));
assertEquals("value for TxFromMultipleComposedAnnotations.", asList("TxInheritedComposed", "TxComposed"),
attributes.get("value"));
}
@Test
@@ -274,6 +276,77 @@ public class AnnotatedElementUtilsTests {
assertTrue(isAnnotated(element, name));
}
@Test
public void getAnnotationAttributesWithConventionBasedComposedAnnotation() {
Class<?> element = ConventionBasedComposedContextConfigClass.class;
String name = ContextConfig.class.getName();
AnnotationAttributes attributes = getAnnotationAttributes(element, name);
assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), attributes);
assertArrayEquals("locations", new String[] { "explicitDeclaration" }, attributes.getStringArray("locations"));
assertArrayEquals("value", new String[] { "explicitDeclaration" }, attributes.getStringArray("value"));
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, name));
}
@Test
public void getAnnotationAttributesWithAliasedComposedAnnotation() {
Class<?> element = AliasedComposedContextConfigClass.class;
String name = ContextConfig.class.getName();
AnnotationAttributes attributes = getAnnotationAttributes(element, name);
assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), attributes);
assertArrayEquals("value", new String[] { "test.xml" }, attributes.getStringArray("value"));
assertArrayEquals("locations", new String[] { "test.xml" }, attributes.getStringArray("locations"));
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, name));
}
@Test
public void getAnnotationAttributesWithAliasedValueComposedAnnotation() {
Class<?> element = AliasedValueComposedContextConfigClass.class;
String name = ContextConfig.class.getName();
AnnotationAttributes attributes = getAnnotationAttributes(element, name);
assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), attributes);
assertArrayEquals("locations", new String[] { "test.xml" }, attributes.getStringArray("locations"));
assertArrayEquals("value", new String[] { "test.xml" }, attributes.getStringArray("value"));
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, name));
}
@Test
public void getAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() {
Class<?> element = InvalidConventionBasedComposedContextConfigClass.class;
String name = ContextConfig.class.getName();
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or(
containsString("attribute [locations] and its alias [value]")));
exception.expectMessage(either(
containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or(
containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one declaration is permitted"));
getAnnotationAttributes(element, name);
}
@Test
public void getAnnotationAttributesWithInvalidAliasedComposedAnnotation() {
Class<?> element = InvalidAliasedComposedContextConfigClass.class;
String name = ContextConfig.class.getName();
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or(
containsString("attribute [locations] and its alias [value]")));
exception.expectMessage(either(containsString("values of [{duplicateDeclaration}] and [{test.xml}]")).or(
containsString("values of [{test.xml}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one declaration is permitted"));
getAnnotationAttributes(element, name);
}
@Test
public void findAnnotationAttributesOnInheritedAnnotationInterface() {
AnnotationAttributes attributes = findAnnotationAttributes(InheritedAnnotationInterface.class, Transactional.class);
@@ -375,27 +448,39 @@ public class AnnotatedElementUtilsTests {
assertEquals("TX qualifier for MetaAndLocalTxConfigClass.", "localTxMgr", attributes.getString("qualifier"));
}
@Test
public void findAnnotationAttributesOnClassWithAttributeAliasesInTargetAnnotation() {
AnnotationAttributes attributes = findAnnotationAttributes(AliasedTransactionalComponentClass.class,
AliasedTransactional.class);
assertNotNull("Should find @AliasedTransactional on AliasedTransactionalComponentClass", attributes);
assertEquals("TX value for AliasedTransactionalComponentClass.", "aliasForQualifier",
attributes.getString("value"));
assertEquals("TX qualifier for AliasedTransactionalComponentClass.", "aliasForQualifier",
attributes.getString("qualifier"));
}
private Set<String> names(Class<?>... classes) {
return stream(classes).map(clazz -> clazz.getName()).collect(Collectors.toSet());
}
// -------------------------------------------------------------------------
@MetaCycle3
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
@Documented
@interface MetaCycle1 {
}
@MetaCycle1
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
@Documented
@interface MetaCycle2 {
}
@MetaCycle2
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@interface MetaCycle3 {
}
@@ -407,7 +492,6 @@ public class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Inherited
@interface Transactional {
@@ -418,19 +502,29 @@ public class AnnotatedElementUtilsTests {
boolean readOnly() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Inherited
@interface AliasedTransactional {
@AliasFor(attribute = "qualifier")
String value() default "";
@AliasFor(attribute = "value")
String qualifier() default "";
}
@Transactional(qualifier = "composed1")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
@interface Composed1 {
@interface InheritedComposed {
}
@Transactional(qualifier = "composed2", readOnly = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@interface Composed2 {
@interface Composed {
}
@Transactional
@@ -440,14 +534,14 @@ public class AnnotatedElementUtilsTests {
String qualifier() default "txMgr";
}
@Transactional("TxComposed1")
@Transactional("TxInheritedComposed")
@Retention(RetentionPolicy.RUNTIME)
@interface TxComposed1 {
@interface TxInheritedComposed {
}
@Transactional("TxComposed2")
@Transactional("TxComposed")
@Retention(RetentionPolicy.RUNTIME)
@interface TxComposed2 {
@interface TxComposed {
}
@Transactional
@@ -461,6 +555,12 @@ public class AnnotatedElementUtilsTests {
@interface ComposedTransactionalComponent {
}
@AliasedTransactional(value = "aliasForQualifier")
@Component
@Retention(RetentionPolicy.RUNTIME)
@interface AliasedTransactionalComponent {
}
@TxComposedWithOverride
// Override default "txMgr" from @TxComposedWithOverride with "localTxMgr"
@Transactional(qualifier = "localTxMgr")
@@ -469,6 +569,63 @@ public class AnnotatedElementUtilsTests {
@interface MetaAndLocalTxConfig {
}
/**
* Mock of {@link org.springframework.test.context.ContextConfiguration}.
*/
@Retention(RetentionPolicy.RUNTIME)
static @interface ContextConfig {
@AliasFor(attribute = "locations")
String[] value() default {};
@AliasFor(attribute = "value")
String[] locations() default {};
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
static @interface ConventionBasedComposedContextConfig {
String[] locations() default {};
}
@ContextConfig(value = "duplicateDeclaration")
@Retention(RetentionPolicy.RUNTIME)
static @interface InvalidConventionBasedComposedContextConfig {
String[] locations();
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasedComposedContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] xmlConfigFiles();
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasedValueComposedContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "value")
String[] locations();
}
/**
* Invalid because the configuration declares a value for 'value' and
* requires a value for the aliased 'locations'. So we likely end up with
* both 'value' and 'locations' being present in {@link AnnotationAttributes}
* but with different values, which violates the contract of {@code @AliasFor}.
*/
@ContextConfig(value = "duplicateDeclaration")
@Retention(RetentionPolicy.RUNTIME)
static @interface InvalidAliasedComposedContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] xmlConfigFiles();
}
// -------------------------------------------------------------------------
static class NonAnnotatedClass {
@@ -485,22 +642,26 @@ public class AnnotatedElementUtilsTests {
static class ComposedTransactionalComponentClass {
}
@AliasedTransactionalComponent
static class AliasedTransactionalComponentClass {
}
@Transactional
static class ClassWithInheritedAnnotation {
}
@Composed2
@Composed
static class SubClassWithInheritedAnnotation extends ClassWithInheritedAnnotation {
}
static class SubSubClassWithInheritedAnnotation extends SubClassWithInheritedAnnotation {
}
@Composed1
@InheritedComposed
static class ClassWithInheritedComposedAnnotation {
}
@Composed2
@Composed
static class SubClassWithInheritedComposedAnnotation extends ClassWithInheritedComposedAnnotation {
}
@@ -519,8 +680,8 @@ public class AnnotatedElementUtilsTests {
static class DerivedTxConfig extends TxConfig {
}
@TxComposed1
@TxComposed2
@TxInheritedComposed
@TxComposed
static class TxFromMultipleComposedAnnotations {
}
@@ -595,4 +756,23 @@ public class AnnotatedElementUtilsTests {
public static interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface {
}
@ConventionBasedComposedContextConfig(locations = "explicitDeclaration")
static class ConventionBasedComposedContextConfigClass {
}
@InvalidConventionBasedComposedContextConfig(locations = "requiredLocationsDeclaration")
static class InvalidConventionBasedComposedContextConfigClass {
}
@AliasedComposedContextConfig(xmlConfigFiles = "test.xml")
static class AliasedComposedContextConfigClass {
}
@AliasedValueComposedContextConfig(locations = "test.xml")
static class AliasedValueComposedContextConfigClass {
}
@InvalidAliasedComposedContextConfig(xmlConfigFiles = "test.xml")
static class InvalidAliasedComposedContextConfigClass {
}
}
@@ -23,11 +23,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
@@ -48,6 +50,9 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
*/
public class AnnotationUtilsTests {
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void findMethodAnnotationOnLeaf() throws Exception {
Method m = Leaf.class.getMethod("annotatedOnLeaf");
@@ -154,7 +159,8 @@ public class AnnotationUtilsTests {
/** @since 4.1.2 */
@Test
public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() {
Component component = AnnotationUtils.findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class);
Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class,
Component.class);
assertNotNull(component);
assertEquals("meta2", component.value());
}
@@ -162,7 +168,7 @@ public class AnnotationUtilsTests {
/** @since 4.0.3 */
@Test
public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() {
Transactional transactional = AnnotationUtils.findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class);
Transactional transactional = findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class);
assertNotNull(transactional);
assertTrue("readOnly flag for SubSubClassWithInheritedAnnotation", transactional.readOnly());
}
@@ -170,21 +176,21 @@ public class AnnotationUtilsTests {
/** @since 4.0.3 */
@Test
public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() {
Component component = AnnotationUtils.findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class);
Component component = findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class);
assertNotNull(component);
assertEquals("meta2", component.value());
}
@Test
public void findClassAnnotationOnMetaMetaAnnotatedClass() {
Component component = AnnotationUtils.findAnnotation(MetaMetaAnnotatedClass.class, Component.class);
Component component = findAnnotation(MetaMetaAnnotatedClass.class, Component.class);
assertNotNull("Should find meta-annotation on composed annotation on class", component);
assertEquals("meta2", component.value());
}
@Test
public void findClassAnnotationOnMetaMetaMetaAnnotatedClass() {
Component component = AnnotationUtils.findAnnotation(MetaMetaMetaAnnotatedClass.class, Component.class);
Component component = findAnnotation(MetaMetaMetaAnnotatedClass.class, Component.class);
assertNotNull("Should find meta-annotation on meta-annotation on composed annotation on class", component);
assertEquals("meta2", component.value());
}
@@ -192,55 +198,55 @@ public class AnnotationUtilsTests {
@Test
public void findClassAnnotationOnAnnotatedClassWithMissingTargetMetaAnnotation() {
// TransactionalClass is NOT annotated or meta-annotated with @Component
Component component = AnnotationUtils.findAnnotation(TransactionalClass.class, Component.class);
Component component = findAnnotation(TransactionalClass.class, Component.class);
assertNull("Should not find @Component on TransactionalClass", component);
}
@Test
public void findClassAnnotationOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() {
Component component = AnnotationUtils.findAnnotation(MetaCycleAnnotatedClass.class, Component.class);
Component component = findAnnotation(MetaCycleAnnotatedClass.class, Component.class);
assertNull("Should not find @Component on MetaCycleAnnotatedClass", component);
}
/** @since 4.2 */
@Test
public void findClassAnnotationOnInheritedAnnotationInterface() {
Transactional tx = AnnotationUtils.findAnnotation(InheritedAnnotationInterface.class, Transactional.class);
Transactional tx = findAnnotation(InheritedAnnotationInterface.class, Transactional.class);
assertNotNull("Should find @Transactional on InheritedAnnotationInterface", tx);
}
/** @since 4.2 */
@Test
public void findClassAnnotationOnSubInheritedAnnotationInterface() {
Transactional tx = AnnotationUtils.findAnnotation(SubInheritedAnnotationInterface.class, Transactional.class);
Transactional tx = findAnnotation(SubInheritedAnnotationInterface.class, Transactional.class);
assertNotNull("Should find @Transactional on SubInheritedAnnotationInterface", tx);
}
/** @since 4.2 */
@Test
public void findClassAnnotationOnSubSubInheritedAnnotationInterface() {
Transactional tx = AnnotationUtils.findAnnotation(SubSubInheritedAnnotationInterface.class, Transactional.class);
Transactional tx = findAnnotation(SubSubInheritedAnnotationInterface.class, Transactional.class);
assertNotNull("Should find @Transactional on SubSubInheritedAnnotationInterface", tx);
}
/** @since 4.2 */
@Test
public void findClassAnnotationOnNonInheritedAnnotationInterface() {
Order order = AnnotationUtils.findAnnotation(NonInheritedAnnotationInterface.class, Order.class);
Order order = findAnnotation(NonInheritedAnnotationInterface.class, Order.class);
assertNotNull("Should find @Order on NonInheritedAnnotationInterface", order);
}
/** @since 4.2 */
@Test
public void findClassAnnotationOnSubNonInheritedAnnotationInterface() {
Order order = AnnotationUtils.findAnnotation(SubNonInheritedAnnotationInterface.class, Order.class);
Order order = findAnnotation(SubNonInheritedAnnotationInterface.class, Order.class);
assertNotNull("Should find @Order on SubNonInheritedAnnotationInterface", order);
}
/** @since 4.2 */
@Test
public void findClassAnnotationOnSubSubNonInheritedAnnotationInterface() {
Order order = AnnotationUtils.findAnnotation(SubSubNonInheritedAnnotationInterface.class, Order.class);
Order order = findAnnotation(SubSubNonInheritedAnnotationInterface.class, Order.class);
assertNotNull("Should find @Order on SubSubNonInheritedAnnotationInterface", order);
}
@@ -375,13 +381,53 @@ public class AnnotationUtilsTests {
assertFalse(isAnnotationInherited(Order.class, SubNonInheritedAnnotationClass.class));
}
@Test
public void getAnnotationAttributesWithoutAttributeAliases() {
Component component = WebController.class.getAnnotation(Component.class);
assertNotNull(component);
AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(component);
assertNotNull(attributes);
assertEquals("value attribute: ", "webController", attributes.getString(VALUE));
assertEquals(Component.class, attributes.annotationType());
}
@Test
public void getAnnotationAttributesWithAttributeAliases() throws Exception {
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
WebMapping webMapping = method.getAnnotation(WebMapping.class);
AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(webMapping);
assertNotNull(attributes);
assertEquals(WebMapping.class, attributes.annotationType());
assertEquals("name attribute: ", "foo", attributes.getString("name"));
assertEquals("value attribute: ", "/test", attributes.getString(VALUE));
assertEquals("path attribute: ", "/test", attributes.getString("path"));
method = WebController.class.getMethod("handleMappedWithPathAttribute");
webMapping = method.getAnnotation(WebMapping.class);
attributes = (AnnotationAttributes) getAnnotationAttributes(webMapping);
assertNotNull(attributes);
assertEquals(WebMapping.class, attributes.annotationType());
assertEquals("name attribute: ", "bar", attributes.getString("name"));
assertEquals("value attribute: ", "/test", attributes.getString(VALUE));
assertEquals("path attribute: ", "/test", attributes.getString("path"));
method = WebController.class.getMethod("handleMappedWithPathValueAndAttributes");
webMapping = method.getAnnotation(WebMapping.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("attribute [value] and its alias [path]"));
exception.expectMessage(containsString("values of [/enigma] and [/test]"));
exception.expectMessage(containsString("but only one declaration is permitted"));
getAnnotationAttributes(webMapping);
}
@Test
public void getValueFromAnnotation() throws Exception {
Method method = SimpleFoo.class.getMethod("something", Object.class);
Order order = findAnnotation(method, Order.class);
assertEquals(1, AnnotationUtils.getValue(order, AnnotationUtils.VALUE));
assertEquals(1, AnnotationUtils.getValue(order));
assertEquals(1, getValue(order, VALUE));
assertEquals(1, getValue(order));
}
@Test
@@ -391,8 +437,8 @@ public class AnnotationUtilsTests {
Annotation annotation = declaredAnnotations[0];
assertNotNull(annotation);
assertEquals("NonPublicAnnotation", annotation.annotationType().getSimpleName());
assertEquals(42, AnnotationUtils.getValue(annotation, AnnotationUtils.VALUE));
assertEquals(42, AnnotationUtils.getValue(annotation));
assertEquals(42, getValue(annotation, VALUE));
assertEquals(42, getValue(annotation));
}
@Test
@@ -400,8 +446,8 @@ public class AnnotationUtilsTests {
Method method = SimpleFoo.class.getMethod("something", Object.class);
Order order = findAnnotation(method, Order.class);
assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(order, AnnotationUtils.VALUE));
assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(order));
assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(order, VALUE));
assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(order));
}
@Test
@@ -411,14 +457,14 @@ public class AnnotationUtilsTests {
Annotation annotation = declaredAnnotations[0];
assertNotNull(annotation);
assertEquals("NonPublicAnnotation", annotation.annotationType().getSimpleName());
assertEquals(-1, AnnotationUtils.getDefaultValue(annotation, AnnotationUtils.VALUE));
assertEquals(-1, AnnotationUtils.getDefaultValue(annotation));
assertEquals(-1, getDefaultValue(annotation, VALUE));
assertEquals(-1, getDefaultValue(annotation));
}
@Test
public void getDefaultValueFromAnnotationType() throws Exception {
assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(Order.class, AnnotationUtils.VALUE));
assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(Order.class));
assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(Order.class, VALUE));
assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(Order.class));
}
@Test
@@ -431,14 +477,174 @@ public class AnnotationUtilsTests {
@Test
public void getRepeatableFromMethod() throws Exception {
Method method = InterfaceWithRepeated.class.getMethod("foo");
Set<MyRepeatable> annotions = AnnotationUtils.getRepeatableAnnotation(method,
MyRepeatableContainer.class, MyRepeatable.class);
Set<String> values = new HashSet<String>();
for (MyRepeatable myRepeatable : annotions) {
values.add(myRepeatable.value());
Set<MyRepeatable> annotations = getRepeatableAnnotation(method, MyRepeatableContainer.class, MyRepeatable.class);
assertNotNull(annotations);
List<String> values = annotations.stream().map(MyRepeatable::value).collect(Collectors.toList());
assertThat(values, equalTo(Arrays.asList("a", "b", "c", "meta")));
}
@Test
public void getRepeatableWithAttributeAliases() throws Exception {
Set<ContextConfig> annotations = getRepeatableAnnotation(TestCase.class, Hierarchy.class, ContextConfig.class);
assertNotNull(annotations);
List<String> locations = annotations.stream().map(ContextConfig::locations).collect(Collectors.toList());
assertThat(locations, equalTo(Arrays.asList("A", "B")));
List<String> values = annotations.stream().map(ContextConfig::value).collect(Collectors.toList());
assertThat(values, equalTo(Arrays.asList("A", "B")));
}
@Test
public void getAliasedAttributeNameFromAliasedComposedAnnotation() throws Exception {
Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile");
assertEquals("locations", getAliasedAttributeName(attribute, ContextConfig.class));
}
@Test
public void synthesizeAnnotationWithoutAttributeAliases() throws Exception {
Component component = findAnnotation(WebController.class, Component.class);
assertNotNull(component);
Component synthesizedComponent = synthesizeAnnotation(component);
assertNotNull(synthesizedComponent);
assertSame(component, synthesizedComponent);
assertEquals("value attribute: ", "webController", synthesizedComponent.value());
}
@Test
public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception {
AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [foo] in"));
exception.expectMessage(containsString(AliasForNonexistentAttribute.class.getName()));
exception.expectMessage(containsString("is declared as an @AliasFor nonexistent attribute [bar]"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliasWithoutMirroredAliasFor() throws Exception {
AliasForWithoutMirroredAliasFor annotation = AliasForWithoutMirroredAliasForClass.class.getAnnotation(AliasForWithoutMirroredAliasFor.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [bar] in"));
exception.expectMessage(containsString(AliasForWithoutMirroredAliasFor.class.getName()));
exception.expectMessage(containsString("must be declared as an @AliasFor [foo]"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliasWithMirroredAliasForWrongAttribute() throws Exception {
AliasForWithMirroredAliasForWrongAttribute annotation = AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation(AliasForWithMirroredAliasForWrongAttribute.class);
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
//
// In other words, we can't be certain which type of exception message we'll get,
// so we allow for both possibilities.
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [bar] in"));
exception.expectMessage(containsString(AliasForWithMirroredAliasForWrongAttribute.class.getName()));
exception.expectMessage(either(containsString("must be declared as an @AliasFor [foo], not [quux]")).
or(containsString("is declared as an @AliasFor nonexistent attribute [quux]")));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliasForAttributeOfDifferentType() throws Exception {
AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation(AliasForAttributeOfDifferentType.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Misconfigured aliases"));
exception.expectMessage(containsString(AliasForAttributeOfDifferentType.class.getName()));
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
//
// In other words, we don't know if "foo" or "bar" will come first.
exception.expectMessage(containsString("attribute [foo]"));
exception.expectMessage(containsString("attribute [bar]"));
exception.expectMessage(containsString("must declare the same return type"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliasForWithMissingDefaultValues() throws Exception {
AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation(AliasForWithMissingDefaultValues.class);
exception.expectMessage(startsWith("Misconfigured aliases"));
exception.expectMessage(containsString(AliasForWithMissingDefaultValues.class.getName()));
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
//
// In other words, we don't know if "foo" or "bar" will come first.
exception.expectMessage(containsString("attribute [foo]"));
exception.expectMessage(containsString("attribute [bar]"));
exception.expectMessage(containsString("must declare default values"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliasForAttributeWithDifferentDefaultValue() throws Exception {
AliasForAttributeWithDifferentDefaultValue annotation = AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation(AliasForAttributeWithDifferentDefaultValue.class);
exception.expectMessage(startsWith("Misconfigured aliases"));
exception.expectMessage(containsString(AliasForAttributeWithDifferentDefaultValue.class.getName()));
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
//
// In other words, we don't know if "foo" or "bar" will come first.
exception.expectMessage(containsString("attribute [foo]"));
exception.expectMessage(containsString("attribute [bar]"));
exception.expectMessage(containsString("must declare the same default value"));
synthesizeAnnotation(annotation);
}
@Test
public void synthesizeAnnotationWithAttributeAliases() throws Exception {
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
WebMapping webMapping = method.getAnnotation(WebMapping.class);
assertNotNull(webMapping);
WebMapping synthesizedWebMapping = synthesizeAnnotation(webMapping);
assertNotSame(webMapping, synthesizedWebMapping);
assertThat(synthesizedWebMapping, instanceOf(SynthesizedAnnotation.class));
assertNotNull(synthesizedWebMapping);
assertEquals("name attribute: ", "foo", synthesizedWebMapping.name());
assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping.path());
assertEquals("actual value attribute: ", "/test", synthesizedWebMapping.value());
}
@Test
public void synthesizeAnnotationWithAttributeAliasesInNestedAnnotations() throws Exception {
Hierarchy hierarchy = TestCase.class.getAnnotation(Hierarchy.class);
assertNotNull(hierarchy);
Hierarchy synthesizedHierarchy = synthesizeAnnotation(hierarchy);
assertNotSame(hierarchy, synthesizedHierarchy);
assertThat(synthesizedHierarchy, instanceOf(SynthesizedAnnotation.class));
ContextConfig[] configs = synthesizedHierarchy.value();
assertNotNull(configs);
for (ContextConfig contextConfig : configs) {
assertThat(contextConfig, instanceOf(SynthesizedAnnotation.class));
}
assertThat(values, equalTo((Set<String>) new HashSet<String>(
Arrays.asList("a", "b", "c", "meta"))));
List<String> locations = Arrays.stream(configs).map(ContextConfig::locations).collect(Collectors.toList());
assertThat(locations, equalTo(Arrays.asList("A", "B")));
List<String> values = Arrays.stream(configs).map(ContextConfig::value).collect(Collectors.toList());
assertThat(values, equalTo(Arrays.asList("A", "B")));
}
@Test
public void synthesizeAlreadySynthesizedAnnotation() throws Exception {
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
WebMapping webMapping = method.getAnnotation(WebMapping.class);
assertNotNull(webMapping);
WebMapping synthesizedWebMapping = synthesizeAnnotation(webMapping);
assertNotSame(webMapping, synthesizedWebMapping);
WebMapping synthesizedAgainWebMapping = synthesizeAnnotation(synthesizedWebMapping);
assertSame(synthesizedWebMapping, synthesizedAgainWebMapping);
assertThat(synthesizedAgainWebMapping, instanceOf(SynthesizedAnnotation.class));
assertNotNull(synthesizedAgainWebMapping);
assertEquals("name attribute: ", "foo", synthesizedAgainWebMapping.name());
assertEquals("aliased path attribute: ", "/test", synthesizedAgainWebMapping.path());
assertEquals("actual value attribute: ", "/test", synthesizedAgainWebMapping.value());
}
@@ -710,4 +916,149 @@ public class AnnotationUtilsTests {
void foo();
}
/**
* Mock of {@link org.springframework.web.bind.annotation.RequestMapping}.
*/
@Retention(RetentionPolicy.RUNTIME)
@interface WebMapping {
String name();
@AliasFor(attribute = "path")
String value() default "";
@AliasFor(attribute = "value")
String path() default "";
}
@Component("webController")
static class WebController {
@WebMapping(value = "/test", name = "foo")
public void handleMappedWithValueAttribute() {
}
@WebMapping(path = "/test", name = "bar")
public void handleMappedWithPathAttribute() {
}
@WebMapping(value = "/enigma", path = "/test", name = "baz")
public void handleMappedWithPathValueAndAttributes() {
}
}
/**
* Mock of {@link org.springframework.test.context.ContextConfiguration}.
*/
@Retention(RetentionPolicy.RUNTIME)
static @interface ContextConfig {
@AliasFor(attribute = "locations")
String value() default "";
@AliasFor(attribute = "value")
String locations() default "";
}
/**
* Mock of {@link org.springframework.test.context.ContextHierarchy}.
*/
@Retention(RetentionPolicy.RUNTIME)
static @interface Hierarchy {
ContextConfig[] value();
}
@Hierarchy({ @ContextConfig("A"), @ContextConfig(locations = "B") })
static class TestCase {
}
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasForNonexistentAttribute {
@AliasFor(attribute = "bar")
String foo() default "";
}
@AliasForNonexistentAttribute
static class AliasForNonexistentAttributeClass {
}
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasForWithoutMirroredAliasFor {
@AliasFor(attribute = "bar")
String foo() default "";
String bar() default "";
}
@AliasForWithoutMirroredAliasFor
static class AliasForWithoutMirroredAliasForClass {
}
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasForWithMirroredAliasForWrongAttribute {
@AliasFor(attribute = "bar")
String[] foo() default "";
@AliasFor(attribute = "quux")
String[] bar() default "";
}
@AliasForWithMirroredAliasForWrongAttribute
static class AliasForWithMirroredAliasForWrongAttributeClass {
}
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasForAttributeOfDifferentType {
@AliasFor(attribute = "bar")
String[] foo() default "";
@AliasFor(attribute = "foo")
boolean bar() default true;
}
@AliasForAttributeOfDifferentType
static class AliasForAttributeOfDifferentTypeClass {
}
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasForWithMissingDefaultValues {
@AliasFor(attribute = "bar")
String foo();
@AliasFor(attribute = "foo")
String bar();
}
@AliasForWithMissingDefaultValues(foo = "foo", bar = "bar")
static class AliasForWithMissingDefaultValuesClass {
}
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasForAttributeWithDifferentDefaultValue {
@AliasFor(attribute = "bar")
String foo() default "X";
@AliasFor(attribute = "foo")
String bar() default "Z";
}
@AliasForAttributeWithDifferentDefaultValue
static class AliasForAttributeWithDifferentDefaultValueClass {
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
static @interface AliasedComposedContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String xmlConfigFile();
}
}