Synthesize annotation from a map of attributes

Spring Framework 4.2 RC1 introduced support for synthesizing an
annotation from an existing annotation in order to provide additional
functionality above and beyond that provided by Java. Specifically,
such synthesized annotations provide support for @AliasFor semantics.
As luck would have it, the same principle can be used to synthesize an
annotation from any map of attributes, and in particular, from an
instance of AnnotationAttributes.

The following highlight the major changes in this commit toward
achieving this goal.

- Introduced AnnotationAttributeExtractor abstraction and refactored
  SynthesizedAnnotationInvocationHandler to delegate to an
  AnnotationAttributeExtractor.

- Extracted code from SynthesizedAnnotationInvocationHandler into new
  AbstractAliasAwareAnnotationAttributeExtractor and
  DefaultAnnotationAttributeExtractor implementation classes.

- Introduced MapAnnotationAttributeExtractor for synthesizing an
  annotation that is backed by a map or AnnotationAttributes instance.

- Introduced a variant of synthesizeAnnotation() in AnnotationUtils
  that accepts a map.

- Introduced findAnnotation(*) methods in AnnotatedElementUtils that
  synthesize merged AnnotationAttributes back into an annotation of the
  target type.

The following classes have been refactored to use the new support for
synthesizing AnnotationAttributes back into an annotation.

- ApplicationListenerMethodAdapter
- TestAnnotationUtils
- AbstractTestContextBootstrapper
- ActiveProfilesUtils
- ContextLoaderUtils
- DefaultActiveProfilesResolver
- DirtiesContextTestExecutionListener
- TestPropertySourceAttributes
- TestPropertySourceUtils
- TransactionalTestExecutionListener
- MetaAnnotationUtils
- MvcUriComponentsBuilder
- RequestMappingHandlerMapping

In addition, this commit also includes changes to ensure that arrays
returned by synthesized annotations are properly cloned first.

Issue: SPR-13067
This commit is contained in:
Sam Brannen
2015-05-25 16:58:18 +02:00
parent f41de12cf6
commit e30c9b2ef3
25 changed files with 881 additions and 358 deletions

View File

@@ -16,11 +16,13 @@
package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
@@ -30,6 +32,7 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import static java.util.Arrays.*;
@@ -450,14 +453,33 @@ public class AnnotatedElementUtilsTests {
}
@Test
public void findAnnotationAttributesOnClassWithAttributeAliasesInTargetAnnotation() {
public void findAndSynthesizeAnnotationAttributesOnClassWithAttributeAliasesInTargetAnnotation() {
String qualifier = "aliasForQualifier";
// 1) Find and merge AnnotationAttributes from the annotation hierarchy
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"));
assertNotNull("@AliasedTransactional on AliasedTransactionalComponentClass.", attributes);
// 2) Synthesize the AnnotationAttributes back into the target annotation
AliasedTransactional annotation = AnnotationUtils.synthesizeAnnotation(attributes,
AliasedTransactional.class, AliasedTransactionalComponentClass.class);
assertNotNull(annotation);
// 3) Verify that the AnnotationAttributes and synthesized annotation are equivalent
assertEquals("TX value via attributes.", qualifier, attributes.getString("value"));
assertEquals("TX value via synthesized annotation.", qualifier, annotation.value());
assertEquals("TX qualifier via attributes.", qualifier, attributes.getString("qualifier"));
assertEquals("TX qualifier via synthesized annotation.", qualifier, annotation.qualifier());
}
@Test
public void findAnnotationWithAttributeAliasesInTargetAnnotation() {
Class<?> element = AliasedTransactionalComponentClass.class;
AliasedTransactional annotation = findAnnotation(element, AliasedTransactional.class);
assertNotNull("@AliasedTransactional on " + element, annotation);
assertEquals("TX value via synthesized annotation.", "aliasForQualifier", annotation.value());
assertEquals("TX qualifier via synthesized annotation.", "aliasForQualifier", annotation.qualifier());
}
@Test
@@ -480,6 +502,12 @@ public class AnnotatedElementUtilsTests {
}
static AnnotationAttributes findAnnotationAttributes(AnnotatedElement element, Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "annotationType must not be null");
return AnnotatedElementUtils.findAnnotationAttributes(element, annotationType.getName(), false, false);
}
// -------------------------------------------------------------------------
@MetaCycle3

