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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user