Stop implementing SynthesizedAnnotation in annotation proxies

SynthesizedAnnotation was originally introduced as a convenience for
easily detecting if an annotation had been synthesized by Spring via a
simple `if (myAnnotation instanceof SynthesizedAnnotation)` check.

However, the introduction of SynthesizedAnnotation in the JDK dynamic
proxy for a synthesized annotation results in a separate proxy class
for each annotation synthesized by Spring, and this causes issues with
GraalVM native images since users and framework developers must always
ensure that the additional proxy classes are registered.

This commit completely removes the use of SynthesizedAnnotation in
synthesized annotation proxies. This change allows the proxy class for
an annotation to be reused for a synthesized annotation of the same
annotation type.

Consequently:

- Extra proxy classes are not generated on the JVM or in a native image.
- Extra proxy classes are not required to be registered for a native image.

Closes gh-29041
This commit is contained in:
Sam Brannen
2022-09-02 11:00:40 +02:00
parent 7214b1e03c
commit 0ec03a8dd6
5 changed files with 32 additions and 20 deletions

View File

@@ -49,7 +49,6 @@ public abstract class RuntimeHintsUtils {
* at runtime.
* @param hints the {@link RuntimeHints} instance to use
* @param annotationType the annotation type
* @see SynthesizedAnnotation
* @deprecated as annotation attributes are visible without additional hints
*/
@Deprecated
@@ -70,7 +69,6 @@ public abstract class RuntimeHintsUtils {
* that determines if the hints are required.
* @param hints the {@link RuntimeHints} instance to use
* @param annotationType the annotation type
* @see SynthesizedAnnotation
*/
public static void registerSynthesizedAnnotation(RuntimeHints hints, Class<?> annotationType) {
hints.proxies().registerJdkProxy(annotationType, SynthesizedAnnotation.class);

View File

@@ -22,6 +22,7 @@ import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -1288,7 +1289,15 @@ public abstract class AnnotationUtils {
* @since 5.3.23
*/
public static boolean isSynthesizedAnnotation(@Nullable Annotation annotation) {
return (annotation instanceof SynthesizedAnnotation);
try {
return ((annotation != null) && Proxy.isProxyClass(annotation.getClass()) &&
(Proxy.getInvocationHandler(annotation) instanceof SynthesizedMergedAnnotationInvocationHandler));
}
catch (SecurityException ex) {
// Security settings disallow reflective access to the InvocationHandler:
// assume the annotation has not been synthesized by Spring.
return false;
}
}
/**

View File

@@ -299,9 +299,8 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
@SuppressWarnings("unchecked")
static <A extends Annotation> A createProxy(MergedAnnotation<A> annotation, Class<A> type) {
ClassLoader classLoader = type.getClassLoader();
Class<?>[] interfaces = new Class<?>[] {type};
InvocationHandler handler = new SynthesizedMergedAnnotationInvocationHandler<>(annotation, type);
Class<?>[] interfaces = isVisible(classLoader, SynthesizedAnnotation.class) ?
new Class<?>[] {type, SynthesizedAnnotation.class} : new Class<?>[] {type};
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
}
@@ -310,17 +309,4 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return (canonicalName != null ? canonicalName : clazz.getName());
}
private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) {
if (classLoader == interfaceClass.getClassLoader()) {
return true;
}
try {
return Class.forName(interfaceClass.getName(), false, classLoader) == interfaceClass;
}
catch (ClassNotFoundException ex) {
return false;
}
}
}

View File

@@ -363,7 +363,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
* @since 5.3.22
*/
private boolean isSynthesizable(Annotation annotation) {
if (annotation instanceof SynthesizedAnnotation) {
if (AnnotationUtils.isSynthesizedAnnotation(annotation)) {
return false;
}
return isSynthesizable();

View File

@@ -1511,6 +1511,26 @@ class MergedAnnotationsTests {
assertThat(MergedAnnotation.from(component).isSynthesizable()).isFalse();
}
/**
* @since 6.0
*/
@Test
void synthesizedAnnotationShouldReuseJdkProxyClass() throws Exception {
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
RequestMapping jdkRequestMapping = method.getAnnotation(RequestMapping.class);
assertThat(jdkRequestMapping).isNotNull();
assertThat(jdkRequestMapping.value()).containsExactly("/test");
assertThat(jdkRequestMapping.path()).containsExactly("");
RequestMapping synthesizedRequestMapping = MergedAnnotation.from(jdkRequestMapping).synthesize();
assertSynthesized(synthesizedRequestMapping);
assertThat(synthesizedRequestMapping.value()).containsExactly("/test");
assertThat(synthesizedRequestMapping.path()).containsExactly("/test");
assertThat(jdkRequestMapping.getClass()).isSameAs(synthesizedRequestMapping.getClass());
}
@Test
void synthesizeAlreadySynthesized() throws Exception {
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
@@ -1585,7 +1605,6 @@ class MergedAnnotationsTests {
mergedAnnotations.get(EnableGlobalAuthentication.class);
assertThat(enableGlobalAuthenticationMergedAnnotation.isSynthesizable()).isFalse();
EnableGlobalAuthentication enableGlobalAuthentication = enableGlobalAuthenticationMergedAnnotation.synthesize();
assertThat(enableGlobalAuthentication).isNotInstanceOf(SynthesizedAnnotation.class);
assertNotSynthesized(enableGlobalAuthentication);
}