Merge from sbrannen/SPR-13067

* SPR-13067:
  Synthesize annotation from a map of attributes
This commit is contained in:
Sam Brannen 2015-05-29 01:43:50 +02:00
commit a31d1bdf60
25 changed files with 881 additions and 358 deletions

View File

@ -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;

View File

@ -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<? extends Annotation> annotationType;
private final AnnotatedElement annotatedElement;
private final Object source;
private final Map<String, String> 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<? extends Annotation> 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<? extends Annotation> 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);
}
}

View File

@ -48,8 +48,8 @@ import org.springframework.util.StringUtils;
* <h3>Annotation Attribute Overrides</h3>
* <p>Support for meta-annotations with <em>attribute overrides</em> in
* <em>composed annotations</em> is provided by all variants of the
* {@code getAnnotationAttributes()} and {@code findAnnotationAttributes()}
* methods.
* {@code getAnnotationAttributes()}, {@code findAnnotation()}, and
* {@code findAnnotationAttributes()} methods.
*
* <h3>Find vs. Get Semantics</h3>
* <p>The search algorithms used by methods in this class follow either
@ -224,6 +224,9 @@ public class AnnotatedElementUtils {
* merge that annotation's attributes with <em>matching</em> attributes from
* annotations in lower levels of the annotation hierarchy.
*
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both
* within a single annotation and within the annotation hierarchy.
*
* <p>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.
*
* <p>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.
*
* <p>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 <em>above</em> the supplied {@code element} and
* the annotation hierarchy <em>above</em> the supplied {@code element},
* merge that annotation's attributes with <em>matching</em> 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}.
*
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both
* within a single annotation and within the annotation hierarchy.
*
* <p>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<? extends Annotation> annotationType) {
public static <A extends Annotation> A findAnnotation(AnnotatedElement element, Class<A> 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 <em>above</em> the supplied {@code element} and
* the annotation hierarchy <em>above</em> the supplied {@code element},
* merge that annotation's attributes with <em>matching</em> 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}.
*
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both
* within a single annotation and within the annotation hierarchy.
*
* <p>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 extends Annotation> A findAnnotation(AnnotatedElement element, String annotationType) {
AnnotationAttributes attributes = findAnnotationAttributes(element, annotationType, false, false);
return ((attributes != null) ? AnnotationUtils.synthesizeAnnotation(attributes,
(Class<A>) attributes.annotationType(), element) : null);
}
/**
@ -336,7 +352,9 @@ public class AnnotatedElementUtils {
* annotations in lower levels of the annotation hierarchy.
*
* <p>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.
*
* <p>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,

View File

@ -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<? extends Annotation> 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);
}

View File

@ -809,8 +809,10 @@ public abstract class AnnotationUtils {
* merging attributes within an annotation hierarchy. When running in <em>merge mode</em>,
* the following special rules apply:
* <ol>
* <li>The supplied annotation will <strong>not</strong> be
* {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes.</li>
* <li>The supplied annotation will <em>not</em> be
* {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes;
* however, nested annotations and arrays of nested annotations <em>will</em> be
* synthesized.</li>
* <li>Default values will be replaced with {@link #DEFAULT_VALUE_PLACEHOLDER}.</li>
* <li>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
* <em>synthesizable</em>; {@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 extends Annotation> A synthesizeAnnotation(A annotation, AnnotatedElement annotatedElement) {
@ -1068,20 +1071,58 @@ public abstract class AnnotationUtils {
}
Class<? extends Annotation> 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<A>) annotationType, SynthesizedAnnotation.class }, handler);
return synthesizedAnnotation;
}
/**
* <em>Synthesize</em> the supplied map of annotation attributes by
* wrapping it in a dynamic proxy that implements an annotation of type
* {@code annotationType} and transparently enforces <em>attribute alias</em>
* semantics for annotation attributes that are annotated with
* {@link AliasFor @AliasFor}.
* <p>The supplied map must contain key-value pairs for every attribute
* defined by the supplied {@code annotationType}.
* <p>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 extends Annotation> A synthesizeAnnotation(Map<String, Object> attributes,
Class<A> 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<String, String> getAttributeAliasMap(Class<? extends Annotation> annotationType) {
static Map<String, String> getAttributeAliasMap(Class<? extends Annotation> annotationType) {
if (annotationType == null) {
return Collections.emptyMap();
}
@ -1334,7 +1375,7 @@ public abstract class AnnotationUtils {
methods = new ArrayList<Method>();
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}.
*

View File

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

View File

@ -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}.
* <p>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<String, Object> attributes, Class<? extends Annotation> annotationType,
AnnotatedElement annotatedElement) {
super(annotationType, annotatedElement, new HashMap<String, Object>(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<String, Object> getMap() {
return (Map<String, Object>) 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<String, Object> attributes, Class<? extends Annotation> 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()));
}
}
}
}

View File

@ -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.*;
* <em>synthesized</em> (i.e., wrapped in a dynamic proxy) with additional
* functionality.
*
* <p>{@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.,
* <em>lazily</em>).
*
* @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 AnnotationAttributeExtractor attributeExtractor;
private final Annotation annotation;
private final Class<? extends Annotation> annotationType;
private final Map<String, String> aliasMap;
private final Map<String, Object> computedValueCache;
private final Map<String, Object> valueCache = new ConcurrentHashMap<String, Object>(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<String, String> aliasMap) {
this.annotatedElement = annotatedElement;
this.annotation = annotation;
this.annotationType = annotation.annotationType();
this.aliasMap = aliasMap;
this.computedValueCache = new ConcurrentHashMap<String, Object>(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<? extends Annotation> 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());
}
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;
}
// If the user didn't declare the annotation with an explicit value, return
// the value of the alias.
if (nullSafeEquals(value, defaultValue)) {
value = aliasedValue;
}
this.valueCache.put(attributeName, value);
}
// Synthesize nested annotations before returning them.
if (value instanceof Annotation) {
value = synthesizeAnnotation((Annotation) value, this.annotatedElement);
// Clone arrays so that users cannot alter the contents of values in our cache.
if (value.getClass().isArray()) {
value = cloneArray(value);
}
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.computedValueCache.put(methodName, 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<Method> iterator = getAttributeMethods(this.annotationType).iterator();
Iterator<Method> 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -401,7 +401,7 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
Builder mappingName(String name);
/**
* Set a custom conditions to use.
* Set a custom condition to use.
*/
Builder customCondition(RequestCondition<?> condition);

View File

@ -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);
}

View File

@ -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;
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 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)}.
* 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.
*/
@Deprecated
@SuppressWarnings("unused")
protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation,
protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping,
RequestCondition<?> customCondition) {
return 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
*/
protected RequestMappingInfo createRequestMappingInfo(AnnotationAttributes attributes,
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();