Browse Source

Overhaul AnnotatedElementUtils

- Methods which search for a specific annotation now properly ensure
   that the sought annotation was actually found.

 - Both the "get" and the "find" search algorithms no longer needlessly
   traverse meta-annotation hierarchies twice.

 - Both the "get" and the "find" search algorithms now properly
   increment the metaDepth when recursively searching within the
   meta-annotation hierarchy.

 - Redesigned getMetaAnnotationTypes() so that it doesn't needlessly
   search irrelevant annotations.

 - Documented and tested hasMetaAnnotationTypes().

 - Documented isAnnotated().

Issue: SPR-11514
pull/791/head
Sam Brannen 11 years ago
parent
commit
ba84458c65
  1. 105
      spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java
  2. 4
      spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java
  3. 55
      spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
  4. 30
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java

105
spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java

@ -45,14 +45,16 @@ public class AnnotatedElementUtils { @@ -45,14 +45,16 @@ public class AnnotatedElementUtils {
* <em>present</em> on the annotation (of the specified
* {@code annotationType}) on the supplied {@link AnnotatedElement}.
*
* <p>This method also finds all meta-annotations in the annotation
* hierarchy above the specified annotation.
* <p>This method finds all meta-annotations in the annotation hierarchy
* above the specified annotation.
*
* @param element the annotated element; never {@code null}
* @param annotationType the annotation type on which to find
* meta-annotations; never {@code null}
* @return the names of all meta-annotations present on the annotation,
* or {@code null} if not found
* @see #getMetaAnnotationTypes(AnnotatedElement, String)
* @see #hasMetaAnnotationTypes
*/
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element,
Class<? extends Annotation> annotationType) {
@ -65,14 +67,16 @@ public class AnnotatedElementUtils { @@ -65,14 +67,16 @@ public class AnnotatedElementUtils {
* <em>present</em> on the annotation (of the specified
* {@code annotationType}) on the supplied {@link AnnotatedElement}.
*
* <p>This method also finds all meta-annotations in the annotation
* hierarchy above the specified annotation.
* <p>This method finds all meta-annotations in the annotation hierarchy
* above the specified annotation.
*
* @param element the annotated element; never {@code null}
* @param annotationType the fully qualified class name of the annotation
* type on which to find meta-annotations; never {@code null} or empty
* @return the names of all meta-annotations present on the annotation,
* or {@code null} if not found
* @see #getMetaAnnotationTypes(AnnotatedElement, Class)
* @see #hasMetaAnnotationTypes
*/
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element, String annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null");
@ -80,32 +84,49 @@ public class AnnotatedElementUtils { @@ -80,32 +84,49 @@ public class AnnotatedElementUtils {
final Set<String> types = new LinkedHashSet<String>();
processWithGetSemantics(element, annotationType, new SimpleAnnotationProcessor<Object>() {
@Override
public Object process(Annotation annotation, int metaDepth) {
if (metaDepth > 0) {
types.add(annotation.annotationType().getName());
}
return null;
try {
Annotation annotation = getAnnotation(element, annotationType);
if (annotation != null) {
processWithGetSemantics(annotation.annotationType(), annotationType, new SimpleAnnotationProcessor<Object>() {
@Override
public Object process(Annotation annotation, int metaDepth) {
types.add(annotation.annotationType().getName());
return null;
}
}, new HashSet<AnnotatedElement>(), 1);
}
});
}
catch (Throwable ex) {
throw new IllegalStateException("Failed to introspect annotations on " + element, ex);
}
return (types.isEmpty() ? null : types);
}
/**
* Determine if the supplied {@link AnnotatedElement} is annotated with
* a <em>composed annotation</em> that is meta-annotated with an
* annotation of the specified {@code annotationType}.
*
* <p>This method finds all meta-annotations in the annotation hierarchy
* above the specified element.
*
* @param element the annotated element; never {@code null}
* @param annotationType the fully qualified class name of the annotation
* @param annotationType the fully qualified class name of the meta-annotation
* type to find; never {@code null} or empty
* @return {@code true} if a matching meta-annotation is present
* @see #getMetaAnnotationTypes
*/
public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationType) {
public static boolean hasMetaAnnotationTypes(AnnotatedElement element, final String annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null");
Assert.hasText(annotationType, "annotationType must not be null or empty");
return Boolean.TRUE.equals(processWithGetSemantics(element, annotationType, new SimpleAnnotationProcessor<Boolean>() {
@Override
public Boolean process(Annotation annotation, int metaDepth) {
if (metaDepth > 0) {
boolean found = annotation.annotationType().getName().equals(annotationType);
if (found && (metaDepth > 0)) {
return Boolean.TRUE;
}
return null;
@ -114,18 +135,27 @@ public class AnnotatedElementUtils { @@ -114,18 +135,27 @@ public class AnnotatedElementUtils {
}
/**
* Determine if an annotation of the specified {@code annotationType}
* is <em>present</em> on the supplied {@link AnnotatedElement} or
* within the annotation hierarchy above the specified element.
*
* <p>If this method returns {@code true}, then {@link #getAnnotationAttributes}
* will return a non-null value.
*
* @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 {@code true} if a matching annotation is present
*/
public static boolean isAnnotated(AnnotatedElement element, String annotationType) {
public static boolean isAnnotated(AnnotatedElement element, final String annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null");
Assert.hasText(annotationType, "annotationType must not be null or empty");
return Boolean.TRUE.equals(processWithGetSemantics(element, annotationType, new SimpleAnnotationProcessor<Boolean>() {
@Override
public Boolean process(Annotation annotation, int metaDepth) {
return Boolean.TRUE;
boolean found = annotation.annotationType().getName().equals(annotationType);
return (found ? Boolean.TRUE : null);
}
}));
}
@ -167,7 +197,7 @@ public class AnnotatedElementUtils { @@ -167,7 +197,7 @@ public class AnnotatedElementUtils {
*/
public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationType,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
return processWithGetSemantics(element, annotationType, new MergeAnnotationAttributesProcessor(
return processWithGetSemantics(element, annotationType, new MergeAnnotationAttributesProcessor(annotationType,
classValuesAsString, nestedAnnotationsAsMap));
}
@ -269,7 +299,7 @@ public class AnnotatedElementUtils { @@ -269,7 +299,7 @@ public class AnnotatedElementUtils {
return processWithFindSemantics(element, annotationType, searchOnInterfaces, searchOnSuperclasses,
searchOnMethodsInInterfaces, searchOnMethodsInSuperclasses, new MergeAnnotationAttributesProcessor(
classValuesAsString, nestedAnnotationsAsMap));
annotationType, classValuesAsString, nestedAnnotationsAsMap));
}
/**
@ -376,21 +406,12 @@ public class AnnotatedElementUtils { @@ -376,21 +406,12 @@ public class AnnotatedElementUtils {
// Search in local annotations
for (Annotation annotation : annotations) {
// TODO Add check for !isInJavaLangAnnotationPackage()
// if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)
// && (annotation.annotationType().getName().equals(annotationType) ||
// metaDepth > 0)) {
// TODO Test for !AnnotationUtils.isInJavaLangAnnotationPackage(annotation)
if (annotation.annotationType().getName().equals(annotationType) || metaDepth > 0) {
T result = processor.process(annotation, metaDepth);
if (result != null) {
return result;
}
result = processWithGetSemantics(annotation.annotationType(), annotationType, processor,
visited, metaDepth + 1);
if (result != null) {
processor.postProcess(annotation, result);
return result;
}
}
}
@ -398,7 +419,7 @@ public class AnnotatedElementUtils { @@ -398,7 +419,7 @@ public class AnnotatedElementUtils {
for (Annotation annotation : annotations) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
T result = processWithGetSemantics(annotation.annotationType(), annotationType, processor,
visited, metaDepth);
visited, metaDepth + 1);
if (result != null) {
processor.postProcess(annotation, result);
return result;
@ -492,13 +513,6 @@ public class AnnotatedElementUtils { @@ -492,13 +513,6 @@ public class AnnotatedElementUtils {
if (result != null) {
return result;
}
result = processWithFindSemantics(annotation.annotationType(), annotationType,
searchOnInterfaces, searchOnSuperclasses, searchOnMethodsInInterfaces,
searchOnMethodsInSuperclasses, processor, visited, metaDepth + 1);
if (result != null) {
processor.postProcess(annotation, result);
return result;
}
}
}
@ -507,7 +521,7 @@ public class AnnotatedElementUtils { @@ -507,7 +521,7 @@ public class AnnotatedElementUtils {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
T result = processWithFindSemantics(annotation.annotationType(), annotationType,
searchOnInterfaces, searchOnSuperclasses, searchOnMethodsInInterfaces,
searchOnMethodsInSuperclasses, processor, visited, metaDepth);
searchOnMethodsInSuperclasses, processor, visited, metaDepth + 1);
if (result != null) {
processor.postProcess(annotation, result);
return result;
@ -636,6 +650,14 @@ public class AnnotatedElementUtils { @@ -636,6 +650,14 @@ public class AnnotatedElementUtils {
return null;
}
private static Annotation getAnnotation(AnnotatedElement element, String annotationType) {
for (Annotation annotation : element.getAnnotations()) {
if (annotation.annotationType().getName().equals(annotationType)) {
return annotation;
}
}
return null;
}
/**
* Callback interface that is used to process a target annotation (or
@ -707,18 +729,21 @@ public class AnnotatedElementUtils { @@ -707,18 +729,21 @@ public class AnnotatedElementUtils {
*/
private static class MergeAnnotationAttributesProcessor implements Processor<AnnotationAttributes> {
private final String annotationType;
private final boolean classValuesAsString;
private final boolean nestedAnnotationsAsMap;
MergeAnnotationAttributesProcessor(boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
MergeAnnotationAttributesProcessor(String annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
this.annotationType = annotationType;
this.classValuesAsString = classValuesAsString;
this.nestedAnnotationsAsMap = nestedAnnotationsAsMap;
}
@Override
public AnnotationAttributes process(Annotation annotation, int metaDepth) {
return AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap);
boolean found = annotation.annotationType().getName().equals(annotationType);
return (!found ? null : AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap));
}
@Override

4
spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java

@ -1,5 +1,5 @@ @@ -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.
@ -28,7 +28,7 @@ import org.springframework.util.ClassUtils; @@ -28,7 +28,7 @@ import org.springframework.util.ClassUtils;
* A simple filter which matches classes with a given annotation,
* checking inherited annotations as well.
*
* <p>The matching logic mirrors that of {@code Class.isAnnotationPresent()}.
* <p>The matching logic mirrors that of {@link java.lang.Class#isAnnotationPresent(Class)}.
*
* @author Mark Fisher
* @author Ramnivas Laddad

55
spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java

@ -62,11 +62,33 @@ public class AnnotatedElementUtilsTests { @@ -62,11 +62,33 @@ public class AnnotatedElementUtilsTests {
@Test
public void getMetaAnnotationTypesOnClassWithMetaDepth2() {
Set<String> names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class,
ComposedTransactionalComponent.class);
Set<String> names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class);
assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class, Retention.class, Documented.class, Target.class, Inherited.class), names);
}
@Test
public void hasMetaAnnotationTypesOnNonAnnotatedClass() {
assertFalse(hasMetaAnnotationTypes(NonAnnotatedClass.class, Transactional.class.getName()));
}
@Test
public void hasMetaAnnotationTypesOnClassWithMetaDepth0() {
assertFalse(hasMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class.getName()));
}
@Test
public void hasMetaAnnotationTypesOnClassWithMetaDepth1() {
assertTrue(hasMetaAnnotationTypes(TransactionalComponentClass.class, Transactional.class.getName()));
assertTrue(hasMetaAnnotationTypes(TransactionalComponentClass.class, Component.class.getName()));
}
@Test
public void hasMetaAnnotationTypesOnClassWithMetaDepth2() {
assertTrue(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, Transactional.class.getName()));
assertTrue(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, Component.class.getName()));
assertFalse(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName()));
}
@Test
public void getAllAnnotationAttributesOnClassWithLocalAnnotation() {
MultiValueMap<String, Object> attributes = getAllAnnotationAttributes(TxConfig.class,
@ -277,6 +299,14 @@ public class AnnotatedElementUtilsTests { @@ -277,6 +299,14 @@ public class AnnotatedElementUtilsTests {
assertNotNull("Should find @Order on StringGenericParameter.getFor() bridge method", attributes);
}
/** @since 4.2 */
@Test
public void findAnnotationAttributesOnClassWithMetaAndLocalTxConfig() {
AnnotationAttributes attributes = findAnnotationAttributes(MetaAndLocalTxConfigClass.class, Transactional.class);
assertNotNull("Should find @Transactional on MetaAndLocalTxConfigClass", attributes);
assertEquals("TX qualifier for MetaAndLocalTxConfigClass.", "localTxMgr", attributes.getString("qualifier"));
}
// -------------------------------------------------------------------------
@ -315,6 +345,8 @@ public class AnnotatedElementUtilsTests { @@ -315,6 +345,8 @@ public class AnnotatedElementUtilsTests {
String value() default "";
String qualifier() default "transactionManager";
boolean readOnly() default false;
}
@ -333,6 +365,13 @@ public class AnnotatedElementUtilsTests { @@ -333,6 +365,13 @@ public class AnnotatedElementUtilsTests {
@interface Composed2 {
}
@Transactional
@Retention(RetentionPolicy.RUNTIME)
@interface TxComposedWithOverride {
String qualifier() default "txMgr";
}
@Transactional("TxComposed1")
@Retention(RetentionPolicy.RUNTIME)
@interface TxComposed1 {
@ -354,6 +393,14 @@ public class AnnotatedElementUtilsTests { @@ -354,6 +393,14 @@ public class AnnotatedElementUtilsTests {
@interface ComposedTransactionalComponent {
}
@TxComposedWithOverride
// Override default "txMgr" from @TxComposedWithOverride with "localTxMgr"
@Transactional(qualifier = "localTxMgr")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MetaAndLocalTxConfig {
}
// -------------------------------------------------------------------------
static class NonAnnotatedClass {
@ -389,6 +436,10 @@ public class AnnotatedElementUtilsTests { @@ -389,6 +436,10 @@ public class AnnotatedElementUtilsTests {
static class SubSubClassWithInheritedComposedAnnotation extends SubClassWithInheritedComposedAnnotation {
}
@MetaAndLocalTxConfig
static class MetaAndLocalTxConfigClass {
}
@Transactional("TxConfig")
static class TxConfig {
}

30
spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java

@ -1,5 +1,5 @@ @@ -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.
@ -18,18 +18,14 @@ package org.springframework.web.servlet.config.annotation; @@ -18,18 +18,14 @@ package org.springframework.web.servlet.config.annotation;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.joda.time.DateTime;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
@ -83,14 +79,21 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; @@ -83,14 +79,21 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.ViewResolverComposite;
import org.springframework.web.util.UrlPathHelper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import static org.junit.Assert.*;
/**
* A test fixture with an {@link WebMvcConfigurationSupport} instance.
* Integration tests for {@link WebMvcConfigurationSupport} (imported via
* {@link EnableWebMvc @EnableWebMvc}).
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @author Sam Brannen
*/
public class WebMvcConfigurationSupportTests {
@ -106,10 +109,10 @@ public class WebMvcConfigurationSupportTests { @@ -106,10 +109,10 @@ public class WebMvcConfigurationSupportTests {
assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[0].getClass());
chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/scoped"));
assertNotNull(chain);
assertNotNull("HandlerExecutionChain for '/scoped' mapping should not be null.", chain);
chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/scopedProxy"));
assertNotNull(chain);
assertNotNull("HandlerExecutionChain for '/scopedProxy' mapping should not be null.", chain);
}
@Test
@ -207,6 +210,7 @@ public class WebMvcConfigurationSupportTests { @@ -207,6 +210,7 @@ public class WebMvcConfigurationSupportTests {
}
@Test
@SuppressWarnings("unchecked")
public void handlerExceptionResolver() throws Exception {
ApplicationContext context = initContext(WebConfig.class);
HandlerExceptionResolverComposite compositeResolver =
@ -267,9 +271,6 @@ public class WebMvcConfigurationSupportTests { @@ -267,9 +271,6 @@ public class WebMvcConfigurationSupportTests {
ApplicationContext context = initContext(CustomViewResolverOrderConfig.class);
ViewResolverComposite resolver = context.getBean("mvcViewResolver", ViewResolverComposite.class);
Map<String, ViewResolver> map = BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, ViewResolver.class, true, false);
assertNotNull(resolver);
assertEquals(1, resolver.getViewResolvers().size());
assertEquals(InternalResourceViewResolver.class, resolver.getViewResolvers().get(0).getClass());
@ -287,8 +288,7 @@ public class WebMvcConfigurationSupportTests { @@ -287,8 +288,7 @@ public class WebMvcConfigurationSupportTests {
assertEquals(AntPathMatcher.class, pathMatcher.getClass());
}
private ApplicationContext initContext(Class... configClasses) {
private ApplicationContext initContext(Class<?>... configClasses) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.setServletContext(new MockServletContext());
context.register(configClasses);

Loading…
Cancel
Save