View File

@@ -24,7 +24,9 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Rule;
@@ -436,7 +438,7 @@ public class AnnotationUtilsTests {
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"));
exception.expectMessage(containsString("but only one is permitted"));
getAnnotationAttributes(webMapping);
}
@@ -504,7 +506,8 @@ public class AnnotationUtilsTests {
@Test
public void getRepeatableWithAttributeAliases() throws Exception {
Set<ContextConfig> annotations = getRepeatableAnnotation(TestCase.class, Hierarchy.class, ContextConfig.class);
Set<ContextConfig> annotations = getRepeatableAnnotation(ConfigHierarchyTestCase.class, Hierarchy.class,
ContextConfig.class);
assertNotNull(annotations);
List<String> locations = annotations.stream().map(ContextConfig::locations).collect(toList());
@@ -522,7 +525,7 @@ public class AnnotationUtilsTests {
@Test
public void synthesizeAnnotationWithoutAttributeAliases() throws Exception {
Component component = findAnnotation(WebController.class, Component.class);
Component component = WebController.class.getAnnotation(Component.class);
assertNotNull(component);
Component synthesizedComponent = synthesizeAnnotation(component);
assertNotNull(synthesizedComponent);
@@ -530,6 +533,29 @@ public class AnnotationUtilsTests {
assertEquals("value attribute: ", "webController", synthesizedComponent.value());
}
@Test
public void synthesizeAnnotationsFromNullSources() throws Exception {
assertNull("null annotation", synthesizeAnnotation(null, null));
assertNull("null map", synthesizeAnnotation(null, WebMapping.class, null));
}
@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());
}
@Test
public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception {
AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class);
@@ -638,6 +664,78 @@ public class AnnotationUtilsTests {
assertEquals("actual value attribute: ", "/test", synthesizedWebMapping2.value());
}
@Test
public void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exception {
Component component = WebController.class.getAnnotation(Component.class);
assertNotNull(component);
Map<String, Object> map = new HashMap<String, Object>();
map.put(VALUE, "webController");
Component synthesizedComponent = synthesizeAnnotation(map, Component.class, WebController.class);
assertNotNull(synthesizedComponent);
assertNotSame(component, synthesizedComponent);
assertEquals("value from component: ", "webController", component.value());
assertEquals("value from synthesized component: ", "webController", synthesizedComponent.value());
}
@Test
public void synthesizeAnnotationFromMapWithMissingAttributeValue() throws Exception {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("Attributes map"));
exception.expectMessage(containsString("returned null for required attribute [value]"));
exception.expectMessage(containsString("defined by annotation type [" + Component.class.getName() + "]"));
synthesizeAnnotation(new HashMap<String, Object>(), Component.class, null);
}
@Test
public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
map.put(VALUE, null);
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("Attributes map"));
exception.expectMessage(containsString("returned null for required attribute [value]"));
exception.expectMessage(containsString("defined by annotation type [" + Component.class.getName() + "]"));
synthesizeAnnotation(map, Component.class, null);
}
@Test
public void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
map.put(VALUE, 42L);
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("Attributes map"));
exception.expectMessage(containsString("returned a value of type [java.lang.Long]"));
exception.expectMessage(containsString("for attribute [value]"));
exception.expectMessage(containsString("but a value of type [java.lang.String] is required"));
exception.expectMessage(containsString("as defined by annotation type [" + Component.class.getName() + "]"));
synthesizeAnnotation(map, Component.class, null);
}
@Test
public void synthesizeAnnotationFromAnnotationAttributesWithoutAttributeAliases() throws Exception {
// 1) Get an annotation
Component component = WebController.class.getAnnotation(Component.class);
assertNotNull(component);
// 2) Convert the annotation into AnnotationAttributes
AnnotationAttributes attributes = getAnnotationAttributes(WebController.class, component);
assertNotNull(attributes);
// 3) Synthesize the AnnotationAttributes back into an annotation
Component synthesizedComponent = synthesizeAnnotation(attributes, Component.class, WebController.class);
assertNotNull(synthesizedComponent);
// 4) Verify that the original and synthesized annotations are equivalent
assertNotSame(component, synthesizedComponent);
assertEquals(component, synthesizedComponent);
assertEquals("value from component: ", "webController", component.value());
assertEquals("value from synthesized component: ", "webController", synthesizedComponent.value());
}
@Test
public void toStringForSynthesizedAnnotations() throws Exception {
Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute");
@@ -670,7 +768,7 @@ public class AnnotationUtilsTests {
assertThat(string, containsString("path=/test"));
assertThat(string, containsString("name=bar"));
assertThat(string, containsString("method="));
assertThat(string, either(containsString("[GET, POST]")).or(containsString("[POST, GET]")));
assertThat(string, containsString("[GET, POST]"));
assertThat(string, endsWith(")"));
}
@@ -778,7 +876,7 @@ public class AnnotationUtilsTests {
@Test
public void synthesizeAnnotationWithAttributeAliasesInNestedAnnotations() throws Exception {
Hierarchy hierarchy = TestCase.class.getAnnotation(Hierarchy.class);
Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class);
assertNotNull(hierarchy);
Hierarchy synthesizedHierarchy = synthesizeAnnotation(hierarchy);
assertNotSame(hierarchy, synthesizedHierarchy);
@@ -797,20 +895,44 @@ public class AnnotationUtilsTests {
}
@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));
public void synthesizeAnnotationWithArrayOfAnnotations() throws Exception {
Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class);
assertNotNull(hierarchy);
Hierarchy synthesizedHierarchy = synthesizeAnnotation(hierarchy);
assertThat(synthesizedHierarchy, 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());
ContextConfig contextConfig = SimpleConfigTestCase.class.getAnnotation(ContextConfig.class);
assertNotNull(contextConfig);
ContextConfig[] configs = synthesizedHierarchy.value();
List<String> locations = Arrays.stream(configs).map(ContextConfig::locations).collect(toList());
assertThat(locations, equalTo(Arrays.asList("A", "B")));
// Alter array returned from synthesized annotation
configs[0] = contextConfig;
// Re-retrieve the array from the synthesized annotation
configs = synthesizedHierarchy.value();
List<String> values = Arrays.stream(configs).map(ContextConfig::value).collect(toList());
assertThat(values, equalTo(Arrays.asList("A", "B")));
}
@Test
public void synthesizeAnnotationWithArrayOfChars() throws Exception {
CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation(CharsContainer.class);
assertNotNull(charsContainer);
CharsContainer synthesizedCharsContainer = synthesizeAnnotation(charsContainer);
assertThat(synthesizedCharsContainer, instanceOf(SynthesizedAnnotation.class));
char[] chars = synthesizedCharsContainer.chars();
assertArrayEquals(new char[] { 'x', 'y', 'z' }, chars);
// Alter array returned from synthesized annotation
chars[0] = '?';
// Re-retrieve the array from the synthesized annotation
chars = synthesizedCharsContainer.chars();
assertArrayEquals(new char[] { 'x', 'y', 'z' }, chars);
}
@@ -1149,9 +1271,28 @@ public class AnnotationUtilsTests {
}
@Hierarchy({ @ContextConfig("A"), @ContextConfig(locations = "B") })
static class TestCase {
static class ConfigHierarchyTestCase {
}
@ContextConfig("simple.xml")
static class SimpleConfigTestCase {
}
@Retention(RetentionPolicy.RUNTIME)
@interface CharsContainer {
@AliasFor(attribute = "chars")
char[] value() default {};
@AliasFor(attribute = "value")
char[] chars() default {};
}
@CharsContainer(chars = { 'x', 'y', 'z' })
static class GroupOfCharsClass {
}
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForNonexistentAttribute {