Skip to content

Commit 0ec03a8

Browse files
committed
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
1 parent 7214b1e commit 0ec03a8

File tree

5 files changed

+32
-20
lines changed

5 files changed

+32
-20
lines changed

spring-core/src/main/java/org/springframework/aot/hint/support/RuntimeHintsUtils.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public abstract class RuntimeHintsUtils {
4949
* at runtime.
5050
* @param hints the {@link RuntimeHints} instance to use
5151
* @param annotationType the annotation type
52-
* @see SynthesizedAnnotation
5352
* @deprecated as annotation attributes are visible without additional hints
5453
*/
5554
@Deprecated
@@ -70,7 +69,6 @@ public static void registerAnnotation(RuntimeHints hints, Class<?> annotationTyp
7069
* that determines if the hints are required.
7170
* @param hints the {@link RuntimeHints} instance to use
7271
* @param annotationType the annotation type
73-
* @see SynthesizedAnnotation
7472
*/
7573
public static void registerSynthesizedAnnotation(RuntimeHints hints, Class<?> annotationType) {
7674
hints.proxies().registerJdkProxy(annotationType, SynthesizedAnnotation.class);

spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.lang.reflect.InvocationTargetException;
2323
import java.lang.reflect.Method;
2424
import java.lang.reflect.Modifier;
25+
import java.lang.reflect.Proxy;
2526
import java.util.Collection;
2627
import java.util.Collections;
2728
import java.util.List;
@@ -1288,7 +1289,15 @@ static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, Annotate
12881289
* @since 5.3.23
12891290
*/
12901291
public static boolean isSynthesizedAnnotation(@Nullable Annotation annotation) {
1291-
return (annotation instanceof SynthesizedAnnotation);
1292+
try {
1293+
return ((annotation != null) && Proxy.isProxyClass(annotation.getClass()) &&
1294+
(Proxy.getInvocationHandler(annotation) instanceof SynthesizedMergedAnnotationInvocationHandler));
1295+
}
1296+
catch (SecurityException ex) {
1297+
// Security settings disallow reflective access to the InvocationHandler:
1298+
// assume the annotation has not been synthesized by Spring.
1299+
return false;
1300+
}
12921301
}
12931302

12941303
/**

spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,8 @@ private Object cloneArray(Object array) {
299299
@SuppressWarnings("unchecked")
300300
static <A extends Annotation> A createProxy(MergedAnnotation<A> annotation, Class<A> type) {
301301
ClassLoader classLoader = type.getClassLoader();
302+
Class<?>[] interfaces = new Class<?>[] {type};
302303
InvocationHandler handler = new SynthesizedMergedAnnotationInvocationHandler<>(annotation, type);
303-
Class<?>[] interfaces = isVisible(classLoader, SynthesizedAnnotation.class) ?
304-
new Class<?>[] {type, SynthesizedAnnotation.class} : new Class<?>[] {type};
305304
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
306305
}
307306

@@ -310,17 +309,4 @@ private static String getName(Class<?> clazz) {
310309
return (canonicalName != null ? canonicalName : clazz.getName());
311310
}
312311

313-
314-
private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) {
315-
if (classLoader == interfaceClass.getClassLoader()) {
316-
return true;
317-
}
318-
try {
319-
return Class.forName(interfaceClass.getName(), false, classLoader) == interfaceClass;
320-
}
321-
catch (ClassNotFoundException ex) {
322-
return false;
323-
}
324-
}
325-
326312
}

spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ private boolean isTargetAnnotation(@Nullable Object obj) {
363363
* @since 5.3.22
364364
*/
365365
private boolean isSynthesizable(Annotation annotation) {
366-
if (annotation instanceof SynthesizedAnnotation) {
366+
if (AnnotationUtils.isSynthesizedAnnotation(annotation)) {
367367
return false;
368368
}
369369
return isSynthesizable();

spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,26 @@ void isSynthesizableWithoutAttributeAliases() throws Exception {
15111511
assertThat(MergedAnnotation.from(component).isSynthesizable()).isFalse();
15121512
}
15131513

1514+
/**
1515+
* @since 6.0
1516+
*/
1517+
@Test
1518+
void synthesizedAnnotationShouldReuseJdkProxyClass() throws Exception {
1519+
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
1520+
1521+
RequestMapping jdkRequestMapping = method.getAnnotation(RequestMapping.class);
1522+
assertThat(jdkRequestMapping).isNotNull();
1523+
assertThat(jdkRequestMapping.value()).containsExactly("/test");
1524+
assertThat(jdkRequestMapping.path()).containsExactly("");
1525+
1526+
RequestMapping synthesizedRequestMapping = MergedAnnotation.from(jdkRequestMapping).synthesize();
1527+
assertSynthesized(synthesizedRequestMapping);
1528+
assertThat(synthesizedRequestMapping.value()).containsExactly("/test");
1529+
assertThat(synthesizedRequestMapping.path()).containsExactly("/test");
1530+
1531+
assertThat(jdkRequestMapping.getClass()).isSameAs(synthesizedRequestMapping.getClass());
1532+
}
1533+
15141534
@Test
15151535
void synthesizeAlreadySynthesized() throws Exception {
15161536
Method method = WebController.class.getMethod("handleMappedWithValueAttribute");
@@ -1585,7 +1605,6 @@ void synthesizeShouldNotSynthesizeNonsynthesizableAnnotationsWhenUsingMergedAnno
15851605
mergedAnnotations.get(EnableGlobalAuthentication.class);
15861606
assertThat(enableGlobalAuthenticationMergedAnnotation.isSynthesizable()).isFalse();
15871607
EnableGlobalAuthentication enableGlobalAuthentication = enableGlobalAuthenticationMergedAnnotation.synthesize();
1588-
assertThat(enableGlobalAuthentication).isNotInstanceOf(SynthesizedAnnotation.class);
15891608
assertNotSynthesized(enableGlobalAuthentication);
15901609
}
15911610

0 commit comments

Comments
 (0)