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();