Allow meta-annotations to override attributes from their parent

Issue: SPR-10181
This commit is contained in:
Juergen Hoeller
2013-08-21 17:35:56 +02:00
parent 87c2d6fd12
commit 6d3649858e
11 changed files with 434 additions and 261 deletions

View File

@@ -0,0 +1,202 @@
/*
* Copyright 2002-2013 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.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Utility class used to collect all annotation values including those declared on meta-annotations.
*
* @author Phillip Webb
* @author Juergen Hoeller
* @since 4.0
*/
public class AnnotatedElementUtils {
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element, String annotationType) {
final Set<String> types = new LinkedHashSet<String>();
process(element, annotationType, new Processor<Object>() {
@Override
public Object process(Annotation annotation, int depth) {
if (depth > 0) {
types.add(annotation.annotationType().getName());
}
return null;
}
@Override
public void postProcess(Annotation annotation, Object result) {
}
});
return (types.isEmpty() ? null : types);
}
public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationType) {
return Boolean.TRUE.equals(process(element, annotationType, new Processor<Boolean>() {
@Override
public Boolean process(Annotation annotation, int depth) {
if (depth > 0) {
return true;
}
return null;
}
@Override
public void postProcess(Annotation annotation, Boolean result) {
}
}));
}
public static boolean isAnnotated(AnnotatedElement element, String annotationType) {
return Boolean.TRUE.equals(process(element, annotationType, new Processor<Boolean>() {
@Override
public Boolean process(Annotation annotation, int depth) {
return true;
}
@Override
public void postProcess(Annotation annotation, Boolean result) {
}
}));
}
public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationType) {
return getAnnotationAttributes(element, annotationType, false, false);
}
public static AnnotationAttributes getAnnotationAttributes(
AnnotatedElement element, String annotationType,
final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
return process(element, annotationType, new Processor<AnnotationAttributes>() {
@Override
public AnnotationAttributes process(Annotation annotation, int depth) {
return AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap);
}
@Override
public void postProcess(Annotation annotation, AnnotationAttributes result) {
for (String key : result.keySet()) {
if (!"value".equals(key)) {
Object value = AnnotationUtils.getValue(annotation, key);
if (value != null) {
result.put(key, value);
}
}
}
}
});
}
public static MultiValueMap<String, Object> getAllAnnotationAttributes(
AnnotatedElement element, final String annotationType,
final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
final MultiValueMap<String, Object> attributes = new LinkedMultiValueMap<String, Object>();
process(element, annotationType, new Processor<Void>() {
@Override
public Void process(Annotation annotation, int depth) {
if (annotation.annotationType().getName().equals(annotationType)) {
for (Map.Entry<String, Object> entry :
AnnotationUtils.getAnnotationAttributes(
annotation, classValuesAsString, nestedAnnotationsAsMap).entrySet()) {
attributes.add(entry.getKey(), entry.getValue());
}
}
return null;
}
@Override
public void postProcess(Annotation annotation, Void result) {
for (String key : attributes.keySet()) {
if (!"value".equals(key)) {
Object value = AnnotationUtils.getValue(annotation, key);
if (value != null) {
attributes.add(key, value);
}
}
}
}
});
return (attributes.isEmpty() ? null : attributes);
}
/**
* Process all annotations of the specified annotation type and recursively all
* meta-annotations on the specified type.
* @param element the annotated element
* @param annotationType the annotation type to find. Only items of the specified
* type or meta-annotations of the specified type will be processed.
* @param processor the processor
* @return the result of the processor
*/
private static <T> T process(AnnotatedElement element, String annotationType, Processor<T> processor) {
return doProcess(element, annotationType, processor, new HashSet<AnnotatedElement>(), 0);
}
private static <T> T doProcess(AnnotatedElement element, String annotationType,
Processor<T> processor, Set<AnnotatedElement> visited, int depth) {
if (visited.add(element)) {
for (Annotation annotation : element.getAnnotations()) {
if (annotation.annotationType().getName().equals(annotationType) || depth > 0) {
T result = processor.process(annotation, depth);
if (result != null) {
return result;
}
result = doProcess(annotation.annotationType(), annotationType, processor, visited, depth + 1);
if (result != null) {
processor.postProcess(annotation, result);
return result;
}
}
}
for (Annotation annotation : element.getAnnotations()) {
T result = doProcess(annotation.annotationType(), annotationType, processor, visited, depth);
if (result != null) {
processor.postProcess(annotation, result);
return result;
}
}
}
return null;
}
/**
* Callback interface used to process an annotation.
* @param <T> the result type
*/
private static interface Processor<T> {
/**
* Called to process the annotation.
* @param annotation the annotation to process
* @param depth the depth of the annotation relative to the initial match.
* For example, a matched annotation will have a depth of 0, a meta-annotation
* 1 and a meta-meta-annotation 2
* @return the result of the processing or {@code null} to continue
*/
T process(Annotation annotation, int depth);
void postProcess(Annotation annotation, T result);
}
}

