Find annotations on implemented generic superclass methods as well

Includes Java 8 getDeclaredAnnotation shortcut for lookup on Class.

Issue: SPR-17146
This commit is contained in:
Juergen Hoeller
2018-08-08 23:52:47 +02:00
parent fa72186e28
commit 4521a79b2d
4 changed files with 92 additions and 78 deletions

View File

@@ -386,13 +386,14 @@ public abstract class AnnotatedElementUtils {
@Nullable @Nullable
public static <A extends Annotation> A getMergedAnnotation(AnnotatedElement element, Class<A> annotationType) { public static <A extends Annotation> A getMergedAnnotation(AnnotatedElement element, Class<A> annotationType) {
// Shortcut: directly present on the element, with no merging needed? // Shortcut: directly present on the element, with no merging needed?
if (!(element instanceof Class)) { A annotation = element.getDeclaredAnnotation(annotationType);
// Do not use this shortcut against a Class: Inherited annotations if (annotation != null) {
// would get preferred over locally declared composed annotations. return AnnotationUtils.synthesizeAnnotation(annotation, element);
A annotation = element.getAnnotation(annotationType); }
if (annotation != null) {
return AnnotationUtils.synthesizeAnnotation(annotation, element); // Shortcut: no non-java annotations to be found on plain Java classes and org.springframework.lang types...
} if (AnnotationUtils.hasPlainJavaAnnotationsOnly(element) && !annotationType.getName().startsWith("java")) {
return null;
} }
// Exhaustive retrieval of merged annotation attributes... // Exhaustive retrieval of merged annotation attributes...
@@ -671,13 +672,9 @@ public abstract class AnnotatedElementUtils {
@Nullable @Nullable
public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> annotationType) { public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> annotationType) {
// Shortcut: directly present on the element, with no merging needed? // Shortcut: directly present on the element, with no merging needed?
if (!(element instanceof Class)) { A annotation = element.getDeclaredAnnotation(annotationType);
// Do not use this shortcut against a Class: Inherited annotations if (annotation != null) {
// would get preferred over locally declared composed annotations. return AnnotationUtils.synthesizeAnnotation(annotation, element);
A annotation = element.getAnnotation(annotationType);
if (annotation != null) {
return AnnotationUtils.synthesizeAnnotation(annotation, element);
}
} }
// Shortcut: no non-java annotations to be found on plain Java classes and org.springframework.lang types... // Shortcut: no non-java annotations to be found on plain Java classes and org.springframework.lang types...
@@ -1145,8 +1142,7 @@ public abstract class AnnotatedElementUtils {
Set<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethodsInBaseType(clazz); Set<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethodsInBaseType(clazz);
if (!annotatedMethods.isEmpty()) { if (!annotatedMethods.isEmpty()) {
for (Method annotatedMethod : annotatedMethods) { for (Method annotatedMethod : annotatedMethods) {
if (annotatedMethod.getName().equals(method.getName()) && if (AnnotationUtils.isOverride(method, annotatedMethod)) {
Arrays.equals(annotatedMethod.getParameterTypes(), method.getParameterTypes())) {
Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod); Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod);
result = searchWithFindSemantics(resolvedSuperMethod, annotationType, annotationName, result = searchWithFindSemantics(resolvedSuperMethod, annotationType, annotationName,
containerType, processor, visited, metaDepth); containerType, processor, visited, metaDepth);
@@ -1203,8 +1199,7 @@ public abstract class AnnotatedElementUtils {
Set<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethodsInBaseType(ifc); Set<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethodsInBaseType(ifc);
if (!annotatedMethods.isEmpty()) { if (!annotatedMethods.isEmpty()) {
for (Method annotatedMethod : annotatedMethods) { for (Method annotatedMethod : annotatedMethods) {
if (annotatedMethod.getName().equals(method.getName()) && if (AnnotationUtils.isOverride(method, annotatedMethod)) {
Arrays.equals(annotatedMethod.getParameterTypes(), method.getParameterTypes())) {
T result = searchWithFindSemantics(annotatedMethod, annotationType, annotationName, T result = searchWithFindSemantics(annotatedMethod, annotationType, annotationName,
containerType, processor, visited, metaDepth); containerType, processor, visited, metaDepth);
if (result != null) { if (result != null) {
@@ -1280,7 +1275,7 @@ public abstract class AnnotatedElementUtils {
/** /**
* Validate that the supplied {@code containerType} is a proper container * Validate that the supplied {@code containerType} is a proper container
* annotation for the supplied repeatable {@code annotationType} (i.e., * annotation for the supplied repeatable {@code annotationType} (i.e.
* that it declares a {@code value} attribute that holds an array of the * that it declares a {@code value} attribute that holds an array of the
* {@code annotationType}). * {@code annotationType}).
* @throws AnnotationConfigurationException if the supplied {@code containerType} * @throws AnnotationConfigurationException if the supplied {@code containerType}
@@ -1322,27 +1317,24 @@ public abstract class AnnotatedElementUtils {
/** /**
* Callback interface that is used to process annotations during a search. * Callback interface that is used to process annotations during a search.
* <p>Depending on the use case, a processor may choose to * <p>Depending on the use case, a processor may choose to {@linkplain #process}
* {@linkplain #process} a single target annotation, multiple target * a single target annotation, multiple target annotations, or all annotations
* annotations, or all annotations discovered by the currently executing * discovered by the currently executing search. The term "target" in this
* search. The term "target" in this context refers to a matching * context refers to a matching annotation (i.e. a specific annotation type
* annotation (i.e., a specific annotation type that was found during * that was found during the search).
* the search). * <p>Returning a non-null value from the {@link #process} method instructs
* <p>Returning a non-null value from the {@link #process} * the search algorithm to stop searching further; whereas, returning
* method instructs the search algorithm to stop searching further; * {@code null} from the {@link #process} method instructs the search
* whereas, returning {@code null} from the {@link #process} method * algorithm to continue searching for additional annotations. One exception
* instructs the search algorithm to continue searching for additional * to this rule applies to processors that {@linkplain #aggregates aggregate}
* annotations. One exception to this rule applies to processors * results. If an aggregating processor returns a non-null value, that value
* that {@linkplain #aggregates aggregate} results. If an aggregating * will be added to the {@linkplain #getAggregatedResults aggregated results}
* processor returns a non-null value, that value will be added to the
* list of {@linkplain #getAggregatedResults aggregated results}
* and the search algorithm will continue. * and the search algorithm will continue.
* <p>Processors can optionally {@linkplain #postProcess post-process} * <p>Processors can optionally {@linkplain #postProcess post-process} the
* the result of the {@link #process} method as the search algorithm * result of the {@link #process} method as the search algorithm goes back
* goes back down the annotation hierarchy from an invocation of * down the annotation hierarchy from an invocation of {@link #process} that
* {@link #process} that returned a non-null value down to the * returned a non-null value down to the {@link AnnotatedElement} that was
* {@link AnnotatedElement} that was supplied as the starting point to * supplied as the starting point to the search algorithm.
* the search algorithm.
* @param <T> the type of result returned by the processor * @param <T> the type of result returned by the processor
*/ */
private interface Processor<T> { private interface Processor<T> {

View File

@@ -520,7 +520,7 @@ public abstract class AnnotationUtils {
/** /**
* Find a single {@link Annotation} of {@code annotationType} on the supplied * Find a single {@link Annotation} of {@code annotationType} on the supplied
* {@link Method}, traversing its super methods (i.e., from superclasses and * {@link Method}, traversing its super methods (i.e. from superclasses and
* interfaces) if the annotation is not <em>directly present</em> on the given * interfaces) if the annotation is not <em>directly present</em> on the given
* method itself. * method itself.
* <p>Correctly handles bridge {@link Method Methods} generated by the compiler. * <p>Correctly handles bridge {@link Method Methods} generated by the compiler.
@@ -560,8 +560,7 @@ public abstract class AnnotationUtils {
Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(clazz); Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(clazz);
if (!annotatedMethods.isEmpty()) { if (!annotatedMethods.isEmpty()) {
for (Method annotatedMethod : annotatedMethods) { for (Method annotatedMethod : annotatedMethods) {
if (annotatedMethod.getName().equals(method.getName()) && if (isOverride(method, annotatedMethod)) {
Arrays.equals(annotatedMethod.getParameterTypes(), method.getParameterTypes())) {
Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod); Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod);
result = findAnnotation((AnnotatedElement) resolvedSuperMethod, annotationType); result = findAnnotation((AnnotatedElement) resolvedSuperMethod, annotationType);
if (result != null) { if (result != null) {
@@ -650,7 +649,7 @@ public abstract class AnnotationUtils {
return false; return false;
} }
private static boolean isOverride(Method method, Method candidate) { static boolean isOverride(Method method, Method candidate) {
if (!candidate.getName().equals(method.getName()) || if (!candidate.getName().equals(method.getName()) ||
candidate.getParameterCount() != method.getParameterCount()) { candidate.getParameterCount() != method.getParameterCount()) {
return false; return false;
@@ -843,7 +842,7 @@ public abstract class AnnotationUtils {
/** /**
* Determine whether an annotation of the specified {@code annotationType} * Determine whether an annotation of the specified {@code annotationType}
* is declared locally (i.e., <em>directly present</em>) on the supplied * is declared locally (i.e. <em>directly present</em>) on the supplied
* {@code clazz}. * {@code clazz}.
* <p>The supplied {@link Class} may represent any type. * <p>The supplied {@link Class} may represent any type.
* <p>Meta-annotations will <em>not</em> be searched. * <p>Meta-annotations will <em>not</em> be searched.
@@ -872,8 +871,8 @@ public abstract class AnnotationUtils {
/** /**
* Determine whether an annotation of the specified {@code annotationType} * Determine whether an annotation of the specified {@code annotationType}
* is <em>present</em> on the supplied {@code clazz} and is * is <em>present</em> on the supplied {@code clazz} and is
* {@linkplain java.lang.annotation.Inherited inherited} (i.e., not * {@linkplain java.lang.annotation.Inherited inherited}
* <em>directly present</em>). * (i.e. not <em>directly present</em>).
* <p>Meta-annotations will <em>not</em> be searched. * <p>Meta-annotations will <em>not</em> be searched.
* <p>If the supplied {@code clazz} is an interface, only the interface * <p>If the supplied {@code clazz} is an interface, only the interface
* itself will be checked. In accordance with standard meta-annotation * itself will be checked. In accordance with standard meta-annotation
@@ -1681,10 +1680,10 @@ public abstract class AnnotationUtils {
* in the supplied annotation type. * in the supplied annotation type.
* <p>The map is keyed by attribute name with each value representing * <p>The map is keyed by attribute name with each value representing
* a list of names of aliased attributes. * a list of names of aliased attributes.
* <p>For <em>explicit</em> alias pairs such as x and y (i.e., where x * <p>For <em>explicit</em> alias pairs such as x and y (i.e. where x
* is an {@code @AliasFor("y")} and y is an {@code @AliasFor("x")}, there * is an {@code @AliasFor("y")} and y is an {@code @AliasFor("x")}, there
* will be two entries in the map: {@code x -> (y)} and {@code y -> (x)}. * will be two entries in the map: {@code x -> (y)} and {@code y -> (x)}.
* <p>For <em>implicit</em> aliases (i.e., attributes that are declared * <p>For <em>implicit</em> aliases (i.e. attributes that are declared
* as attribute overrides for the same attribute in the same meta-annotation), * as attribute overrides for the same attribute in the same meta-annotation),
* there will be n entries in the map. For example, if x, y, and z are * there will be n entries in the map. For example, if x, y, and z are
* implicit aliases, the map will contain the following entries: * implicit aliases, the map will contain the following entries:
@@ -1733,7 +1732,7 @@ public abstract class AnnotationUtils {
/** /**
* Determine if annotations of the supplied {@code annotationType} are * Determine if annotations of the supplied {@code annotationType} are
* <em>synthesizable</em> (i.e., in need of being wrapped in a dynamic * <em>synthesizable</em> (i.e. in need of being wrapped in a dynamic
* proxy that provides functionality above that of a standard JDK * proxy that provides functionality above that of a standard JDK
* annotation). * annotation).
* <p>Specifically, an annotation is <em>synthesizable</em> if it declares * <p>Specifically, an annotation is <em>synthesizable</em> if it declares

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -525,19 +525,11 @@ public class AnnotatedElementUtilsTests {
assertNotNull("Should find @Transactional on ConcreteClassWithInheritedAnnotation.handle() method", attributes); assertNotNull("Should find @Transactional on ConcreteClassWithInheritedAnnotation.handle() method", attributes);
} }
/**
* <p>{@code AbstractClassWithInheritedAnnotation} declares {@code handleParameterized(T)}; whereas,
* {@code ConcreteClassWithInheritedAnnotation} declares {@code handleParameterized(String)}.
* <p>As of Spring 4.2, {@code AnnotatedElementUtils.processWithFindSemantics()} does not resolve an
* <em>equivalent</em> method in {@code AbstractClassWithInheritedAnnotation} for the <em>bridged</em>
* {@code handleParameterized(String)} method.
* @since 4.2
*/
@Test @Test
public void findMergedAnnotationAttributesInheritedFromBridgedMethod() throws NoSuchMethodException { public void findMergedAnnotationAttributesInheritedFromBridgedMethod() throws NoSuchMethodException {
Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleParameterized", String.class); Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleParameterized", String.class);
AnnotationAttributes attributes = findMergedAnnotationAttributes(method, Transactional.class); AnnotationAttributes attributes = findMergedAnnotationAttributes(method, Transactional.class);
assertNull("Should not find @Transactional on bridged ConcreteClassWithInheritedAnnotation.handleParameterized()", attributes); assertNotNull("Should find @Transactional on bridged ConcreteClassWithInheritedAnnotation.handleParameterized()", attributes);
} }
/** /**
@@ -546,7 +538,7 @@ public class AnnotatedElementUtilsTests {
* @since 4.2 * @since 4.2
*/ */
@Test @Test
public void findMergedAnnotationAttributesFromBridgeMethod() throws NoSuchMethodException { public void findMergedAnnotationAttributesFromBridgeMethod() {
Method[] methods = StringGenericParameter.class.getMethods(); Method[] methods = StringGenericParameter.class.getMethods();
Method bridgeMethod = null; Method bridgeMethod = null;
Method bridgedMethod = null; Method bridgedMethod = null;
@@ -733,6 +725,20 @@ public class AnnotatedElementUtilsTests {
assertEquals(1, allMergedAnnotations.size()); assertEquals(1, allMergedAnnotations.size());
} }
@Test // SPR-16060
public void findMethodAnnotationFromGenericInterface() throws Exception {
Method method = ImplementsInterfaceWithGenericAnnotatedMethod.class.getMethod("foo", String.class);
Order order = findMergedAnnotation(method, Order.class);
assertNotNull(order);
}
@Test // SPR-17146
public void findMethodAnnotationFromGenericSuperclass() throws Exception {
Method method = ExtendsBaseClassWithGenericAnnotatedMethod.class.getMethod("foo", String.class);
Order order = findMergedAnnotation(method, Order.class);
assertNotNull(order);
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View File

@@ -167,9 +167,7 @@ public class AnnotationUtilsTests {
assertNull(bridgedMethod.getAnnotation(Order.class)); assertNull(bridgedMethod.getAnnotation(Order.class));
assertNull(getAnnotation(bridgedMethod, Order.class)); assertNull(getAnnotation(bridgedMethod, Order.class));
// AnnotationUtils.findAnnotation(Method, Class<A>) will not find an annotation on assertNotNull(findAnnotation(bridgedMethod, Order.class));
// the bridge method for a bridged method.
assertNull(findAnnotation(bridgedMethod, Order.class));
assertNotNull(bridgedMethod.getAnnotation(Transactional.class)); assertNotNull(bridgedMethod.getAnnotation(Transactional.class));
assertNotNull(getAnnotation(bridgedMethod, Transactional.class)); assertNotNull(getAnnotation(bridgedMethod, Transactional.class));
@@ -190,6 +188,13 @@ public class AnnotationUtilsTests {
assertNotNull(order); assertNotNull(order);
} }
@Test // SPR-17146
public void findMethodAnnotationFromGenericSuperclass() throws Exception {
Method method = ExtendsBaseClassWithGenericAnnotatedMethod.class.getMethod("foo", String.class);
Order order = findAnnotation(method, Order.class);
assertNotNull(order);
}
@Test @Test
public void findMethodAnnotationFromInterfaceOnSuper() throws Exception { public void findMethodAnnotationFromInterfaceOnSuper() throws Exception {
Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo");
@@ -204,7 +209,7 @@ public class AnnotationUtilsTests {
assertNotNull(order); assertNotNull(order);
} }
/** @since 4.1.2 */ // @since 4.1.2
@Test @Test
public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() { public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() {
Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class); Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class);
@@ -212,7 +217,7 @@ public class AnnotationUtilsTests {
assertEquals("meta2", component.value()); assertEquals("meta2", component.value());
} }
/** @since 4.0.3 */ // @since 4.0.3
@Test @Test
public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() { public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() {
Transactional transactional = findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class); Transactional transactional = findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class);
@@ -220,7 +225,7 @@ public class AnnotationUtilsTests {
assertTrue("readOnly flag for SubSubClassWithInheritedAnnotation", transactional.readOnly()); assertTrue("readOnly flag for SubSubClassWithInheritedAnnotation", transactional.readOnly());
} }
/** @since 4.0.3 */ // @since 4.0.3
@Test @Test
public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() { public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() {
Component component = findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class); Component component = findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class);
@@ -1762,18 +1767,6 @@ public class AnnotationUtilsTests {
public static class SubTransactionalAndOrderedClass extends TransactionalAndOrderedClass { public static class SubTransactionalAndOrderedClass extends TransactionalAndOrderedClass {
} }
public interface InterfaceWithGenericAnnotatedMethod<T> {
@Order
void foo(T t);
}
public static class ImplementsInterfaceWithGenericAnnotatedMethod implements InterfaceWithGenericAnnotatedMethod<String> {
public void foo(String t) {
}
}
public interface InterfaceWithAnnotatedMethod { public interface InterfaceWithAnnotatedMethod {
@Order @Order
@@ -1806,6 +1799,30 @@ public class AnnotationUtilsTests {
} }
} }
public interface InterfaceWithGenericAnnotatedMethod<T> {
@Order
void foo(T t);
}
public static class ImplementsInterfaceWithGenericAnnotatedMethod implements InterfaceWithGenericAnnotatedMethod<String> {
public void foo(String t) {
}
}
public static abstract class BaseClassWithGenericAnnotatedMethod<T> {
@Order
abstract void foo(T t);
}
public static class ExtendsBaseClassWithGenericAnnotatedMethod extends BaseClassWithGenericAnnotatedMethod<String> {
public void foo(String t) {
}
}
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Inherited @Inherited
@interface MyRepeatableContainer { @interface MyRepeatableContainer {