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

@@ -19,7 +19,6 @@ package org.springframework.test.annotation;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
/**
@@ -38,13 +37,12 @@ public class TestAnnotationUtils {
* annotated with {@code @Timed}
*/
public static long getTimeout(Method method) {
AnnotationAttributes attributes = AnnotatedElementUtils.findAnnotationAttributes(method, Timed.class.getName());
if (attributes == null) {
Timed timed = AnnotatedElementUtils.findAnnotation(method, Timed.class);
if (timed == null) {
return 0;
}
else {
long millis = attributes.<Long> getNumber("millis").longValue();
return Math.max(0, millis);
return Math.max(0, timed.millis());
}
}

View File

@@ -87,7 +87,7 @@ class MergedSqlConfig {
// Get global attributes, if any.
AnnotationAttributes attributes = AnnotatedElementUtils.findAnnotationAttributes(testClass,
SqlConfig.class.getName());
SqlConfig.class.getName(), false, false);
// Override global attributes with local attributes.
if (attributes != null) {

View File

@@ -32,7 +32,6 @@ import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
@@ -115,7 +114,6 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public final List<TestExecutionListener> getTestExecutionListeners() {
Class<?> clazz = getBootstrapContext().getTestClass();
@@ -139,23 +137,20 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
// Traverse the class hierarchy...
while (descriptor != null) {
Class<?> declaringClass = descriptor.getDeclaringClass();
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
TestExecutionListeners testExecutionListeners = descriptor.getMergedAnnotation();
if (logger.isTraceEnabled()) {
logger.trace(String.format(
"Retrieved @TestExecutionListeners attributes [%s] for declaring class [%s].", annAttrs,
declaringClass.getName()));
logger.trace(String.format("Retrieved @TestExecutionListeners [%s] for declaring class [%s].",
testExecutionListeners, declaringClass.getName()));
}
Class<? extends TestExecutionListener>[] listenerClasses = (Class<? extends TestExecutionListener>[]) annAttrs.getClassArray("listeners");
boolean inheritListeners = annAttrs.getBoolean("inheritListeners");
boolean inheritListeners = testExecutionListeners.inheritListeners();
AnnotationDescriptor<TestExecutionListeners> superDescriptor = MetaAnnotationUtils.findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
// If there are no listeners to inherit, we might need to merge the
// locally declared listeners with the defaults.
if ((!inheritListeners || superDescriptor == null)
&& (annAttrs.getEnum("mergeMode") == MergeMode.MERGE_WITH_DEFAULTS)) {
&& (testExecutionListeners.mergeMode() == MergeMode.MERGE_WITH_DEFAULTS)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Merging default listeners with listeners configured via @TestExecutionListeners for class [%s].",
@@ -165,7 +160,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
classesList.addAll(getDefaultTestExecutionListenerClasses());
}
classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses));
classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
descriptor = (inheritListeners ? superDescriptor : null);
}

View File

@@ -23,7 +23,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.util.MetaAnnotationUtils;
@@ -87,14 +86,14 @@ abstract class ActiveProfilesUtils {
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass();
Class<?> declaringClass = descriptor.getDeclaringClass();
ActiveProfiles annotation = descriptor.getMergedAnnotation();
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].",
annAttrs, declaringClass.getName()));
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
declaringClass.getName()));
}
Class<? extends ActiveProfilesResolver> resolverClass = annAttrs.getClass("resolver");
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
if (ActiveProfilesResolver.class == resolverClass) {
resolverClass = DefaultActiveProfilesResolver.class;
}
@@ -125,8 +124,8 @@ abstract class ActiveProfilesUtils {
}
}
descriptor = annAttrs.getBoolean("inheritProfiles") ? MetaAnnotationUtils.findAnnotationDescriptor(
rootDeclaringClass.getSuperclass(), annotationType) : null;
descriptor = (annotation.inheritProfiles() ? MetaAnnotationUtils.findAnnotationDescriptor(
rootDeclaringClass.getSuperclass(), annotationType) : null);
}
return StringUtils.toStringArray(activeProfiles);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,6 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextHierarchy;
@@ -133,8 +132,9 @@ abstract class ContextLoaderUtils {
final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>();
if (contextConfigDeclaredLocally) {
convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(),
rootDeclaringClass, configAttributesList);
convertContextConfigToConfigAttributesAndAddToList(
(ContextConfiguration) descriptor.getMergedAnnotation(), rootDeclaringClass,
configAttributesList);
}
else if (contextHierarchyDeclaredLocally) {
ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType);
@@ -256,7 +256,7 @@ abstract class ContextLoaderUtils {
annotationType.getName(), testClass.getName()));
while (descriptor != null) {
convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(),
convertContextConfigToConfigAttributesAndAddToList(descriptor.getMergedAnnotation(),
descriptor.getRootDeclaringClass(), attributesList);
descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
}
@@ -284,24 +284,4 @@ abstract class ContextLoaderUtils {
attributesList.add(attributes);
}
/**
* Convenience method for creating a {@link ContextConfigurationAttributes}
* instance from the supplied {@link AnnotationAttributes} and declaring
* class and then adding the attributes to the supplied list.
* @since 4.0
*/
private static void convertAnnotationAttributesToConfigAttributesAndAddToList(AnnotationAttributes annAttrs,
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ContextConfiguration attributes [%s] for declaring class [%s].",
annAttrs, declaringClass.getName()));
}
ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, annAttrs);
if (logger.isTraceEnabled()) {
logger.trace("Resolved context configuration attributes: " + attributes);
}
attributesList.add(attributes);
}
}

