From 323d1907c100e69a1755803162a8f969ba7f7d25 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 6 Sep 2022 22:07:20 +0200 Subject: [PATCH] Do not require full type reflection when listing methods/fields Prior to this commit, the `RuntimeHintsAgent` and its testing infrastructure would assume that calling `MyClass.class.getMethods()` requires a reflection hint on the class for introspecting public/private methods. GraalVM does not require this, in fact this call only returns methods that have reflection hints in the native image. This commit refines the agent behavior for `Class.getMethods()`, `Class.getDeclaredMethods()`, `Class.getFields()` and `Class.getDeclaredFields()`. With this change, registering at least one method/field for reflection is enough to match. During the execution of Java tests, all methods and fields will be provided, regardless of hints being registered or not. This could cause false negatives where we're missing reflection hints on methods or fields. This risk is mitigated thanks to additional instrumentation on `Method.getAnnotations()`, `Method.getParameterTypes()` and `Method.invoke()`. If a method is found reflectively, chances are it will be used for further reflection. Closes gh-29091 --- .../aot/RuntimeHintsAgentTests.java | 16 +++ .../aot/agent/InstrumentedBridgeMethods.java | 27 ++++ .../aot/agent/InstrumentedMethod.java | 90 +++++++----- .../aot/agent/InstrumentedMethodTests.java | 136 +++++++++++++++--- .../predicate/ReflectionHintsPredicates.java | 23 +++ .../ReflectionHintsPredicatesTests.java | 50 +++++++ 6 files changed, 288 insertions(+), 54 deletions(-) diff --git a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java index 2309cbc01d..e29518bae3 100644 --- a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java +++ b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java @@ -164,6 +164,22 @@ public class RuntimeHintsAgentTests { throw new RuntimeException(e); } }, MethodReference.of(Method.class, "invoke")), + Arguments.of((Runnable) () -> { + try { + toStringMethod.getAnnotations(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Method.class, "getAnnotations")), + Arguments.of((Runnable) () -> { + try { + toStringMethod.getParameterTypes(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }, MethodReference.of(Method.class, "getParameterTypes")), Arguments.of((Runnable) () -> { try { privateGreetMethod.invoke(new PrivateClass()); diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java index df2d880c6e..30c9aef2c5 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java @@ -18,6 +18,7 @@ package org.springframework.aot.agent; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; @@ -335,6 +336,32 @@ public abstract class InstrumentedBridgeMethods { * Bridge methods for java.lang.reflect.Method */ + public static Annotation[] methodgetAnnotations(Method method) { + Annotation[] result = null; + try { + result = method.getAnnotations(); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_GETANNOTATIONS) + .onInstance(method).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Class[] methodgetParameterTypes(Method method) { + Class[] result = null; + try { + result = method.getParameterTypes(); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_GETPARAMETERTYPES) + .onInstance(method).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + public static Object methodinvoke(Method method, Object object, Object... arguments) throws InvocationTargetException, IllegalAccessException { Object result = null; boolean accessibilityChanged = false; diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java index c242d8575a..f7fc7e8a51 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java @@ -31,6 +31,9 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource; + /** * Java method that is instrumented by the {@link RuntimeHintsAgent}. * @@ -55,7 +58,7 @@ enum InstrumentedMethod { CLASS_FORNAME(Class.class, "forName", HintType.REFLECTION, invocation -> { String className = invocation.getArgument(0); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(className)); + return reflection().onType(TypeReference.of(className)); }), /** @@ -64,7 +67,7 @@ enum InstrumentedMethod { CLASS_GETCLASSES(Class.class, "getClasses", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)) + return reflection().onType(TypeReference.of(thisClass)) .withAnyMemberCategory(MemberCategory.DECLARED_CLASSES, MemberCategory.PUBLIC_CLASSES); } ), @@ -78,7 +81,7 @@ enum InstrumentedMethod { if (constructor == null) { return runtimeHints -> false; } - return RuntimeHintsPredicates.reflection().onConstructor(constructor).introspect(); + return reflection().onConstructor(constructor).introspect(); } ), @@ -88,9 +91,10 @@ enum InstrumentedMethod { CLASS_GETCONSTRUCTORS(Class.class, "getConstructors", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)).withAnyMemberCategory( + return reflection().onType(TypeReference.of(thisClass)).withAnyMemberCategory( MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) + .or(reflection().onType(TypeReference.of(thisClass)).withAnyConstructor()); } ), @@ -100,7 +104,7 @@ enum InstrumentedMethod { CLASS_GETDECLAREDCLASSES(Class.class, "getDeclaredClasses", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)).withMemberCategory(MemberCategory.DECLARED_CLASSES); + return reflection().onType(TypeReference.of(thisClass)).withMemberCategory(MemberCategory.DECLARED_CLASSES); } ), @@ -114,8 +118,8 @@ enum InstrumentedMethod { return runtimeHints -> false; } TypeReference thisType = invocation.getInstanceTypeReference(); - return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS) - .or(RuntimeHintsPredicates.reflection().onConstructor(constructor).introspect()); + return reflection().onType(thisType).withMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS) + .or(reflection().onConstructor(constructor).introspect()); } ), @@ -125,8 +129,9 @@ enum InstrumentedMethod { CLASS_GETDECLAREDCONSTRUCTORS(Class.class, "getDeclaredConstructors", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + return reflection().onType(TypeReference.of(thisClass)) + .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) + .or(reflection().onType(TypeReference.of(thisClass)).withAnyConstructor()); }), /** @@ -139,8 +144,8 @@ enum InstrumentedMethod { return runtimeHints -> false; } TypeReference thisType = invocation.getInstanceTypeReference(); - return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS) - .or(RuntimeHintsPredicates.reflection().onField(field)); + return reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS) + .or(reflection().onField(field)); } ), @@ -150,7 +155,8 @@ enum InstrumentedMethod { CLASS_GETDECLAREDFIELDS(Class.class, "getDeclaredFields", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)).withMemberCategory(MemberCategory.DECLARED_FIELDS); + return reflection().onType(TypeReference.of(thisClass)).withMemberCategory(MemberCategory.DECLARED_FIELDS) + .or(reflection().onType(TypeReference.of(thisClass)).withAnyField()); } ), @@ -164,9 +170,9 @@ enum InstrumentedMethod { return runtimeHints -> false; } TypeReference thisType = invocation.getInstanceTypeReference(); - return RuntimeHintsPredicates.reflection().onType(thisType) + return reflection().onType(thisType) .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) - .or(RuntimeHintsPredicates.reflection().onMethod(method).introspect()); + .or(reflection().onMethod(method).introspect()); } ), @@ -176,8 +182,9 @@ enum InstrumentedMethod { CLASS_GETDECLAREDMETHODS(Class.class, "getDeclaredMethods", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS); + return reflection().onType(TypeReference.of(thisClass)) + .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) + .or(reflection().onType(TypeReference.of(thisClass)).withAnyMethod()); } ), @@ -191,10 +198,10 @@ enum InstrumentedMethod { return runtimeHints -> false; } TypeReference thisType = invocation.getInstanceTypeReference(); - return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.PUBLIC_FIELDS) + return reflection().onType(thisType).withMemberCategory(MemberCategory.PUBLIC_FIELDS) .and(runtimeHints -> Modifier.isPublic(field.getModifiers())) - .or(RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS)) - .or(RuntimeHintsPredicates.reflection().onField(invocation.getReturnValue())); + .or(reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS)) + .or(reflection().onField(invocation.getReturnValue())); }), /** @@ -203,8 +210,9 @@ enum InstrumentedMethod { CLASS_GETFIELDS(Class.class, "getFields", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS); + return reflection().onType(TypeReference.of(thisClass)) + .withAnyMemberCategory(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS) + .or(reflection().onType(TypeReference.of(thisClass)).withAnyField()); } ), @@ -218,11 +226,11 @@ enum InstrumentedMethod { return runtimeHints -> false; } TypeReference thisType = invocation.getInstanceTypeReference(); - return RuntimeHintsPredicates.reflection().onType(thisType).withAnyMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS) + return reflection().onType(thisType).withAnyMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS) .and(runtimeHints -> Modifier.isPublic(method.getModifiers())) - .or(RuntimeHintsPredicates.reflection().onType(thisType).withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS)) - .or(RuntimeHintsPredicates.reflection().onMethod(method).introspect()) - .or(RuntimeHintsPredicates.reflection().onMethod(method).invoke()); + .or(reflection().onType(thisType).withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS)) + .or(reflection().onMethod(method).introspect()) + .or(reflection().onMethod(method).invoke()); } ), @@ -232,9 +240,10 @@ enum InstrumentedMethod { CLASS_GETMETHODS(Class.class, "getMethods", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return RuntimeHintsPredicates.reflection().onType(TypeReference.of(thisClass)).withAnyMemberCategory( + return reflection().onType(TypeReference.of(thisClass)).withAnyMemberCategory( MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS); + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) + .or(reflection().onType(TypeReference.of(thisClass)).withAnyMethod()); } ), @@ -247,32 +256,41 @@ enum InstrumentedMethod { if (klass == null) { return runtimeHints -> false; } - return RuntimeHintsPredicates.reflection().onType(klass); + return reflection().onType(klass); }), /** * {@link Constructor#newInstance(Object...)}. */ CONSTRUCTOR_NEWINSTANCE(Constructor.class, "newInstance", HintType.REFLECTION, - invocation -> RuntimeHintsPredicates.reflection().onConstructor(invocation.getInstance()).invoke()), + invocation -> reflection().onConstructor(invocation.getInstance()).invoke()), + + /** + * {@link Method#getParameterTypes()}. + */ + METHOD_GETANNOTATIONS(Method.class, "getAnnotations", HintType.REFLECTION, + invocation -> reflection().onMethod(invocation.getInstance())), + + METHOD_GETPARAMETERTYPES(Method.class, "getParameterTypes", HintType.REFLECTION, + invocation -> reflection().onMethod(invocation.getInstance())), /** * {@link Method#invoke(Object, Object...)}. */ METHOD_INVOKE(Method.class, "invoke", HintType.REFLECTION, - invocation -> RuntimeHintsPredicates.reflection().onMethod(invocation.getInstance()).invoke()), + invocation -> reflection().onMethod(invocation.getInstance()).invoke()), /** * {@link Field#get(Object)}. */ FIELD_GET(Field.class, "get", HintType.REFLECTION, - invocation -> RuntimeHintsPredicates.reflection().onField(invocation.getInstance())), + invocation -> reflection().onField(invocation.getInstance())), /** * {@link Field#set(Object, Object)}. */ FIELD_SET(Field.class, "set", HintType.REFLECTION, - invocation -> RuntimeHintsPredicates.reflection().onField(invocation.getInstance()).withWriteMode()), + invocation -> reflection().onField(invocation.getInstance()).withWriteMode()), /* @@ -285,7 +303,7 @@ enum InstrumentedMethod { RESOURCEBUNDLE_GETBUNDLE(ResourceBundle.class, "getBundle", HintType.RESOURCE_BUNDLE, invocation -> { String bundleName = invocation.getArgument(0); - return RuntimeHintsPredicates.resource().forBundle(bundleName); + return resource().forBundle(bundleName); }), /* @@ -299,7 +317,7 @@ enum InstrumentedMethod { invocation -> { Class thisClass = invocation.getInstance(); String resourceName = invocation.getArgument(0); - return RuntimeHintsPredicates.resource().forResource(TypeReference.of(thisClass), resourceName); + return resource().forResource(TypeReference.of(thisClass), resourceName); }), /** @@ -315,7 +333,7 @@ enum InstrumentedMethod { CLASSLOADER_GETRESOURCE(ClassLoader.class, "getResource", HintType.RESOURCE_PATTERN, invocation -> { String resourceName = invocation.getArgument(0); - return RuntimeHintsPredicates.resource().forResource(resourceName); + return resource().forResource(resourceName); }), /** diff --git a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java index e2839c46ad..0a3863a08c 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java @@ -16,6 +16,7 @@ package org.springframework.aot.agent; +import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collections; import java.util.Comparator; @@ -185,6 +186,18 @@ class InstrumentedMethodTests { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); } + @Test + void classGetConstructorsShouldNotMatchTypeReflectionHint() { + hints.reflection().registerType(String.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + } + + @Test + void classGetConstructorsShouldMatchConstructorReflectionHint() throws Exception { + hints.reflection().registerConstructor(String.class.getConstructor()); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + } + @Test void classGetDeclaredConstructorShouldMatchIntrospectDeclaredConstructorsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); @@ -229,6 +242,17 @@ class InstrumentedMethodTests { assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); } + @Test + void classGetDeclaredConstructorsShouldNotMatchTypeReflectionHint() { + hints.reflection().registerType(String.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); + } + + @Test + void classGetDeclaredConstructorsShouldMatchConstructorReflectionHint() throws Exception { + hints.reflection().registerConstructor(String.class.getConstructor()); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); + } @Test void constructorNewInstanceShouldMatchInvokeHintOnConstructor() throws NoSuchMethodException { @@ -257,6 +281,8 @@ class InstrumentedMethodTests { RecordedInvocation stringGetScaleMethod; + RecordedInvocation stringGetMethods; + @BeforeEach void setup() throws Exception { this.stringGetToStringMethod = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHOD) @@ -265,6 +291,8 @@ class InstrumentedMethodTests { this.stringGetScaleMethod = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDMETHOD) .onInstance(String.class).withArguments("scale", new Class[] { int.class, float.class }) .returnValue(String.class.getDeclaredMethod("scale", int.class, float.class)).build(); + this.stringGetMethods = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS) + .onInstance(String.class).returnValue(String.class.getMethods()).build(); } @Test @@ -309,60 +337,79 @@ class InstrumentedMethodTests { } @Test - void classGetDeclaredMethodsShouldMatchIntrospectPublicMethodsHint() { + void classGetDeclaredMethodsShouldNotMatchIntrospectPublicMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); } + @Test + void classGetDeclaredMethodsShouldNotMatchTypeReflectionHint() { + hints.reflection().registerType(String.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); + } + + @Test + void classGetDeclaredMethodsShouldMatchMethodReflectionHint() throws Exception { + hints.reflection().registerMethod(String.class.getMethod("toString")); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); + } + @Test void classGetMethodsShouldMatchInstrospectDeclaredMethodsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test void classGetMethodsShouldMatchInstrospectPublicMethodsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test void classGetMethodsShouldMatchInvokeDeclaredMethodsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); hints.reflection().registerType(String.class, MemberCategory.INVOKE_DECLARED_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test void classGetMethodsShouldMatchInvokePublicMethodsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test void classGetMethodsShouldNotMatchForWrongType() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); hints.reflection().registerType(Integer.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, invocation); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test - void classGetMethodShouldMatchInstrospectPublicMethodsHint() throws NoSuchMethodException { + void classGetMethodsShouldNotMatchTypeReflectionHint() { + hints.reflection().registerType(String.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); + } + + @Test + void classGetMethodsShouldMatchMethodReflectionHint() throws Exception { + hints.reflection().registerMethod(String.class.getMethod("toString")); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); + } + + @Test + void classGetMethodShouldMatchInstrospectPublicMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @Test - void classGetMethodShouldMatchInvokePublicMethodsHint() throws NoSuchMethodException { + void classGetMethodShouldMatchInvokePublicMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @Test - void classGetMethodShouldMatchInstrospectDeclaredMethodsHint() throws NoSuchMethodException { + void classGetMethodShouldMatchIntrospectDeclaredMethodsHint() { hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @@ -405,6 +452,28 @@ class InstrumentedMethodTests { assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } + @Test + void methodGetAnnotationsShouldMatchIntrospectHintOnMethod() throws NoSuchMethodException { + Method toString = String.class.getMethod("toString"); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_GETANNOTATIONS) + .onInstance(toString).withArguments("testString") + .returnValue(toString.getAnnotations()).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMethod("toString", + Collections.emptyList(), ExecutableMode.INTROSPECT)); + assertThatInvocationMatches(InstrumentedMethod.METHOD_GETANNOTATIONS, invocation); + } + + @Test + void methodGetParameterTypesShouldMatchIntrospectHintOnMethod() throws NoSuchMethodException { + Method toString = String.class.getMethod("toString"); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_GETPARAMETERTYPES) + .onInstance(toString).withArguments("testString") + .returnValue(toString.getParameterTypes()).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMethod("toString", + Collections.emptyList(), ExecutableMode.INTROSPECT)); + assertThatInvocationMatches(InstrumentedMethod.METHOD_GETPARAMETERTYPES, invocation); + } + @Test void methodInvokeShouldMatchInvokeHintOnMethod() throws NoSuchMethodException { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_INVOKE) @@ -432,6 +501,10 @@ class InstrumentedMethodTests { RecordedInvocation stringGetDeclaredField; + RecordedInvocation stringGetDeclaredFields; + + RecordedInvocation stringGetFields; + @BeforeEach void setup() throws Exception { this.getPublicField = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) @@ -439,6 +512,10 @@ class InstrumentedMethodTests { .returnValue(PublicField.class.getField("field")).build(); this.stringGetDeclaredField = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELD) .onInstance(String.class).withArgument("value").returnValue(String.class.getDeclaredField("value")).build(); + this.stringGetDeclaredFields = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELDS) + .onInstance(String.class).returnValue(String.class.getDeclaredFields()).build(); + this.stringGetFields = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELDS) + .onInstance(String.class).returnValue(String.class.getFields()).build(); } @Test @@ -461,16 +538,26 @@ class InstrumentedMethodTests { @Test void classGetDeclaredFieldsShouldMatchDeclaredFieldsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELDS).onInstance(String.class).build(); hints.reflection().registerType(String.class, MemberCategory.DECLARED_FIELDS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, invocation); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); } @Test void classGetDeclaredFieldsShouldNotMatchPublicFieldsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELDS).onInstance(String.class).build(); hints.reflection().registerType(String.class, MemberCategory.PUBLIC_FIELDS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, invocation); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); + } + + @Test + void classGetDeclaredFieldsShouldNotMatchTypeHint() { + hints.reflection().registerType(String.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); + } + + @Test + void classGetDeclaredFieldsShouldMatchFieldHint() throws Exception { + hints.reflection().registerField(String.class.getDeclaredField("value")); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); } @Test @@ -531,6 +618,19 @@ class InstrumentedMethodTests { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, invocation); } + + @Test + void classGetFieldsShouldNotMatchTypeHint() { + hints.reflection().registerType(String.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELDS, this.stringGetFields); + } + + @Test + void classGetFieldsShouldMatchFieldHint() throws Exception { + hints.reflection().registerField(String.class.getDeclaredField("value")); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, this.stringGetFields); + } + } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java index 9d35519a0f..c745cff465 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java @@ -212,6 +212,29 @@ public class ReflectionHintsPredicates { .anyMatch(memberCategory -> getTypeHint(hints).getMemberCategories().contains(memberCategory))); } + /** + * Refine the current predicate to only match if a hint is present for any of its constructors. + * @return the refined {@link RuntimeHints} predicate + */ + public Predicate withAnyConstructor() { + return this.and(hints -> getTypeHint(hints).constructors().findAny().isPresent()); + } + + /** + * Refine the current predicate to only match if a hint is present for any of its methods. + * @return the refined {@link RuntimeHints} predicate + */ + public Predicate withAnyMethod() { + return this.and(hints -> getTypeHint(hints).methods().findAny().isPresent()); + } + + /** + * Refine the current predicate to only match if a hint is present for any of its fields. + * @return the refined {@link RuntimeHints} predicate + */ + public Predicate withAnyField() { + return this.and(hints -> getTypeHint(hints).fields().findAny().isPresent()); + } } public abstract static class ExecutableHintPredicate implements Predicate { diff --git a/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java b/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java index 957c58aa1a..aa15264535 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java @@ -18,6 +18,8 @@ package org.springframework.aot.hint.predicate; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.Collections; import java.util.function.Predicate; @@ -45,6 +47,14 @@ class ReflectionHintsPredicatesTests { private static Constructor publicConstructor; + private static Method privateMethod; + + private static Method publicMethod; + + private static Field privateField; + + private static Field publicField; + private final ReflectionHintsPredicates reflection = new ReflectionHintsPredicates(); private final RuntimeHints runtimeHints = new RuntimeHints(); @@ -54,6 +64,10 @@ class ReflectionHintsPredicatesTests { static void setupAll() throws Exception { privateConstructor = SampleClass.class.getDeclaredConstructor(String.class); publicConstructor = SampleClass.class.getConstructor(); + privateMethod = SampleClass.class.getDeclaredMethod("privateMethod"); + publicMethod = SampleClass.class.getMethod("publicMethod"); + privateField = SampleClass.class.getDeclaredField("privateField"); + publicField = SampleClass.class.getField("publicField"); } @Nested @@ -289,6 +303,18 @@ class ReflectionHintsPredicatesTests { assertPredicateMatches(reflection.onConstructor(privateConstructor).invoke()); } + @Test + void reflectionOnAnyConstructorDoesNotMatchTypeReflection() { + runtimeHints.reflection().registerType(SampleClass.class); + assertPredicateDoesNotMatch(reflection.onType(SampleClass.class).withAnyConstructor()); + } + + @Test + void reflectionOnAnyConstructorMatchesConstructorReflection() { + runtimeHints.reflection().registerConstructor(publicConstructor); + assertPredicateMatches(reflection.onType(SampleClass.class).withAnyConstructor()); + } + } @Nested @@ -432,6 +458,18 @@ class ReflectionHintsPredicatesTests { assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); } + @Test + void reflectionOnAnyMethodDoesNotMatchTypeReflection() { + runtimeHints.reflection().registerType(SampleClass.class); + assertPredicateDoesNotMatch(reflection.onType(SampleClass.class).withAnyMethod()); + } + + @Test + void reflectionOnAnyMethodMatchesMethodReflection() { + runtimeHints.reflection().registerMethod(publicMethod); + assertPredicateMatches(reflection.onType(SampleClass.class).withAnyMethod()); + } + } @Nested @@ -505,6 +543,18 @@ class ReflectionHintsPredicatesTests { assertPredicateMatches(reflection.onField(SampleClass.class, "privateField")); } + @Test + void reflectionOnAnyFieldDoesNotMatchTypeReflection() { + runtimeHints.reflection().registerType(SampleClass.class); + assertPredicateDoesNotMatch(reflection.onType(SampleClass.class).withAnyField()); + } + + @Test + void reflectionOnAnyFieldMatchesFieldReflection() { + runtimeHints.reflection().registerField(publicField); + assertPredicateMatches(reflection.onType(SampleClass.class).withAnyField()); + } + } private void assertPredicateMatches(Predicate predicate) {