Synthesize annotation from a map of attributes

Spring Framework 4.2 RC1 introduced support for synthesizing an
annotation from an existing annotation in order to provide additional
functionality above and beyond that provided by Java. Specifically,
such synthesized annotations provide support for @AliasFor semantics.
As luck would have it, the same principle can be used to synthesize an
annotation from any map of attributes, and in particular, from an
instance of AnnotationAttributes.

The following highlight the major changes in this commit toward
achieving this goal.

- Introduced AnnotationAttributeExtractor abstraction and refactored
  SynthesizedAnnotationInvocationHandler to delegate to an
  AnnotationAttributeExtractor.

- Extracted code from SynthesizedAnnotationInvocationHandler into new
  AbstractAliasAwareAnnotationAttributeExtractor and
  DefaultAnnotationAttributeExtractor implementation classes.

- Introduced MapAnnotationAttributeExtractor for synthesizing an
  annotation that is backed by a map or AnnotationAttributes instance.

- Introduced a variant of synthesizeAnnotation() in AnnotationUtils
  that accepts a map.

- Introduced findAnnotation(*) methods in AnnotatedElementUtils that
  synthesize merged AnnotationAttributes back into an annotation of the
  target type.

The following classes have been refactored to use the new support for
synthesizing AnnotationAttributes back into an annotation.

- ApplicationListenerMethodAdapter
- TestAnnotationUtils
- AbstractTestContextBootstrapper
- ActiveProfilesUtils
- ContextLoaderUtils
- DefaultActiveProfilesResolver
- DirtiesContextTestExecutionListener
- TestPropertySourceAttributes
- TestPropertySourceUtils
- TransactionalTestExecutionListener
- MetaAnnotationUtils
- MvcUriComponentsBuilder
- RequestMappingHandlerMapping

In addition, this commit also includes changes to ensure that arrays
returned by synthesized annotations are properly cloned first.

Issue: SPR-13067
This commit is contained in:
Sam Brannen
2015-05-25 16:58:18 +02:00
parent f41de12cf6
commit e30c9b2ef3
25 changed files with 881 additions and 358 deletions

View File

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