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.reflect.AnnotatedElement;
 | 
			
		||||
import java.lang.reflect.Array;
 | 
			
		||||
import java.lang.reflect.InvocationHandler;
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.lang.reflect.Proxy;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.NoSuchElementException;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +53,8 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
 | 
			
		|||
 | 
			
		||||
	private final AttributeMethods attributes;
 | 
			
		||||
 | 
			
		||||
	private final Map<String, Object> valueCache = new ConcurrentHashMap<>(8);
 | 
			
		||||
 | 
			
		||||
	@Nullable
 | 
			
		||||
	private volatile Integer hashCode;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -167,11 +172,53 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	private Object getAttributeValue(Method method) {
 | 
			
		||||
		String name = method.getName();
 | 
			
		||||
		Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
 | 
			
		||||
		return this.annotation.getValue(name, type).orElseThrow(
 | 
			
		||||
				() -> new NoSuchElementException("No value found for attribute named '" + name +
 | 
			
		||||
						"' in merged annotation " + this.annotation.getType().getName()));
 | 
			
		||||
		Object value = this.valueCache.computeIfAbsent(method.getName(), attributeName -> {
 | 
			
		||||
			Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
 | 
			
		||||
			return this.annotation.getValue(attributeName, type).orElseThrow(
 | 
			
		||||
					() -> new NoSuchElementException("No value found for attribute named '" + attributeName +
 | 
			
		||||
							"' 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")
 | 
			
		||||
| 
						 | 
				
			
			@ -183,6 +230,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
 | 
			
		|||
		return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) {
 | 
			
		||||
		if (classLoader == interfaceClass.getClassLoader()) {
 | 
			
		||||
			return true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue