Reintroduce synthesized annotation attribute value caching
Prior to the introduction of the MergedAnnotation API in Spring Framework 5.2, our SynthesizedAnnotationInvocationHandler utilized a cache for annotation attribute values; whereas, the new SynthesizedMergedAnnotationInvocationHandler has no such caching in place. Issues such as gh-24961 indicate a regression in performance caused by the lack of such an attribute value cache. For example, the required attribute in @RequestParam is looked up using the internal meta-model in the MergedAnnotation API twice per request for each @RequestParam in a given controller handler method. This commit reintroduces the attribute value cache to avoid the unnecessary performance overhead associated with multiple lookups of the same attribute in a synthesized annotation. This applies not only to direct attribute method invocations but also to invocations of equals() and hashCode() on a synthesized annotation. Note, however, that this commit does NOT address multiple lookups of annotation attribute values for invocations of toString(). That behavior currently remains unchanged in the implementation of org.springframework.core.annotation.TypeMappedAnnotation.toString(). Closes gh-24970
This commit is contained in:
parent
0e9da17910
commit
72b1abd226
|
@ -18,11 +18,14 @@ package org.springframework.core.annotation;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.AnnotatedElement;
|
import java.lang.reflect.AnnotatedElement;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
import java.lang.reflect.InvocationHandler;
|
import java.lang.reflect.InvocationHandler;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -50,6 +53,8 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||||
|
|
||||||
private final AttributeMethods attributes;
|
private final AttributeMethods attributes;
|
||||||
|
|
||||||
|
private final Map<String, Object> valueCache = new ConcurrentHashMap<>(8);
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private volatile Integer hashCode;
|
private volatile Integer hashCode;
|
||||||
|
|
||||||
|
@ -167,11 +172,53 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object getAttributeValue(Method method) {
|
private Object getAttributeValue(Method method) {
|
||||||
String name = method.getName();
|
Object value = this.valueCache.computeIfAbsent(method.getName(), attributeName -> {
|
||||||
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
|
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
|
||||||
return this.annotation.getValue(name, type).orElseThrow(
|
return this.annotation.getValue(attributeName, type).orElseThrow(
|
||||||
() -> new NoSuchElementException("No value found for attribute named '" + name +
|
() -> new NoSuchElementException("No value found for attribute named '" + attributeName +
|
||||||
"' in merged annotation " + this.annotation.getType().getName()));
|
"' in merged annotation " + this.annotation.getType().getName()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clone non-empty arrays so that users cannot alter the contents of values in our cache.
|
||||||
|
if (value.getClass().isArray() && Array.getLength(value) > 0) {
|
||||||
|
value = cloneArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone the provided array, ensuring that original component type is retained.
|
||||||
|
* @param array the array to clone
|
||||||
|
*/
|
||||||
|
private Object cloneArray(Object array) {
|
||||||
|
if (array instanceof boolean[]) {
|
||||||
|
return ((boolean[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof byte[]) {
|
||||||
|
return ((byte[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof char[]) {
|
||||||
|
return ((char[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof double[]) {
|
||||||
|
return ((double[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof float[]) {
|
||||||
|
return ((float[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof int[]) {
|
||||||
|
return ((int[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof long[]) {
|
||||||
|
return ((long[]) array).clone();
|
||||||
|
}
|
||||||
|
if (array instanceof short[]) {
|
||||||
|
return ((short[]) array).clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// else
|
||||||
|
return ((Object[]) array).clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -183,6 +230,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||||
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
|
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) {
|
private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) {
|
||||||
if (classLoader == interfaceClass.getClassLoader()) {
|
if (classLoader == interfaceClass.getClassLoader()) {
|
||||||
return true;
|
return true;
|
||||||
|
|
Loading…
Reference in New Issue