View File

@@ -22,9 +22,9 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -73,14 +73,14 @@ public class DefaultActiveProfilesResolver implements ActiveProfilesResolver {
}
else {
Class<?> declaringClass = descriptor.getDeclaringClass();
ActiveProfiles annotation = descriptor.getMergedAnnotation();
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].",
annAttrs, declaringClass.getName()));
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
declaringClass.getName()));
}
for (String profile : annAttrs.getStringArray("profiles")) {
for (String profile : annotation.profiles()) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}

View File

@@ -23,7 +23,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
@@ -150,28 +149,27 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
*/
private void beforeOrAfterTestMethod(TestContext testContext, String phase, MethodMode requiredMethodMode,
ClassMode requiredClassMode) throws Exception {
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
Method testMethod = testContext.getTestMethod();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes methodAnnAttrs = AnnotatedElementUtils.findAnnotationAttributes(testMethod, annotationType);
AnnotationAttributes classAnnAttrs = AnnotatedElementUtils.findAnnotationAttributes(testClass, annotationType);
boolean methodAnnotated = methodAnnAttrs != null;
boolean classAnnotated = classAnnAttrs != null;
MethodMode methodMode = methodAnnotated ? methodAnnAttrs.<MethodMode> getEnum("methodMode") : null;
ClassMode classMode = classAnnotated ? classAnnAttrs.<ClassMode> getEnum("classMode") : null;
DirtiesContext methodAnn = AnnotatedElementUtils.findAnnotation(testMethod, DirtiesContext.class);
DirtiesContext classAnn = AnnotatedElementUtils.findAnnotation(testClass, DirtiesContext.class);
boolean methodAnnotated = (methodAnn != null);
boolean classAnnotated = (classAnn != null);
MethodMode methodMode = (methodAnnotated ? methodAnn.methodMode() : null);
ClassMode classMode = (classAnnotated ? classAnn.classMode() : null);
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"%s test method: context %s, class annotated with @DirtiesContext [%s] with mode [%s], method annotated with @DirtiesContext [%s] with mode [%s].",
phase, testContext, classAnnotated, classMode, methodAnnotated, methodMode));
logger.debug(String.format("%s test method: context %s, class annotated with @DirtiesContext [%s] "
+ "with mode [%s], method annotated with @DirtiesContext [%s] with mode [%s].", phase, testContext,
classAnnotated, classMode, methodAnnotated, methodMode));
}
if ((methodMode == requiredMethodMode) || (classMode == requiredClassMode)) {
HierarchyMode hierarchyMode = methodAnnotated ? methodAnnAttrs.<HierarchyMode> getEnum("hierarchyMode")
: classAnnAttrs.<HierarchyMode> getEnum("hierarchyMode");
HierarchyMode hierarchyMode = (methodAnnotated ? methodAnn.hierarchyMode() : classAnn.hierarchyMode());
dirtyContext(testContext, hierarchyMode);
}
}
@@ -185,10 +183,9 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes classAnnAttrs = AnnotatedElementUtils.findAnnotationAttributes(testClass, annotationType);
boolean classAnnotated = classAnnAttrs != null;
ClassMode classMode = classAnnotated ? classAnnAttrs.<ClassMode> getEnum("classMode") : null;
DirtiesContext dirtiesContext = AnnotatedElementUtils.findAnnotation(testClass, DirtiesContext.class);
boolean classAnnotated = (dirtiesContext != null);
ClassMode classMode = (classAnnotated ? dirtiesContext.classMode() : null);
if (logger.isDebugEnabled()) {
logger.debug(String.format(
@@ -197,8 +194,7 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
}
if (classMode == requiredClassMode) {
HierarchyMode hierarchyMode = classAnnAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
dirtyContext(testContext, dirtiesContext.hierarchyMode());
}
}

View File

@@ -19,7 +19,6 @@ package org.springframework.test.context.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.style.ToStringCreator;
import org.springframework.test.context.TestPropertySource;
@@ -58,17 +57,17 @@ class TestPropertySourceAttributes {
/**
* Create a new {@code TestPropertySourceAttributes} instance for the
* supplied {@link AnnotationAttributes} (parsed from a
* {@link TestPropertySource @TestPropertySource} annotation) and
* the {@linkplain Class test class} that declared them, enforcing
* supplied {@link TestPropertySource @TestPropertySource} annotation and
* the {@linkplain Class test class} that declared it, enforcing
* configuration rules and detecting a default properties file if
* necessary.
* @param declaringClass the class that declared {@code @TestPropertySource}
* @param annAttrs the annotation attributes from which to retrieve the attributes
* @param testPropertySource the annotation from which to retrieve the attributes
* @since 4.2
*/
TestPropertySourceAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) {
this(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getBoolean("inheritLocations"),
annAttrs.getStringArray("properties"), annAttrs.getBoolean("inheritProperties"));
TestPropertySourceAttributes(Class<?> declaringClass, TestPropertySource testPropertySource) {
this(declaringClass, testPropertySource.locations(), testPropertySource.inheritLocations(),
testPropertySource.properties(), testPropertySource.inheritProperties());
}
private TestPropertySourceAttributes(Class<?> declaringClass, String[] locations, boolean inheritLocations,

View File

@@ -29,7 +29,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
@@ -39,6 +38,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.util.TestContextResourceUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -96,15 +96,16 @@ public abstract class TestPropertySourceUtils {
annotationType.getName(), testClass.getName()));
while (descriptor != null) {
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
TestPropertySource testPropertySource = descriptor.getMergedAnnotation();
Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @TestPropertySource attributes [%s] for declaring class [%s].",
annAttrs, rootDeclaringClass.getName()));
logger.trace(String.format("Retrieved @TestPropertySource [%s] for declaring class [%s].",
testPropertySource, rootDeclaringClass.getName()));
}
TestPropertySourceAttributes attributes = new TestPropertySourceAttributes(rootDeclaringClass, annAttrs);
TestPropertySourceAttributes attributes = new TestPropertySourceAttributes(rootDeclaringClass,
testPropertySource);
if (logger.isTraceEnabled()) {
logger.trace("Resolved TestPropertySource attributes: " + attributes);
}

