Comprehensively cache annotated methods for interfaces and superclasses

Issue: SPR-16675
This commit is contained in:
Juergen Hoeller
2018-03-31 15:12:02 +02:00
parent b8d32095a9
commit 129c05bcff
4 changed files with 158 additions and 145 deletions

View File

@@ -1050,17 +1050,48 @@ public class AnnotatedElementUtils {
try { try {
// Locally declared annotations (ignoring @Inherited) // Locally declared annotations (ignoring @Inherited)
Annotation[] annotations = element.getDeclaredAnnotations(); Annotation[] annotations = element.getDeclaredAnnotations();
List<T> aggregatedResults = (processor.aggregates() ? new ArrayList<>() : null); if (annotations.length > 0) {
List<T> aggregatedResults = (processor.aggregates() ? new ArrayList<>() : null);
// Search in local annotations // Search in local annotations
for (Annotation annotation : annotations) { for (Annotation annotation : annotations) {
Class<? extends Annotation> currentAnnotationType = annotation.annotationType(); Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
if (currentAnnotationType == annotationType || if (currentAnnotationType == annotationType ||
currentAnnotationType.getName().equals(annotationName) || currentAnnotationType.getName().equals(annotationName) ||
processor.alwaysProcesses()) { processor.alwaysProcesses()) {
T result = processor.process(element, annotation, metaDepth); T result = processor.process(element, annotation, metaDepth);
if (result != null) {
if (aggregatedResults != null && metaDepth == 0) {
aggregatedResults.add(result);
}
else {
return result;
}
}
}
// Repeatable annotations in container?
else if (currentAnnotationType == containerType) {
for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {
T result = processor.process(element, contained, metaDepth);
if (aggregatedResults != null && result != null) {
// No need to post-process since repeatable annotations within a
// container cannot be composed annotations.
aggregatedResults.add(result);
}
}
}
}
}
// Recursively search in meta-annotations
for (Annotation annotation : annotations) {
Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
if (hasSearchableMetaAnnotations(currentAnnotationType, annotationType, annotationName)) {
T result = searchWithFindSemantics(currentAnnotationType, annotationType, annotationName,
containerType, processor, visited, metaDepth + 1);
if (result != null) { if (result != null) {
processor.postProcess(currentAnnotationType, annotation, result);
if (aggregatedResults != null && metaDepth == 0) { if (aggregatedResults != null && metaDepth == 0) {
aggregatedResults.add(result); aggregatedResults.add(result);
} }
@@ -1069,43 +1100,14 @@ public class AnnotatedElementUtils {
} }
} }
} }
// Repeatable annotations in container?
else if (currentAnnotationType == containerType) {
for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {
T result = processor.process(element, contained, metaDepth);
if (aggregatedResults != null && result != null) {
// No need to post-process since repeatable annotations within a
// container cannot be composed annotations.
aggregatedResults.add(result);
}
}
}
} }
}
// Recursively search in meta-annotations if (!CollectionUtils.isEmpty(aggregatedResults)) {
for (Annotation annotation : annotations) { // Prepend to support top-down ordering within class hierarchies
Class<? extends Annotation> currentAnnotationType = annotation.annotationType(); processor.getAggregatedResults().addAll(0, aggregatedResults);
if (hasSearchableMetaAnnotations(currentAnnotationType, annotationType, annotationName)) {
T result = searchWithFindSemantics(currentAnnotationType, annotationType, annotationName,
containerType, processor, visited, metaDepth + 1);
if (result != null) {
processor.postProcess(currentAnnotationType, annotation, result);
if (aggregatedResults != null && metaDepth == 0) {
aggregatedResults.add(result);
}
else {
return result;
}
}
} }
} }
if (!CollectionUtils.isEmpty(aggregatedResults)) {
// Prepend to support top-down ordering within class hierarchies
processor.getAggregatedResults().addAll(0, aggregatedResults);
}
if (element instanceof Method) { if (element instanceof Method) {
Method method = (Method) element; Method method = (Method) element;
T result; T result;
@@ -1123,8 +1125,8 @@ public class AnnotatedElementUtils {
// Search on methods in interfaces declared locally // Search on methods in interfaces declared locally
Class<?>[] ifcs = method.getDeclaringClass().getInterfaces(); Class<?>[] ifcs = method.getDeclaringClass().getInterfaces();
if (ifcs.length > 0) { if (ifcs.length > 0) {
result = searchOnInterfaces(method, annotationType, annotationName, containerType, result = searchOnInterfaces(method, annotationType, annotationName,
processor, visited, metaDepth, ifcs); containerType, processor, visited, metaDepth, ifcs);
if (result != null) { if (result != null) {
return result; return result;
} }
@@ -1137,23 +1139,23 @@ public class AnnotatedElementUtils {
if (clazz == null || Object.class == clazz) { if (clazz == null || Object.class == clazz) {
break; break;
} }
Set<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethodsInBaseType(clazz);
try { if (!annotatedMethods.isEmpty()) {
Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); for (Method annotatedMethod : annotatedMethods) {
Method resolvedEquivalentMethod = BridgeMethodResolver.findBridgedMethod(equivalentMethod); if (annotatedMethod.getName().equals(method.getName()) &&
result = searchWithFindSemantics(resolvedEquivalentMethod, annotationType, annotationName, Arrays.equals(annotatedMethod.getParameterTypes(), method.getParameterTypes())) {
containerType, processor, visited, metaDepth); Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod);
if (result != null) { result = searchWithFindSemantics(resolvedSuperMethod, annotationType, annotationName,
return result; containerType, processor, visited, metaDepth);
if (result != null) {
return result;
}
}
} }
} }
catch (NoSuchMethodException ex) {
// No equivalent method found
}
// Search on interfaces declared on superclass // Search on interfaces declared on superclass
result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor, result = searchOnInterfaces(method, annotationType, annotationName,
visited, metaDepth, clazz.getInterfaces()); containerType, processor, visited, metaDepth, clazz.getInterfaces());
if (result != null) { if (result != null) {
return result; return result;
} }
@@ -1164,8 +1166,8 @@ public class AnnotatedElementUtils {
// Search on interfaces // Search on interfaces
for (Class<?> ifc : clazz.getInterfaces()) { for (Class<?> ifc : clazz.getInterfaces()) {
T result = searchWithFindSemantics(ifc, annotationType, annotationName, containerType, T result = searchWithFindSemantics(ifc, annotationType, annotationName,
processor, visited, metaDepth); containerType, processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
} }
@@ -1174,8 +1176,8 @@ public class AnnotatedElementUtils {
// Search on superclass // Search on superclass
Class<?> superclass = clazz.getSuperclass(); Class<?> superclass = clazz.getSuperclass();
if (superclass != null && Object.class != superclass) { if (superclass != null && Object.class != superclass) {
T result = searchWithFindSemantics(superclass, annotationType, annotationName, containerType, T result = searchWithFindSemantics(superclass, annotationType, annotationName,
processor, visited, metaDepth); containerType, processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
} }
@@ -1195,18 +1197,18 @@ public class AnnotatedElementUtils {
Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth, Class<?>[] ifcs) { Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth, Class<?>[] ifcs) {
for (Class<?> ifc : ifcs) { for (Class<?> ifc : ifcs) {
if (AnnotationUtils.isInterfaceWithAnnotatedMethods(ifc)) { Set<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethodsInBaseType(ifc);
try { if (!annotatedMethods.isEmpty()) {
Method equivalentMethod = ifc.getMethod(method.getName(), method.getParameterTypes()); for (Method annotatedMethod : annotatedMethods) {
T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, containerType, if (annotatedMethod.getName().equals(method.getName()) &&
processor, visited, metaDepth); Arrays.equals(annotatedMethod.getParameterTypes(), method.getParameterTypes())) {
if (result != null) { T result = searchWithFindSemantics(annotatedMethod, annotationType, annotationName,
return result; containerType, processor, visited, metaDepth);
if (result != null) {
return result;
}
} }
} }
catch (NoSuchMethodException ex) {
// Skip this interface - it doesn't have the method...
}
} }
} }

View File

@@ -26,6 +26,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -122,7 +123,7 @@ public abstract class AnnotationUtils {
private static final Map<AnnotationCacheKey, Boolean> metaPresentCache = private static final Map<AnnotationCacheKey, Boolean> metaPresentCache =
new ConcurrentReferenceHashMap<>(256); new ConcurrentReferenceHashMap<>(256);
private static final Map<Class<?>, Boolean> annotatedInterfaceCache = private static final Map<Class<?>, Set<Method>> annotatedBaseTypeCache =
new ConcurrentReferenceHashMap<>(256); new ConcurrentReferenceHashMap<>(256);
private static final Map<Class<? extends Annotation>, Boolean> synthesizableCache = private static final Map<Class<? extends Annotation>, Boolean> synthesizableCache =
@@ -539,7 +540,6 @@ public abstract class AnnotationUtils {
if (result == null) { if (result == null) {
Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
result = findAnnotation((AnnotatedElement) resolvedMethod, annotationType); result = findAnnotation((AnnotatedElement) resolvedMethod, annotationType);
if (result == null) { if (result == null) {
result = searchOnInterfaces(method, annotationType, method.getDeclaringClass().getInterfaces()); result = searchOnInterfaces(method, annotationType, method.getDeclaringClass().getInterfaces());
} }
@@ -574,48 +574,63 @@ public abstract class AnnotationUtils {
@Nullable @Nullable
private static <A extends Annotation> A searchOnInterfaces(Method method, Class<A> annotationType, Class<?>... ifcs) { private static <A extends Annotation> A searchOnInterfaces(Method method, Class<A> annotationType, Class<?>... ifcs) {
A annotation = null;
for (Class<?> ifc : ifcs) { for (Class<?> ifc : ifcs) {
if (isInterfaceWithAnnotatedMethods(ifc)) { Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(ifc);
try { if (!annotatedMethods.isEmpty()) {
Method equivalentMethod = ifc.getMethod(method.getName(), method.getParameterTypes()); for (Method annotatedMethod : annotatedMethods) {
annotation = getAnnotation(equivalentMethod, annotationType); if (annotatedMethod.getName().equals(method.getName()) &&
} Arrays.equals(annotatedMethod.getParameterTypes(), method.getParameterTypes())) {
catch (NoSuchMethodException ex) { A annotation = getAnnotation(annotatedMethod, annotationType);
// Skip this interface - it doesn't have the method... if (annotation != null) {
} return annotation;
if (annotation != null) { }
break; }
} }
} }
} }
return annotation; return null;
} }
static boolean isInterfaceWithAnnotatedMethods(Class<?> ifc) { static Set<Method> getAnnotatedMethodsInBaseType(Class<?> baseType) {
if (ClassUtils.isJavaLanguageInterface(ifc)) { if (ClassUtils.isJavaLanguageInterface(baseType)) {
return false; return Collections.emptySet();
} }
Boolean found = annotatedInterfaceCache.get(ifc); Set<Method> annotatedMethods = annotatedBaseTypeCache.get(baseType);
if (found != null) { if (annotatedMethods != null) {
return found; return annotatedMethods;
} }
found = Boolean.FALSE; Method[] methods = (baseType.isInterface() ? baseType.getMethods() : baseType.getDeclaredMethods());
for (Method ifcMethod : ifc.getMethods()) { for (Method baseMethod : methods) {
try { try {
Annotation[] anns = ifcMethod.getAnnotations(); if (hasSearchableAnnotations(baseMethod)) {
if (anns.length > 1 || (anns.length == 1 && anns[0].annotationType() != Nullable.class)) { if (annotatedMethods == null) {
found = Boolean.TRUE; annotatedMethods = new HashSet<>();
break; }
annotatedMethods.add(baseMethod);
} }
} }
catch (Throwable ex) { catch (Throwable ex) {
handleIntrospectionFailure(ifcMethod, ex); handleIntrospectionFailure(baseMethod, ex);
} }
} }
annotatedInterfaceCache.put(ifc, found); if (annotatedMethods == null) {
return found; annotatedMethods = Collections.emptySet();
}
annotatedBaseTypeCache.put(baseType, annotatedMethods);
return annotatedMethods;
}
private static boolean hasSearchableAnnotations(Method ifcMethod) {
Annotation[] anns = ifcMethod.getAnnotations();
if (anns.length == 0) {
return false;
}
if (anns.length == 1) {
Class<?> annType = anns[0].annotationType();
return (annType != Nullable.class && annType != Deprecated.class);
}
return true;
} }
/** /**
@@ -1875,6 +1890,20 @@ public abstract class AnnotationUtils {
} }
} }
/**
* Clear the internal annotation metadata cache.
* @since 4.3.15
*/
public static void clearCache() {
findAnnotationCache.clear();
metaPresentCache.clear();
annotatedBaseTypeCache.clear();
synthesizableCache.clear();
attributeAliasesCache.clear();
attributeMethodsCache.clear();
aliasDescriptorCache.clear();
}
/** /**
* Cache key for the AnnotatedElement cache. * Cache key for the AnnotatedElement cache.

View File

@@ -22,7 +22,6 @@ import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -39,7 +38,6 @@ import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import static java.util.Arrays.*; import static java.util.Arrays.*;
import static java.util.stream.Collectors.*; import static java.util.stream.Collectors.*;
@@ -64,23 +62,8 @@ public class AnnotationUtilsTests {
@Before @Before
public void clearCachesBeforeTests() { public void clearCacheBeforeTests() {
clearCaches(); AnnotationUtils.clearCache();
}
static void clearCaches() {
clearCache("findAnnotationCache", "annotatedInterfaceCache", "metaPresentCache", "synthesizableCache",
"attributeAliasesCache", "attributeMethodsCache", "aliasDescriptorCache");
}
static void clearCache(String... cacheNames) {
stream(cacheNames).forEach(cacheName -> getCache(cacheName).clear());
}
static Map<?, ?> getCache(String cacheName) {
Field field = ReflectionUtils.findField(AnnotationUtils.class, cacheName);
ReflectionUtils.makeAccessible(field);
return (Map<?, ?>) ReflectionUtils.getField(field, null);
} }
@@ -1544,9 +1527,9 @@ public class AnnotationUtilsTests {
@Test @Test
public void interfaceWithAnnotatedMethods() { public void interfaceWithAnnotatedMethods() {
assertFalse(AnnotationUtils.isInterfaceWithAnnotatedMethods(NonAnnotatedInterface.class)); assertTrue(AnnotationUtils.getAnnotatedMethodsInBaseType(NonAnnotatedInterface.class).isEmpty());
assertTrue(AnnotationUtils.isInterfaceWithAnnotatedMethods(AnnotatedInterface.class)); assertFalse(AnnotationUtils.getAnnotatedMethodsInBaseType(AnnotatedInterface.class).isEmpty());
assertFalse(AnnotationUtils.isInterfaceWithAnnotatedMethods(NullableAnnotatedInterface.class)); assertTrue(AnnotationUtils.getAnnotatedMethodsInBaseType(NullableAnnotatedInterface.class).isEmpty());
} }

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.
@@ -17,6 +17,7 @@
package org.springframework.core.annotation; package org.springframework.core.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@@ -41,13 +42,20 @@ import static org.springframework.core.annotation.AnnotationUtilsTests.*;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase { public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Before @Override
public void clearCachesBeforeTests() { protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
AnnotationUtilsTests.clearCaches(); Map<String, Object> attributes = Collections.singletonMap(expected, expected);
return new MapAnnotationAttributeExtractor(attributes, annotationType, clazz);
} }
@Before
public void clearCacheBeforeTests() {
AnnotationUtils.clearCache();
}
@Test @Test
public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() { public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() throws Exception {
Map<String, Object> attributes = new HashMap<>(); Map<String, Object> attributes = new HashMap<>();
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{ Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
put("groovyScript", ""); put("groovyScript", "");
@@ -64,7 +72,7 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
} }
@Test @Test
public void enrichAndValidateAttributesWithImplicitAliases() { public void enrichAndValidateAttributesWithImplicitAliases() throws Exception {
Map<String, Object> attributes = new HashMap<String, Object>() {{ Map<String, Object> attributes = new HashMap<String, Object>() {{
put("groovyScript", "groovy!"); put("groovyScript", "groovy!");
}}; }};
@@ -85,7 +93,6 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
@Test @Test
public void enrichAndValidateAttributesWithSingleElementThatOverridesAnArray() { public void enrichAndValidateAttributesWithSingleElementThatOverridesAnArray() {
// @formatter:off
Map<String, Object> attributes = new HashMap<String, Object>() {{ Map<String, Object> attributes = new HashMap<String, Object>() {{
// Intentionally storing 'value' as a single String instead of an array. // Intentionally storing 'value' as a single String instead of an array.
// put("value", asArray("/foo")); // put("value", asArray("/foo"));
@@ -99,7 +106,6 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
put("name", "test"); put("name", "test");
put("method", new RequestMethod[0]); put("method", new RequestMethod[0]);
}}; }};
// @formatter:on
MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(attributes, WebMapping.class, null); MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(attributes, WebMapping.class, null);
Map<String, Object> enriched = extractor.getSource(); Map<String, Object> enriched = extractor.getSource();
@@ -109,18 +115,17 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) { private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) throws Exception {
Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class; Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class;
// Since the ordering of attribute methods returned by the JVM is // Since the ordering of attribute methods returned by the JVM is non-deterministic,
// non-deterministic, we have to rig the attributeAliasesCache in AnnotationUtils // we have to rig the attributeAliasesCache in AnnotationUtils so that the tests
// so that the tests consistently fail in case enrichAndValidateAttributes() is // consistently fail in case enrichAndValidateAttributes() is buggy.
// buggy. // Otherwise, these tests would intermittently pass even for an invalid implementation.
// Field cacheField = AnnotationUtils.class.getDeclaredField("attributeAliasesCache");
// Otherwise, these tests would intermittently pass even for an invalid cacheField.setAccessible(true);
// implementation.
Map<Class<? extends Annotation>, MultiValueMap<String, String>> attributeAliasesCache = Map<Class<? extends Annotation>, MultiValueMap<String, String>> attributeAliasesCache =
(Map<Class<? extends Annotation>, MultiValueMap<String, String>>) AnnotationUtilsTests.getCache("attributeAliasesCache"); (Map<Class<? extends Annotation>, MultiValueMap<String, String>>) cacheField.get(null);
// Declare aliases in an order that will cause enrichAndValidateAttributes() to // Declare aliases in an order that will cause enrichAndValidateAttributes() to
// fail unless it considers all aliases in the set of implicit aliases. // fail unless it considers all aliases in the set of implicit aliases.
@@ -141,10 +146,4 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue))); expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue)));
} }
@Override
protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
Map<String, Object> attributes = Collections.singletonMap(expected, expected);
return new MapAnnotationAttributeExtractor(attributes, annotationType, clazz);
}
} }