View File

@@ -16,8 +16,7 @@
package org.springframework.core.annotation;
import static java.lang.String.format;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -27,10 +26,9 @@ import org.springframework.util.StringUtils;
/**
* {@link LinkedHashMap} subclass representing annotation attribute key/value pairs
* as read by Spring's reflection- or ASM-based {@link
* org.springframework.core.type.AnnotationMetadata AnnotationMetadata} implementations.
* Provides 'pseudo-reification' to avoid noisy Map generics in the calling code as well
* as convenience methods for looking up annotation attributes in a type-safe fashion.
* as read by Spring's reflection- or ASM-based {@link org.springframework.core.type.AnnotationMetadata}
* implementations. Provides 'pseudo-reification' to avoid noisy Map generics in the calling code
* as well as convenience methods for looking up annotation attributes in a type-safe fashion.
*
* @author Chris Beams
* @since 3.1.1
@@ -63,24 +61,6 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
super(map);
}
/**
* Return an {@link AnnotationAttributes} instance based on the given map; if the map
* is already an {@code AnnotationAttributes} instance, it is casted and returned
* immediately without creating any new instance; otherwise create a new instance by
* wrapping the map with the {@link #AnnotationAttributes(Map)} constructor.
* @param map original source of annotation attribute key/value pairs
*/
public static AnnotationAttributes fromMap(Map<String, Object> map) {
if (map == null) {
return null;
}
if (map instanceof AnnotationAttributes) {
return (AnnotationAttributes) map;
}
return new AnnotationAttributes(map);
}
public String getString(String attributeName) {
return doGet(attributeName, String.class);
@@ -96,7 +76,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
@SuppressWarnings("unchecked")
public <N extends Number> N getNumber(String attributeName) {
return (N) doGet(attributeName, Integer.class);
return (N) doGet(attributeName, Number.class);
}
@SuppressWarnings("unchecked")
@@ -124,11 +104,20 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
@SuppressWarnings("unchecked")
private <T> T doGet(String attributeName, Class<T> expectedType) {
Assert.hasText(attributeName, "attributeName must not be null or empty");
Object value = this.get(attributeName);
Assert.notNull(value, format("Attribute '%s' not found", attributeName));
Assert.isAssignable(expectedType, value.getClass(),
format("Attribute '%s' is of type [%s], but [%s] was expected. Cause: ",
Object value = get(attributeName);
Assert.notNull(value, String.format("Attribute '%s' not found", attributeName));
if (!expectedType.isInstance(value)) {
if (expectedType.isArray() && expectedType.getComponentType().isInstance(value)) {
Object arrayValue = Array.newInstance(expectedType.getComponentType(), 1);
Array.set(arrayValue, 0, value);
value = arrayValue;
}
else {
throw new IllegalArgumentException(
String.format("Attribute '%s' is of type [%s], but [%s] was expected. Cause: ",
attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName()));
}
}
return (T) value;
}
@@ -156,4 +145,23 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
}
return String.valueOf(value);
}
/**
* Return an {@link AnnotationAttributes} instance based on the given map; if the map
* is already an {@code AnnotationAttributes} instance, it is casted and returned
* immediately without creating any new instance; otherwise create a new instance by
* wrapping the map with the {@link #AnnotationAttributes(Map)} constructor.
* @param map original source of annotation attribute key/value pairs
*/
public static AnnotationAttributes fromMap(Map<String, Object> map) {
if (map == null) {
return null;
}
if (map instanceof AnnotationAttributes) {
return (AnnotationAttributes) map;
}
return new AnnotationAttributes(map);
}
}

