From e30c9b2ef3e9be126090cc8afbd76607f3cd70c7 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 25 May 2015 16:58:18 +0200 Subject: [PATCH] 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 --- .../ApplicationListenerMethodAdapter.java | 9 +- ...liasAwareAnnotationAttributeExtractor.java | 135 +++++++++++ .../annotation/AnnotatedElementUtils.java | 71 +++--- .../AnnotationAttributeExtractor.java | 62 +++++ .../core/annotation/AnnotationUtils.java | 79 ++++++- .../DefaultAnnotationAttributeExtractor.java | 65 ++++++ .../MapAnnotationAttributeExtractor.java | 100 ++++++++ ...ynthesizedAnnotationInvocationHandler.java | 215 ++++++++---------- .../AnnotatedElementUtilsTests.java | 40 +++- .../core/annotation/AnnotationUtilsTests.java | 179 +++++++++++++-- .../test/annotation/TestAnnotationUtils.java | 8 +- .../test/context/jdbc/MergedSqlConfig.java | 2 +- .../AbstractTestContextBootstrapper.java | 17 +- .../context/support/ActiveProfilesUtils.java | 13 +- .../context/support/ContextLoaderUtils.java | 30 +-- .../DefaultActiveProfilesResolver.java | 10 +- .../DirtiesContextTestExecutionListener.java | 36 ++- .../support/TestPropertySourceAttributes.java | 15 +- .../support/TestPropertySourceUtils.java | 11 +- .../TransactionalTestExecutionListener.java | 15 +- .../test/util/MetaAnnotationUtils.java | 20 +- .../SqlScriptsTestExecutionListenerTests.java | 2 +- .../mvc/method/RequestMappingInfo.java | 2 +- .../annotation/MvcUriComponentsBuilder.java | 19 +- .../RequestMappingHandlerMapping.java | 84 ++----- 25 files changed, 881 insertions(+), 358 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index f63511bfe8e..ef109691aeb 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -32,7 +32,6 @@ import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.expression.EvaluationContext; @@ -248,11 +247,9 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe */ protected String getCondition() { if (this.condition == null) { - AnnotationAttributes annotationAttributes = AnnotatedElementUtils - .findAnnotationAttributes(this.method, EventListener.class); - if (annotationAttributes != null) { - String value = annotationAttributes.getString("condition"); - this.condition = (value != null ? value : ""); + EventListener eventListener = AnnotatedElementUtils.findAnnotation(this.method, EventListener.class); + if (eventListener != null) { + this.condition = eventListener.condition(); } } return this.condition; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java new file mode 100644 index 00000000000..e9f68bc2a20 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java @@ -0,0 +1,135 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Abstract base class for {@link AnnotationAttributeExtractor} implementations + * that transparently enforce attribute alias semantics for annotation + * attributes that are annotated with {@link AliasFor @AliasFor}. + * + * @author Sam Brannen + * @since 4.2 + * @see Annotation + * @see AliasFor + * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) + */ +abstract class AbstractAliasAwareAnnotationAttributeExtractor implements AnnotationAttributeExtractor { + + private final Class annotationType; + + private final AnnotatedElement annotatedElement; + + private final Object source; + + private final Map attributeAliasMap; + + + /** + * Construct a new {@code AbstractAliasAwareAnnotationAttributeExtractor}. + * @param annotationType the annotation type to synthesize; never {@code null} + * @param annotatedElement the element that is annotated with the annotation + * of the supplied type; may be {@code null} if unknown + * @param source the underlying source of annotation attributes; never {@code null} + */ + AbstractAliasAwareAnnotationAttributeExtractor(Class annotationType, + AnnotatedElement annotatedElement, Object source) { + Assert.notNull(annotationType, "annotationType must not be null"); + Assert.notNull(source, "source must not be null"); + this.annotationType = annotationType; + this.annotatedElement = annotatedElement; + this.source = source; + this.attributeAliasMap = AnnotationUtils.getAttributeAliasMap(annotationType); + } + + @Override + public final Class getAnnotationType() { + return this.annotationType; + } + + @Override + public final AnnotatedElement getAnnotatedElement() { + return this.annotatedElement; + } + + @Override + public Object getSource() { + return this.source; + } + + @Override + public final Object getAttributeValue(Method attributeMethod) { + String attributeName = attributeMethod.getName(); + Object attributeValue = getRawAttributeValue(attributeMethod); + + String aliasName = this.attributeAliasMap.get(attributeName); + if ((aliasName != null)) { + + Object aliasValue = getRawAttributeValue(aliasName); + Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName); + + if (!nullSafeEquals(attributeValue, aliasValue) && !nullSafeEquals(attributeValue, defaultValue) + && !nullSafeEquals(aliasValue, defaultValue)) { + String elementName = (getAnnotatedElement() == null ? "unknown element" + : getAnnotatedElement().toString()); + String msg = String.format("In annotation [%s] declared on [%s] and synthesized from [%s], " + + "attribute [%s] and its alias [%s] are present with values of [%s] and [%s], " + + "but only one is permitted.", getAnnotationType().getName(), elementName, getSource(), + attributeName, aliasName, nullSafeToString(attributeValue), nullSafeToString(aliasValue)); + throw new AnnotationConfigurationException(msg); + } + + // If the user didn't declare the annotation with an explicit value, + // return the value of the alias. + if (nullSafeEquals(attributeValue, defaultValue)) { + attributeValue = aliasValue; + } + } + + return attributeValue; + } + + /** + * Get the raw, unmodified attribute value from the underlying + * {@linkplain #getSource source} that corresponds to the supplied + * attribute method. + */ + protected abstract Object getRawAttributeValue(Method attributeMethod); + + /** + * Get the raw, unmodified attribute value from the underlying + * {@linkplain #getSource source} that corresponds to the supplied + * attribute name. + */ + protected abstract Object getRawAttributeValue(String attributeName); + + private static boolean nullSafeEquals(Object o1, Object o2) { + return ObjectUtils.nullSafeEquals(o1, o2); + } + + private static String nullSafeToString(Object obj) { + return ObjectUtils.nullSafeToString(obj); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 60ebf16335f..3428966d364 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -48,8 +48,8 @@ import org.springframework.util.StringUtils; *

Annotation Attribute Overrides

*

Support for meta-annotations with attribute overrides in * composed annotations is provided by all variants of the - * {@code getAnnotationAttributes()} and {@code findAnnotationAttributes()} - * methods. + * {@code getAnnotationAttributes()}, {@code findAnnotation()}, and + * {@code findAnnotationAttributes()} methods. * *

Find vs. Get Semantics

*

The search algorithms used by methods in this class follow either @@ -224,6 +224,9 @@ public class AnnotatedElementUtils { * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. * + *

{@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + * *

This method delegates to {@link #getAnnotationAttributes(AnnotatedElement, String, boolean, boolean)}, * supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}. * @@ -233,8 +236,8 @@ public class AnnotatedElementUtils { * @return the merged {@code AnnotationAttributes}, or {@code null} if * not found * @see #getAnnotationAttributes(AnnotatedElement, String, boolean, boolean) - * @see #findAnnotationAttributes(AnnotatedElement, Class) - * @see #findAnnotationAttributes(AnnotatedElement, String) + * @see #findAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #findAnnotation(AnnotatedElement, Class) * @see #getAllAnnotationAttributes(AnnotatedElement, String) */ public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationType) { @@ -248,7 +251,9 @@ public class AnnotatedElementUtils { * annotations in lower levels of the annotation hierarchy. * *

Attributes from lower levels in the annotation hierarchy override - * attributes of the same name from higher levels. + * attributes of the same name from higher levels, and + * {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. * *

In contrast to {@link #getAllAnnotationAttributes}, the search * algorithm used by this method will stop searching the annotation @@ -269,8 +274,7 @@ public class AnnotatedElementUtils { * as Annotation instances * @return the merged {@code AnnotationAttributes}, or {@code null} if * not found - * @see #findAnnotationAttributes(AnnotatedElement, Class) - * @see #findAnnotationAttributes(AnnotatedElement, String) + * @see #findAnnotation(AnnotatedElement, Class) * @see #findAnnotationAttributes(AnnotatedElement, String, boolean, boolean) * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) */ @@ -286,47 +290,59 @@ public class AnnotatedElementUtils { /** * Find the first annotation of the specified {@code annotationType} within - * the annotation hierarchy above the supplied {@code element} and + * the annotation hierarchy above the supplied {@code element}, * merge that annotation's attributes with matching attributes from - * annotations in lower levels of the annotation hierarchy. + * annotations in lower levels of the annotation hierarchy, and synthesize + * the result back into an annotation of the specified {@code annotationType}. + * + *

{@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. * *

This method delegates to {@link #findAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} - * supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}. + * (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}) + * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. * * @param element the annotated element; never {@code null} * @param annotationType the annotation type to find; never {@code null} - * @return the merged {@code AnnotationAttributes}, or {@code null} if - * not found + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found * @since 4.2 - * @see #findAnnotationAttributes(AnnotatedElement, String) + * @see #findAnnotation(AnnotatedElement, String) * @see #findAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) */ - public static AnnotationAttributes findAnnotationAttributes(AnnotatedElement element, - Class annotationType) { + public static A findAnnotation(AnnotatedElement element, Class annotationType) { Assert.notNull(annotationType, "annotationType must not be null"); - return findAnnotationAttributes(element, annotationType.getName()); + return findAnnotation(element, annotationType.getName()); } /** * Find the first annotation of the specified {@code annotationType} within - * the annotation hierarchy above the supplied {@code element} and + * the annotation hierarchy above the supplied {@code element}, * merge that annotation's attributes with matching attributes from - * annotations in lower levels of the annotation hierarchy. + * annotations in lower levels of the annotation hierarchy, and synthesize + * the result back into an annotation of the specified {@code annotationType}. + * + *

{@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. * *

This method delegates to {@link #findAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} - * supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}. + * (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}) + * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. * * @param element the annotated element; never {@code null} * @param annotationType the fully qualified class name of the annotation * type to find; never {@code null} or empty - * @return the merged {@code AnnotationAttributes}, or {@code null} if - * not found + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found * @since 4.2 - * @see #findAnnotationAttributes(AnnotatedElement, Class) + * @see #findAnnotation(AnnotatedElement, Class) * @see #findAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) */ - public static AnnotationAttributes findAnnotationAttributes(AnnotatedElement element, String annotationType) { - return findAnnotationAttributes(element, annotationType, false, false); + @SuppressWarnings("unchecked") + public static A findAnnotation(AnnotatedElement element, String annotationType) { + AnnotationAttributes attributes = findAnnotationAttributes(element, annotationType, false, false); + return ((attributes != null) ? AnnotationUtils.synthesizeAnnotation(attributes, + (Class) attributes.annotationType(), element) : null); } /** @@ -336,7 +352,9 @@ public class AnnotatedElementUtils { * annotations in lower levels of the annotation hierarchy. * *

Attributes from lower levels in the annotation hierarchy override - * attributes of the same name from higher levels. + * attributes of the same name from higher levels, and + * {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. * *

In contrast to {@link #getAllAnnotationAttributes}, the search * algorithm used by this method will stop searching the annotation @@ -358,8 +376,7 @@ public class AnnotatedElementUtils { * @return the merged {@code AnnotationAttributes}, or {@code null} if * not found * @since 4.2 - * @see #findAnnotationAttributes(AnnotatedElement, Class) - * @see #findAnnotationAttributes(AnnotatedElement, String) + * @see #findAnnotation(AnnotatedElement, Class) * @see #getAnnotationAttributes(AnnotatedElement, String, boolean, boolean) */ public static AnnotationAttributes findAnnotationAttributes(AnnotatedElement element, String annotationType, diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java new file mode 100644 index 00000000000..ecb1e1c2698 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java @@ -0,0 +1,62 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; + +/** + * An {@code AnnotationAttributeExtractor} is responsible for + * {@linkplain #getAttributeValue extracting} annotation attribute values + * from an underlying {@linkplain #getSource source} such as an + * {@code Annotation} or a {@code Map}. + * + * @author Sam Brannen + * @since 4.2 + * @see SynthesizedAnnotationInvocationHandler + */ +interface AnnotationAttributeExtractor { + + /** + * Get the type of annotation that this extractor extracts attribute + * values for. + */ + Class getAnnotationType(); + + /** + * Get the element that is annotated with an annotation of the annotation + * type supported by this extractor. + * @return the annotated element, or {@code null} if unknown + */ + AnnotatedElement getAnnotatedElement(); + + /** + * Get the underlying source of annotation attributes. + */ + Object getSource(); + + /** + * Get the attribute value from the underlying {@linkplain #getSource source} + * that corresponds to the supplied attribute method. + * @param attributeMethod an attribute method from the annotation type + * supported by this extractor + * @return the value of the annotation attribute + */ + Object getAttributeValue(Method attributeMethod); + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index b2631831b9b..19623076032 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -809,8 +809,10 @@ public abstract class AnnotationUtils { * merging attributes within an annotation hierarchy. When running in merge mode, * the following special rules apply: *

    - *
  1. The supplied annotation will not be - * {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes.
  2. + *
  3. The supplied annotation will not be + * {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes; + * however, nested annotations and arrays of nested annotations will be + * synthesized.
  4. *
  5. Default values will be replaced with {@link #DEFAULT_VALUE_PLACEHOLDER}.
  6. *
  7. The resulting, merged annotation attributes should eventually be * {@linkplain #postProcessAnnotationAttributes post-processed} in order to @@ -1051,12 +1053,13 @@ public abstract class AnnotationUtils { * @param annotation the annotation to synthesize * @param annotatedElement the element that is annotated with the supplied * annotation; may be {@code null} if unknown - * @return the synthesized annotation, if the supplied annotation is + * @return the synthesized annotation if the supplied annotation is * synthesizable; {@code null} if the supplied annotation is - * {@code null}; otherwise, the supplied annotation unmodified + * {@code null}; otherwise the supplied annotation unmodified * @throws AnnotationConfigurationException if invalid configuration of * {@code @AliasFor} is detected * @since 4.2 + * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) */ @SuppressWarnings("unchecked") public static A synthesizeAnnotation(A annotation, AnnotatedElement annotatedElement) { @@ -1068,20 +1071,58 @@ public abstract class AnnotationUtils { } Class annotationType = annotation.annotationType(); - - // No need to synthesize? if (!isSynthesizable(annotationType)) { return annotation; } - InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(annotation, annotatedElement, - getAttributeAliasMap(annotationType)); + AnnotationAttributeExtractor attributeExtractor = new DefaultAnnotationAttributeExtractor(annotation, + annotatedElement); + InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(attributeExtractor); A synthesizedAnnotation = (A) Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), new Class[] { (Class) annotationType, SynthesizedAnnotation.class }, handler); return synthesizedAnnotation; } + /** + * Synthesize the supplied map of annotation attributes by + * wrapping it in a dynamic proxy that implements an annotation of type + * {@code annotationType} and transparently enforces attribute alias + * semantics for annotation attributes that are annotated with + * {@link AliasFor @AliasFor}. + *

    The supplied map must contain key-value pairs for every attribute + * defined by the supplied {@code annotationType}. + *

    Note that {@link AnnotationAttributes} is a specialized type of + * {@link Map} that is a suitable candidate for this method's + * {@code attributes} argument. + * + * @param attributes the map of annotation attributes to synthesize + * @param annotationType the type of annotation to synthesize; never {@code null} + * @param annotatedElement the element that is annotated with the annotation + * corresponding to the supplied attributes; may be {@code null} if unknown + * @return the synthesized annotation, or {@code null} if the supplied attributes + * map is {@code null} + * @throws AnnotationConfigurationException if invalid configuration is detected + * @since 4.2 + * @see #synthesizeAnnotation(Annotation, AnnotatedElement) + */ + @SuppressWarnings("unchecked") + public static A synthesizeAnnotation(Map attributes, + Class annotationType, AnnotatedElement annotatedElement) { + Assert.notNull(annotationType, "annotationType must not be null"); + + if (attributes == null) { + return null; + } + + AnnotationAttributeExtractor attributeExtractor = new MapAnnotationAttributeExtractor(attributes, + annotationType, annotatedElement); + InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(attributeExtractor); + A synthesizedAnnotation = (A) Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), new Class[] { + annotationType, SynthesizedAnnotation.class }, handler); + + return synthesizedAnnotation; + } /** * Get a map of all attribute alias pairs, declared via {@code @AliasFor} @@ -1098,7 +1139,7 @@ public abstract class AnnotationUtils { * @return a map containing attribute alias pairs; never {@code null} * @since 4.2 */ - private static Map getAttributeAliasMap(Class annotationType) { + static Map getAttributeAliasMap(Class annotationType) { if (annotationType == null) { return Collections.emptyMap(); } @@ -1334,7 +1375,7 @@ public abstract class AnnotationUtils { methods = new ArrayList(); for (Method method : annotationType.getDeclaredMethods()) { - if ((method.getParameterTypes().length == 0) && (method.getReturnType() != void.class)) { + if (isAttributeMethod(method)) { ReflectionUtils.makeAccessible(method); methods.add(method); } @@ -1345,6 +1386,24 @@ public abstract class AnnotationUtils { return methods; } + /** + * Determine if the supplied {@code method} is an annotation attribute method. + * @param method the method to check + * @return {@code true} if the method is an attribute method + */ + static boolean isAttributeMethod(Method method) { + return ((method != null) && (method.getParameterTypes().length == 0) && (method.getReturnType() != void.class)); + } + + /** + * Determine if the supplied method is an "annotationType" method. + * @return {@code true} if the method is an "annotationType" method + * @see Annotation#annotationType() + */ + static boolean isAnnotationTypeMethod(Method method) { + return ((method != null) && method.getName().equals("annotationType") && (method.getParameterTypes().length == 0)); + } + /** * Post-process the supplied {@link AnnotationAttributes}. * diff --git a/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java new file mode 100644 index 00000000000..4232c05e29e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java @@ -0,0 +1,65 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; + +import org.springframework.util.ReflectionUtils; + +/** + * Default implementation of the {@link AnnotationAttributeExtractor} strategy + * that is backed by an {@link Annotation}. + * + * @author Sam Brannen + * @since 4.2 + * @see Annotation + * @see AliasFor + * @see AbstractAliasAwareAnnotationAttributeExtractor + * @see MapAnnotationAttributeExtractor + * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) + */ +class DefaultAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttributeExtractor { + + /** + * Construct a new {@code DefaultAnnotationAttributeExtractor}. + * @param annotation the annotation to synthesize; never {@code null} + * @param annotatedElement the element that is annotated with the supplied + * annotation; may be {@code null} if unknown + */ + DefaultAnnotationAttributeExtractor(Annotation annotation, AnnotatedElement annotatedElement) { + super(annotation.annotationType(), annotatedElement, annotation); + } + + @Override + protected Object getRawAttributeValue(Method attributeMethod) { + ReflectionUtils.makeAccessible(attributeMethod); + return ReflectionUtils.invokeMethod(attributeMethod, getAnnotation()); + } + + @Override + protected Object getRawAttributeValue(String attributeName) { + Method attributeMethod = ReflectionUtils.findMethod(getAnnotation().annotationType(), attributeName); + return getRawAttributeValue(attributeMethod); + } + + private Annotation getAnnotation() { + return (Annotation) getSource(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java new file mode 100644 index 00000000000..2a8704fabd1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java @@ -0,0 +1,100 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.ClassUtils; + +import static org.springframework.core.annotation.AnnotationUtils.*; + +/** + * Implementation of the {@link AnnotationAttributeExtractor} strategy that + * is backed by a {@link Map}. + * + * @author Sam Brannen + * @since 4.2 + * @see Annotation + * @see AliasFor + * @see AbstractAliasAwareAnnotationAttributeExtractor + * @see DefaultAnnotationAttributeExtractor + * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) + */ +class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttributeExtractor { + + /** + * Construct a new {@code MapAnnotationAttributeExtractor}. + *

    The supplied map must contain key-value pairs for every attribute + * defined in the supplied {@code annotationType}. + * @param attributes the map of annotation attributes; never {@code null} + * @param annotationType the type of annotation to synthesize; never {@code null} + * @param annotatedElement the element that is annotated with the annotation + * of the supplied type; may be {@code null} if unknown + */ + MapAnnotationAttributeExtractor(Map attributes, Class annotationType, + AnnotatedElement annotatedElement) { + super(annotationType, annotatedElement, new HashMap(attributes)); + validateAttributes(attributes, annotationType); + } + + @Override + protected Object getRawAttributeValue(Method attributeMethod) { + return getMap().get(attributeMethod.getName()); + } + + @Override + protected Object getRawAttributeValue(String attributeName) { + return getMap().get(attributeName); + } + + @SuppressWarnings("unchecked") + private Map getMap() { + return (Map) getSource(); + } + + /** + * Validate the supplied {@code attributes} map by verifying that it + * contains a non-null entry for each annotation attribute in the specified + * {@code annotationType} and that the type of the entry matches the + * return type for the corresponding annotation attribute. + */ + private static void validateAttributes(Map attributes, Class annotationType) { + for (Method attributeMethod : getAttributeMethods(annotationType)) { + String attributeName = attributeMethod.getName(); + + Object attributeValue = attributes.get(attributeName); + if (attributeValue == null) { + throw new IllegalArgumentException(String.format( + "Attributes map [%s] returned null for required attribute [%s] defined by annotation type [%s].", + attributes, attributeName, annotationType.getName())); + } + + Class returnType = attributeMethod.getReturnType(); + if (!ClassUtils.isAssignable(returnType, attributeValue.getClass())) { + throw new IllegalArgumentException(String.format( + "Attributes map [%s] returned a value of type [%s] for attribute [%s], " + + "but a value of type [%s] is required as defined by annotation type [%s].", attributes, + attributeValue.getClass().getName(), attributeName, returnType.getName(), annotationType.getName())); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java index 66dee3f41ca..dedd7c9badc 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java @@ -18,6 +18,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Arrays; @@ -25,6 +26,7 @@ import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -36,150 +38,141 @@ import static org.springframework.util.ReflectionUtils.*; * synthesized (i.e., wrapped in a dynamic proxy) with additional * functionality. * - *

    {@code SynthesizedAnnotationInvocationHandler} transparently enforces - * attribute alias semantics for annotation attributes that are annotated - * with {@link AliasFor @AliasFor}. In addition, nested annotations and - * arrays of nested annotations will be synthesized upon first access (i.e., - * lazily). - * * @author Sam Brannen * @since 4.2 * @see Annotation - * @see AliasFor + * @see AnnotationAttributeExtractor * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) */ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { - private final AnnotatedElement annotatedElement; - - private final Annotation annotation; - - private final Class annotationType; - - private final Map aliasMap; + private final AnnotationAttributeExtractor attributeExtractor; - private final Map computedValueCache; + private final Map valueCache = new ConcurrentHashMap(8); /** - * Construct a new {@code SynthesizedAnnotationInvocationHandler}. - * - * @param annotation the annotation to synthesize - * @param annotatedElement the element that is annotated with the supplied - * annotation; may be {@code null} if unknown - * @param aliasMap the map of attribute alias pairs, declared via - * {@code @AliasFor} in the supplied annotation + * Construct a new {@code SynthesizedAnnotationInvocationHandler} for + * the supplied {@link AnnotationAttributeExtractor}. + * @param attributeExtractor the extractor to delegate to */ - SynthesizedAnnotationInvocationHandler(Annotation annotation, AnnotatedElement annotatedElement, - Map aliasMap) { - this.annotatedElement = annotatedElement; - this.annotation = annotation; - this.annotationType = annotation.annotationType(); - this.aliasMap = aliasMap; - this.computedValueCache = new ConcurrentHashMap(aliasMap.size()); + SynthesizedAnnotationInvocationHandler(AnnotationAttributeExtractor attributeExtractor) { + Assert.notNull(attributeExtractor, "AnnotationAttributeExtractor must not be null"); + this.attributeExtractor = attributeExtractor; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (isEqualsMethod(method)) { - return equals(proxy, args[0]); + return annotationEquals(args[0]); } if (isHashCodeMethod(method)) { - return hashCode(proxy); + return annotationHashCode(); } if (isToStringMethod(method)) { - return toString(proxy); + return annotationToString(); } - - String methodName = method.getName(); - Class returnType = method.getReturnType(); - boolean nestedAnnotation = (Annotation[].class.isAssignableFrom(returnType) || Annotation.class.isAssignableFrom(returnType)); - String aliasedAttributeName = aliasMap.get(methodName); - boolean aliasPresent = (aliasedAttributeName != null); - - makeAccessible(method); - - // No custom processing necessary? - if (!aliasPresent && !nestedAnnotation) { - return invokeMethod(method, this.annotation, args); + if (isAnnotationTypeMethod(method)) { + return annotationType(); } - - Object cachedValue = this.computedValueCache.get(methodName); - if (cachedValue != null) { - return cachedValue; + if (!isAttributeMethod(method)) { + String msg = String.format("Method [%s] is unsupported for synthesized annotation type [%s]", method, + annotationType()); + throw new AnnotationConfigurationException(msg); } + return getAttributeValue(method); + } - Object value = invokeMethod(method, this.annotation, args); + private Class annotationType() { + return this.attributeExtractor.getAnnotationType(); + } - if (aliasPresent) { - Method aliasedMethod = null; - try { - aliasedMethod = this.annotationType.getDeclaredMethod(aliasedAttributeName); - } - catch (NoSuchMethodException e) { - String msg = String.format("In annotation [%s], attribute [%s] is declared as an @AliasFor [%s], " - + "but attribute [%s] does not exist.", this.annotationType.getName(), methodName, - aliasedAttributeName, aliasedAttributeName); - throw new AnnotationConfigurationException(msg); + private Object getAttributeValue(Method attributeMethod) { + String attributeName = attributeMethod.getName(); + Object value = this.valueCache.get(attributeName); + if (value == null) { + value = this.attributeExtractor.getAttributeValue(attributeMethod); + if (value == null) { + throw new IllegalStateException(String.format( + "%s returned null for attribute name [%s] from attribute source [%s]", + this.attributeExtractor.getClass().getName(), attributeName, this.attributeExtractor.getSource())); } - makeAccessible(aliasedMethod); - Object aliasedValue = invokeMethod(aliasedMethod, this.annotation); - Object defaultValue = getDefaultValue(this.annotation, methodName); - - if (!nullSafeEquals(value, aliasedValue) && !nullSafeEquals(value, defaultValue) - && !nullSafeEquals(aliasedValue, defaultValue)) { - String elementName = (this.annotatedElement == null ? "unknown element" - : this.annotatedElement.toString()); - String msg = String.format( - "In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] are " - + "declared with values of [%s] and [%s], but only one declaration is permitted.", - this.annotationType.getName(), elementName, methodName, aliasedAttributeName, - nullSafeToString(value), nullSafeToString(aliasedValue)); - throw new AnnotationConfigurationException(msg); + // Synthesize nested annotations before returning them. + if (value instanceof Annotation) { + value = synthesizeAnnotation((Annotation) value, this.attributeExtractor.getAnnotatedElement()); } - - // If the user didn't declare the annotation with an explicit value, return - // the value of the alias. - if (nullSafeEquals(value, defaultValue)) { - value = aliasedValue; + else if (value instanceof Annotation[]) { + Annotation[] orig = (Annotation[]) value; + Annotation[] clone = (Annotation[]) Array.newInstance(orig.getClass().getComponentType(), orig.length); + for (int i = 0; i < orig.length; i++) { + clone[i] = synthesizeAnnotation(orig[i], this.attributeExtractor.getAnnotatedElement()); + } + value = clone; } - } - // Synthesize nested annotations before returning them. - if (value instanceof Annotation) { - value = synthesizeAnnotation((Annotation) value, this.annotatedElement); - } - else if (value instanceof Annotation[]) { - Annotation[] annotations = (Annotation[]) value; - for (int i = 0; i < annotations.length; i++) { - annotations[i] = synthesizeAnnotation(annotations[i], this.annotatedElement); - } + this.valueCache.put(attributeName, value); } - this.computedValueCache.put(methodName, value); + // Clone arrays so that users cannot alter the contents of values in our cache. + if (value.getClass().isArray()) { + value = cloneArray(value); + } return value; } + /** + * Clone the provided array, ensuring that original component type is + * retained. + * @param array the array to clone + */ + private Object cloneArray(Object array) { + if (array instanceof boolean[]) { + return ((boolean[]) array).clone(); + } + if (array instanceof byte[]) { + return ((byte[]) array).clone(); + } + if (array instanceof char[]) { + return ((char[]) array).clone(); + } + if (array instanceof double[]) { + return ((double[]) array).clone(); + } + if (array instanceof float[]) { + return ((float[]) array).clone(); + } + if (array instanceof int[]) { + return ((int[]) array).clone(); + } + if (array instanceof long[]) { + return ((long[]) array).clone(); + } + if (array instanceof short[]) { + return ((short[]) array).clone(); + } + + // else + return ((Object[]) array).clone(); + } + /** * See {@link Annotation#equals(Object)} for a definition of the required algorithm. - * - * @param proxy the synthesized annotation * @param other the other object to compare against */ - private boolean equals(Object proxy, Object other) { + private boolean annotationEquals(Object other) { if (this == other) { return true; } - if (!this.annotationType.isInstance(other)) { + if (!annotationType().isInstance(other)) { return false; } - for (Method attributeMethod : getAttributeMethods(this.annotationType)) { - Object thisValue = invokeMethod(attributeMethod, proxy); + for (Method attributeMethod : getAttributeMethods(annotationType())) { + Object thisValue = getAttributeValue(attributeMethod); Object otherValue = invokeMethod(attributeMethod, other); - if (!nullSafeEquals(thisValue, otherValue)) { + if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) { return false; } } @@ -189,14 +182,12 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { /** * See {@link Annotation#hashCode()} for a definition of the required algorithm. - * - * @param proxy the synthesized annotation */ - private int hashCode(Object proxy) { + private int annotationHashCode() { int result = 0; - for (Method attributeMethod : getAttributeMethods(this.annotationType)) { - Object value = invokeMethod(attributeMethod, proxy); + for (Method attributeMethod : getAttributeMethods(annotationType())) { + Object value = getAttributeValue(attributeMethod); int hashCode; if (value.getClass().isArray()) { hashCode = hashCodeForArray(value); @@ -250,39 +241,27 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { /** * See {@link Annotation#toString()} for guidelines on the recommended format. - * - * @param proxy the synthesized annotation */ - private String toString(Object proxy) { - StringBuilder sb = new StringBuilder("@").append(annotationType.getName()).append("("); + private String annotationToString() { + StringBuilder sb = new StringBuilder("@").append(annotationType().getName()).append("("); - Iterator iterator = getAttributeMethods(this.annotationType).iterator(); + Iterator iterator = getAttributeMethods(annotationType()).iterator(); while (iterator.hasNext()) { Method attributeMethod = iterator.next(); sb.append(attributeMethod.getName()); sb.append('='); - sb.append(valueToString(invokeMethod(attributeMethod, proxy))); + sb.append(attributeValueToString(getAttributeValue(attributeMethod))); sb.append(iterator.hasNext() ? ", " : ""); } return sb.append(")").toString(); } - private String valueToString(Object value) { + private String attributeValueToString(Object value) { if (value instanceof Object[]) { return "[" + StringUtils.arrayToDelimitedString((Object[]) value, ", ") + "]"; } - - // else return String.valueOf(value); } - private static boolean nullSafeEquals(Object o1, Object o2) { - return ObjectUtils.nullSafeEquals(o1, o2); - } - - private static String nullSafeToString(Object obj) { - return ObjectUtils.nullSafeToString(obj); - } - } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index d885830a170..5e677315df7 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -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 annotationType) { + Assert.notNull(annotationType, "annotationType must not be null"); + return AnnotatedElementUtils.findAnnotationAttributes(element, annotationType.getName(), false, false); + } + + // ------------------------------------------------------------------------- @MetaCycle3 diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index 01f8f62a86e..a23c48380e3 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -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 annotations = getRepeatableAnnotation(TestCase.class, Hierarchy.class, ContextConfig.class); + Set annotations = getRepeatableAnnotation(ConfigHierarchyTestCase.class, Hierarchy.class, + ContextConfig.class); assertNotNull(annotations); List 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 map = new HashMap(); + 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(), Component.class, null); + } + + @Test + public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception { + Map map = new HashMap(); + 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 map = new HashMap(); + 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 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 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 { diff --git a/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java index 57cd5b3c0b9..8b88169f3d6 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java @@ -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. getNumber("millis").longValue(); - return Math.max(0, millis); + return Math.max(0, timed.millis()); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java index ddc82ce0d64..0a2a11f77a7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java @@ -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) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index e10cd78fb6b..1f44ce1e62c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -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 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[] listenerClasses = (Class[]) annAttrs.getClassArray("listeners"); - - boolean inheritListeners = annAttrs.getBoolean("inheritListeners"); + boolean inheritListeners = testExecutionListeners.inheritListeners(); AnnotationDescriptor 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.> asList(listenerClasses)); + classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners())); descriptor = (inheritListeners ? superDescriptor : null); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java index 57cc51496e2..f542eac4ab2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java @@ -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 resolverClass = annAttrs.getClass("resolver"); + Class 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); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java index b0e0b42ed39..78877b64eba 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -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 configAttributesList = new ArrayList(); 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 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); - } - } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java index 36610422ece..a48f32fc71c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java @@ -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()); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java index 54e2af011cb..bde9b0f45a8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java @@ -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. getEnum("methodMode") : null; - ClassMode classMode = classAnnotated ? classAnnAttrs. 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. getEnum("hierarchyMode") - : classAnnAttrs. 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. 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. getEnum("hierarchyMode"); - dirtyContext(testContext, hierarchyMode); + dirtyContext(testContext, dirtiesContext.hierarchyMode()); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java index 041362a7bd4..e2ed3c2c6c6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java @@ -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, diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 1a97860ca8e..4b0994ebd77 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -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); } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 4bad9d97a49..49fa5c39030 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -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; diff --git a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java index dfbe1f522b6..525e0999e41 100644 --- a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java @@ -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) 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 getAnnotationType() { return this.annotation.annotationType(); } diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java index a8883bab2d8..ed91023988b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java @@ -79,7 +79,7 @@ public class SqlScriptsTestExecutionListenerTests { containsString("attribute [scripts] and its alias [value]"))); exception.expectMessage(either(containsString("values of [{foo}] and [{bar}]")).or( containsString("values of [{bar}] and [{foo}]"))); - exception.expectMessage(containsString("but only one declaration is permitted")); + exception.expectMessage(containsString("but only one is permitted")); listener.beforeTestMethod(testContext); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index 90142c46037..a11a4c08717 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -401,7 +401,7 @@ public final class RequestMappingInfo implements RequestCondition condition); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index ad132b676a0..666dde2656f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -26,6 +26,7 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.aopalliance.intercept.MethodInterceptor; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -42,7 +43,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.objenesis.Objenesis; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.AntPathMatcher; @@ -418,13 +418,11 @@ public class MvcUriComponentsBuilder { private static String getTypeRequestMapping(Class controllerType) { Assert.notNull(controllerType, "'controllerType' must not be null"); - String annotType = RequestMapping.class.getName(); - AnnotationAttributes attrs = AnnotatedElementUtils.findAnnotationAttributes(controllerType, annotType); - if (attrs == null) { + RequestMapping requestMapping = AnnotatedElementUtils.findAnnotation(controllerType, RequestMapping.class); + if (requestMapping == null) { return "/"; } - String[] paths = attrs.getStringArray("path"); - paths = ObjectUtils.isEmpty(paths) ? attrs.getStringArray("value") : paths; + String[] paths = requestMapping.path(); if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { return "/"; } @@ -435,13 +433,11 @@ public class MvcUriComponentsBuilder { } private static String getMethodRequestMapping(Method method) { - String annotType = RequestMapping.class.getName(); - AnnotationAttributes attrs = AnnotatedElementUtils.findAnnotationAttributes(method, annotType); - if (attrs == null) { + RequestMapping requestMapping = AnnotatedElementUtils.findAnnotation(method, RequestMapping.class); + if (requestMapping == null) { throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); } - String[] paths = attrs.getStringArray("path"); - paths = ObjectUtils.isEmpty(paths) ? attrs.getStringArray("value") : paths; + String[] paths = requestMapping.path(); if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { return "/"; } @@ -759,7 +755,6 @@ public class MvcUriComponentsBuilder { * that accept the controllerType. */ @Deprecated - @SuppressWarnings("unused") public MethodArgumentBuilder(Method method) { this(method.getDeclaringClass(), method); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 8275fa85b79..238753798c4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -22,12 +22,10 @@ import java.util.List; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.CrossOrigin; @@ -49,6 +47,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sam Brannen * @since 3.1 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping @@ -212,7 +211,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @param handlerType the handler type for which to create the condition * @return the condition, or {@code null} */ - @SuppressWarnings("unused") protected RequestCondition getCustomTypeCondition(Class handlerType) { return null; } @@ -228,77 +226,41 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @param method the handler method for which to create the condition * @return the condition, or {@code null} */ - @SuppressWarnings("unused") protected RequestCondition getCustomMethodCondition(Method method) { return null; } /** - * Transitional method used to invoke one of two createRequestMappingInfo - * variants one of which is deprecated. + * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, + * supplying the appropriate custom {@link RequestCondition} depending on whether + * the supplied {@code annotatedElement} is a class or method. + * + * @see #getCustomTypeCondition(Class) + * @see #getCustomMethodCondition(Method) */ - @SuppressWarnings("deprecation") private RequestMappingInfo createRequestMappingInfo(AnnotatedElement annotatedElement) { - RequestMapping annotation; - AnnotationAttributes attributes; - RequestCondition customCondition; - String annotationType = RequestMapping.class.getName(); - if (annotatedElement instanceof Class) { - Class type = (Class) annotatedElement; - annotation = AnnotationUtils.findAnnotation(type, RequestMapping.class); - attributes = AnnotatedElementUtils.findAnnotationAttributes(type, annotationType); - customCondition = getCustomTypeCondition(type); - } - else { - Method method = (Method) annotatedElement; - annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - attributes = AnnotatedElementUtils.findAnnotationAttributes(method, annotationType); - customCondition = getCustomMethodCondition(method); - } - RequestMappingInfo info = null; - if (annotation != null) { - info = createRequestMappingInfo(annotation, customCondition); - if (info == null) { - info = createRequestMappingInfo(attributes, customCondition); - } - } - return info; - } - - /** - * Create a RequestMappingInfo from a RequestMapping annotation. - * @deprecated as of 4.2 after the introduction of support for - * {@code @RequestMapping} as meta-annotation. Please use - * {@link #createRequestMappingInfo(AnnotationAttributes, RequestCondition)}. - */ - @Deprecated - @SuppressWarnings("unused") - protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, - RequestCondition customCondition) { - - return null; + RequestMapping requestMapping = AnnotatedElementUtils.findAnnotation(annotatedElement, RequestMapping.class); + RequestCondition customCondition = ((annotatedElement instanceof Class) ? getCustomTypeCondition((Class) annotatedElement) + : getCustomMethodCondition((Method) annotatedElement)); + return ((requestMapping != null) ? createRequestMappingInfo(requestMapping, customCondition) : null); } /** - * Create a RequestMappingInfo from the attributes of an - * {@code @RequestMapping} annotation or a meta-annotation, i.e. a custom - * annotation annotated with {@code @RequestMapping}. - * @since 4.2 + * Create a {@link RequestMappingInfo} from the supplied + * {@link RequestMapping @RequestMapping} annotation, which is either + * a directly declared annotation, a meta-annotation, or the synthesized + * result of merging annotation attributes within an annotation hierarchy. */ - protected RequestMappingInfo createRequestMappingInfo(AnnotationAttributes attributes, + protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, RequestCondition customCondition) { - String[] paths = attributes.getStringArray("path"); - paths = ObjectUtils.isEmpty(paths) ? attributes.getStringArray("value") : paths; - paths = resolveEmbeddedValuesInPatterns(paths); - - return RequestMappingInfo.paths(paths) - .methods((RequestMethod[]) attributes.get("method")) - .params(attributes.getStringArray("params")) - .headers(attributes.getStringArray("headers")) - .consumes(attributes.getStringArray("consumes")) - .produces(attributes.getStringArray("produces")) - .mappingName(attributes.getString("name")) + return RequestMappingInfo.paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) + .methods(requestMapping.method()) + .params(requestMapping.params()) + .headers(requestMapping.headers()) + .consumes(requestMapping.consumes()) + .produces(requestMapping.produces()) + .mappingName(requestMapping.name()) .customCondition(customCondition) .options(this.config) .build();