diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index ae68cab903bb21fefdb43e586bec031dad9eb04c..625401c6d7fc4794e3a3fb007fb04217a615c5fb 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.UsesJava8; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -237,6 +238,8 @@ public class TypeDescriptor implements Serializable { /** * Determine if this type descriptor has the specified annotation. + *

As of Spring Framework 4.2, this method supports arbitrary levels + * of meta-annotations. * @param annotationType the annotation type * @return true if the annotation is present */ @@ -245,19 +248,27 @@ public class TypeDescriptor implements Serializable { } /** - * Obtain the annotation associated with this type descriptor of the specified type. + * Obtain the annotation of the specified {@code annotationType} that is + * on this type descriptor. + *

As of Spring Framework 4.2, this method supports arbitrary levels + * of meta-annotations. * @param annotationType the annotation type * @return the annotation, or {@code null} if no such annotation exists on this type descriptor */ @SuppressWarnings("unchecked") public T getAnnotation(Class annotationType) { + // Search in annotations that are "present" (i.e., locally declared or inherited) + // + // NOTE: this unfortunately favors inherited annotations over locally declared composed annotations. for (Annotation annotation : getAnnotations()) { if (annotation.annotationType().equals(annotationType)) { return (T) annotation; } } - for (Annotation metaAnn : getAnnotations()) { - T ann = metaAnn.annotationType().getAnnotation(annotationType); + + // Search in annotation hierarchy + for (Annotation composedAnnotation : getAnnotations()) { + T ann = AnnotationUtils.findAnnotation(composedAnnotation.annotationType(), annotationType); if (ann != null) { return ann; } diff --git a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java index ab750a44ca87406251dfcae76b9102e9d53a29ca..61f30c31430926178d9205acfb6961f04912c5ee 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -50,6 +51,7 @@ import static org.junit.Assert.*; * @author Keith Donald * @author Andy Clement * @author Phillip Webb + * @author Sam Brannen */ @SuppressWarnings("rawtypes") public class TypeDescriptorTests { @@ -369,22 +371,60 @@ public class TypeDescriptorTests { @MethodAnnotation3 private Map, List> property; - @Target({ElementType.METHOD}) + + @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface MethodAnnotation1 { - } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface MethodAnnotation2 { - } @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface MethodAnnotation3 { + } + + @MethodAnnotation1 + @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface ComposedMethodAnnotation1 {} + + @ComposedMethodAnnotation1 + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ComposedComposedMethodAnnotation1 {} + + @MethodAnnotation1 + public void methodWithLocalAnnotation() {} + @ComposedMethodAnnotation1 + public void methodWithComposedAnnotation() {} + + @ComposedComposedMethodAnnotation1 + public void methodWithComposedComposedAnnotation() {} + + private void assertAnnotationFoundOnMethod(Class annotationType, String methodName) throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(getClass().getMethod(methodName), -1)); + assertNotNull("Should have found @" + annotationType.getSimpleName() + " on " + methodName + ".", + typeDescriptor.getAnnotation(annotationType)); + } + + @Test + public void getAnnotationOnMethodThatIsLocallyAnnotated() throws Exception { + assertAnnotationFoundOnMethod(MethodAnnotation1.class, "methodWithLocalAnnotation"); + } + + @Test + public void getAnnotationOnMethodThatIsMetaAnnotated() throws Exception { + assertAnnotationFoundOnMethod(MethodAnnotation1.class, "methodWithComposedAnnotation"); + } + + @Test + public void getAnnotationOnMethodThatIsMetaMetaAnnotated() throws Exception { + assertAnnotationFoundOnMethod(MethodAnnotation1.class, "methodWithComposedComposedAnnotation"); } @Test