View File

@@ -1,165 +0,0 @@
/*
* Copyright 2002-2013 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.type;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Internal utility class used to collect all annotation values including those declared
* on meta-annotations.
*
* @author Phillip Webb
* @since 4.0
*/
class AnnotatedElementUtils {
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element,
String annotationType) {
final Set<String> types = new LinkedHashSet<String>();
process(element, annotationType, new Processor<Object>() {
@Override
public Object process(Annotation annotation, int depth) {
if (depth > 0) {
types.add(annotation.annotationType().getName());
}
return null;
}
});
return (types.size() == 0 ? null : types);
}
public static boolean hasMetaAnnotationTypes(AnnotatedElement element,
String annotationType) {
return Boolean.TRUE.equals(
process(element, annotationType, new Processor<Boolean>() {
@Override
public Boolean process(Annotation annotation, int depth) {
if (depth > 0) {
return true;
}
return null;
}
}));
}
public static boolean isAnnotated(AnnotatedElement element, String annotationType) {
return Boolean.TRUE.equals(
process(element, annotationType, new Processor<Boolean>() {
@Override
public Boolean process(Annotation annotation, int depth) {
return true;
}
}));
}
public static Map<String, Object> getAnnotationAttributes(AnnotatedElement element,
String annotationType, final boolean classValuesAsString,
final boolean nestedAnnotationsAsMap) {
return process(element, annotationType, new Processor<Map<String, Object>>() {
@Override
public Map<String, Object> process(Annotation annotation, int depth) {
return AnnotationUtils.getAnnotationAttributes(annotation,
classValuesAsString, nestedAnnotationsAsMap);
}
});
}
public static MultiValueMap<String, Object> getAllAnnotationAttributes(
AnnotatedElement element, final String annotationType,
final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
final MultiValueMap<String, Object> attributes = new LinkedMultiValueMap<String, Object>();
process(element, annotationType, new Processor<Object>() {
@Override
public Object process(Annotation annotation, int depth) {
if (annotation.annotationType().getName().equals(annotationType)) {
for (Map.Entry<String, Object> entry : AnnotationUtils.getAnnotationAttributes(
annotation, classValuesAsString, nestedAnnotationsAsMap).entrySet()) {
attributes.add(entry.getKey(), entry.getValue());
}
}
return null;
}
});
return (attributes.size() == 0 ? null : attributes);
}
/**
* Process all annotations of the specified annotation type and recursively all
* meta-annotations on the specified type.
* @param element the annotated element
* @param annotationType the annotation type to find. Only items of the specified type
* or meta-annotations of the specified type will be processed
* @param processor the processor
* @return the result of the processor
*/
private static <T> T process(AnnotatedElement element, String annotationType,
Processor<T> processor) {
return recursivelyProcess(element, annotationType, processor,
new HashSet<AnnotatedElement>(), 0);
}
private static <T> T recursivelyProcess(AnnotatedElement element,
String annotationType, Processor<T> processor, Set<AnnotatedElement> visited,
int depth) {
T result = null;
if (visited.add(element)) {
for (Annotation annotation : element.getAnnotations()) {
if (annotation.annotationType().getName().equals(annotationType) || depth > 0) {
result = result != null ? result :
processor.process(annotation, depth);
result = result != null ? result :
recursivelyProcess(annotation.annotationType(), annotationType,
processor, visited, depth + 1);
}
}
for (Annotation annotation : element.getAnnotations()) {
result = result != null ? result :
recursivelyProcess(annotation.annotationType(), annotationType,
processor, visited, depth);
}
}
return result;
}
/**
* Callback interface used to process an annotation.
* @param <T> the result type
*/
private static interface Processor<T> {
/**
* Called to process the annotation.
* @param annotation the annotation to process
* @param depth the depth of the annotation relative to the initial match. For
* example a matched annotation will have a depth of 0, a meta-annotation 1
* and a meta-meta-annotation 2
* @return the result of the processing or {@code null} to continue
*/
T process(Annotation annotation, int depth);
}
}