View File

@@ -30,7 +30,6 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
@@ -500,18 +499,18 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
if (this.configurationAttributes == null) {
Class<?> clazz = testContext.getTestClass();
AnnotationAttributes annAttrs = AnnotatedElementUtils.findAnnotationAttributes(clazz,
TransactionConfiguration.class.getName());
TransactionConfiguration txConfig = AnnotatedElementUtils.findAnnotation(clazz,
TransactionConfiguration.class);
if (logger.isDebugEnabled()) {
logger.debug(String.format("Retrieved @TransactionConfiguration attributes [%s] for test class [%s].",
annAttrs, clazz));
logger.debug(String.format("Retrieved @TransactionConfiguration [%s] for test class [%s].",
txConfig, clazz));
}
String transactionManagerName;
boolean defaultRollback;
if (annAttrs != null) {
transactionManagerName = annAttrs.getString("transactionManager");
defaultRollback = annAttrs.getBoolean("defaultRollback");
if (txConfig != null) {
transactionManagerName = txConfig.transactionManager();
defaultRollback = txConfig.defaultRollback();
}
else {
transactionManagerName = DEFAULT_TRANSACTION_MANAGER_NAME;

View File

@@ -272,12 +272,16 @@ public abstract class MetaAnnotationUtils {
private final T annotation;
private final T mergedAnnotation;
private final AnnotationAttributes annotationAttributes;
public AnnotationDescriptor(Class<?> rootDeclaringClass, T annotation) {
this(rootDeclaringClass, rootDeclaringClass, null, annotation);
}
@SuppressWarnings("unchecked")
public AnnotationDescriptor(Class<?> rootDeclaringClass, Class<?> declaringClass,
Annotation composedAnnotation, T annotation) {
Assert.notNull(rootDeclaringClass, "rootDeclaringClass must not be null");
@@ -286,8 +290,10 @@ public abstract class MetaAnnotationUtils {
this.declaringClass = declaringClass;
this.composedAnnotation = composedAnnotation;
this.annotation = annotation;
this.annotationAttributes = AnnotatedElementUtils.findAnnotationAttributes(
rootDeclaringClass, annotation.annotationType());
this.annotationAttributes = AnnotatedElementUtils.findAnnotationAttributes(rootDeclaringClass,
annotation.annotationType().getName(), false, false);
this.mergedAnnotation = AnnotationUtils.synthesizeAnnotation(annotationAttributes,
(Class<T>) annotation.annotationType(), rootDeclaringClass);
}
public Class<?> getRootDeclaringClass() {
@@ -302,6 +308,16 @@ public abstract class MetaAnnotationUtils {
return this.annotation;
}
/**
* Get the annotation that was synthesized from the merged
* {@link #getAnnotationAttributes AnnotationAttributes}.
* @see #getAnnotationAttributes()
* @see AnnotationUtils#synthesizeAnnotation(java.util.Map, Class, java.lang.reflect.AnnotatedElement)
*/
public T getMergedAnnotation() {
return this.mergedAnnotation;
}
public Class<? extends Annotation> getAnnotationType() {
return this.annotation.annotationType();
}