View File

@@ -22,7 +22,7 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.MultiValueMap;
/**
@@ -52,11 +52,12 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements
/**
* Create a new {@link StandardAnnotationMetadata} wrapper for the given Class,
* providing the option to return any nested annotations or annotation arrays in the
* form of {@link AnnotationAttributes} instead of actual {@link Annotation} instances.
* form of {@link org.springframework.core.annotation.AnnotationAttributes} instead
* of actual {@link Annotation} instances.
* @param introspectedClass the Class to instrospect
* @param nestedAnnotationsAsMap return nested annotations and annotation arrays as
* {@link AnnotationAttributes} for compatibility with ASM-based
* {@link AnnotationMetadata} implementations
* {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility
* with ASM-based {@link AnnotationMetadata} implementations
* @since 3.1.1
*/
public StandardAnnotationMetadata(Class<?> introspectedClass, boolean nestedAnnotationsAsMap) {
@@ -107,8 +108,7 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements
}
@Override
public Map<String, Object> getAnnotationAttributes(String annotationType,
boolean classValuesAsString) {
public Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString) {
return AnnotatedElementUtils.getAnnotationAttributes(getIntrospectedClass(),
annotationType, classValuesAsString, this.nestedAnnotationsAsMap);
}
@@ -119,8 +119,7 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements
}
@Override
public MultiValueMap<String, Object> getAllAnnotationAttributes(
String annotationType, boolean classValuesAsString) {
public MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString) {
return AnnotatedElementUtils.getAllAnnotationAttributes(getIntrospectedClass(),
annotationType, classValuesAsString, this.nestedAnnotationsAsMap);
}
@@ -141,7 +140,6 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements
Method[] methods = getIntrospectedClass().getDeclaredMethods();
Set<MethodMetadata> annotatedMethods = new LinkedHashSet<MethodMetadata>();
for (Method method : methods) {
// TODO: on OpenJDK 8 b99, bridge methods seem to be discovered as annotated as well...
if (AnnotatedElementUtils.isAnnotated(method, annotationType)) {
annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap));
}

View File

@@ -20,6 +20,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
@@ -49,9 +50,14 @@ public class StandardMethodMetadata implements MethodMetadata {
}
/**
* Create a new StandardMethodMetadata wrapper for the given Method.
* Create a new StandardMethodMetadata wrapper for the given Method,
* providing the option to return any nested annotations or annotation arrays in the
* form of {@link org.springframework.core.annotation.AnnotationAttributes} instead
* of actual {@link java.lang.annotation.Annotation} instances.
* @param introspectedMethod the Method to introspect
* @param nestedAnnotationsAsMap
* @param nestedAnnotationsAsMap return nested annotations and annotation arrays as
* {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility
* with ASM-based {@link AnnotationMetadata} implementations
* @since 3.1.1
*/
public StandardMethodMetadata(Method introspectedMethod, boolean nestedAnnotationsAsMap) {
@@ -115,8 +121,7 @@ public class StandardMethodMetadata implements MethodMetadata {
}
@Override
public MultiValueMap<String, Object> getAllAnnotationAttributes(
String annotationType, boolean classValuesAsString) {
public MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString) {
return AnnotatedElementUtils.getAllAnnotationAttributes(this.introspectedMethod,
annotationType, classValuesAsString, this.nestedAnnotationsAsMap);
}