diff --git a/spring-core-test/spring-core-test.gradle b/spring-core-test/spring-core-test.gradle index 04b6953d4d7..264bc8f7ea8 100644 --- a/spring-core-test/spring-core-test.gradle +++ b/spring-core-test/spring-core-test.gradle @@ -2,9 +2,19 @@ description = "Spring Core Test" dependencies { api(project(":spring-core")) + api("org.junit.jupiter:junit-jupiter-api") api("org.assertj:assertj-core") api("com.thoughtworks.qdox:qdox") compileOnly("org.junit.jupiter:junit-jupiter") compileOnly("org.junit.platform:junit-platform-engine") compileOnly("org.junit.platform:junit-platform-launcher") } + +jar { + manifest { + attributes( + 'Premain-Class': 'org.springframework.aot.agent.RuntimeHintsAgent', + 'Can-Redefine-Classes': 'true' + ) + } +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/HintType.java b/spring-core-test/src/main/java/org/springframework/aot/agent/HintType.java new file mode 100644 index 00000000000..09df5b4753f --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/HintType.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + + +import org.springframework.aot.hint.ClassProxyHint; +import org.springframework.aot.hint.JavaSerializationHint; +import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.ResourceBundleHint; +import org.springframework.aot.hint.ResourcePatternHint; + +/** + * Main types of {@link org.springframework.aot.hint.RuntimeHints}. + *

This allows to sort {@link RecordedInvocation recorded invocations} + * into hint categories. + * + * @author Brian Clozel + * @since 6.0 + */ +public enum HintType { + + /** + * Reflection hint, as described by {@link org.springframework.aot.hint.ReflectionHints}. + */ + REFLECTION(ReflectionHints.class), + + /** + * Resource pattern hint, as described by {@link org.springframework.aot.hint.ResourceHints#resourcePatterns()}. + */ + RESOURCE_PATTERN(ResourcePatternHint.class), + + /** + * Resource bundle hint, as described by {@link org.springframework.aot.hint.ResourceHints#resourceBundles()}. + */ + RESOURCE_BUNDLE(ResourceBundleHint.class), + + /** + * Java serialization hint, as described by {@link org.springframework.aot.hint.JavaSerializationHint}. + */ + JAVA_SERIALIZATION(JavaSerializationHint.class), + + /** + * JDK proxies hint, as described by {@link org.springframework.aot.hint.ProxyHints#jdkProxies()}. + */ + JDK_PROXIES(JdkProxyHint.class), + + /** + * Class proxies hint, as described by {@link org.springframework.aot.hint.ProxyHints#classProxies()}. + */ + CLASS_PROXIES(ClassProxyHint.class); + + private final Class hintClass; + + HintType(Class hintClass) { + this.hintClass = hintClass; + } + + public String hintClassName() { + return this.hintClass.getSimpleName(); + } +} 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 new file mode 100644 index 00000000000..89c6168157a --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java @@ -0,0 +1,527 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.net.URL; +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; + + +/** + * Instrumented version of JDK methods to be used by bytecode rewritten by the {@link RuntimeHintsAgent}. + *

Methods implemented here follow a specific naming pattern "lowercase type name + bridged method name", + * so that the agent can consistently rewrite calls to instrumented methods. + * For example {@code Class#forName(String)} will be here names {@code classforName(String)}. + * + * @author Brian Clozel + * @see InstrumentedMethod + * @deprecated This class should only be used by the runtime-hints agent when instrumenting bytecode + * and is not considered public API. + */ +@Deprecated +public abstract class InstrumentedBridgeMethods { + + private InstrumentedBridgeMethods() { + + } + + /* + * Bridge methods for java.lang.Class + */ + + public static Class classforName(String className) throws ClassNotFoundException { + Class result = null; + try { + result = Class.forName(className); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_FORNAME).withArguments(className).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Class classforName(String className, boolean initialize, ClassLoader loader) throws ClassNotFoundException { + Class result = null; + try { + result = Class.forName(className, initialize, loader); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_FORNAME).withArguments(className, initialize, loader).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Constructor[] classgetConstructors(Class clazz) throws SecurityException { + Constructor[] result = null; + try { + result = clazz.getConstructors(); + } + finally { + RecordedInvocationsPublisher.publish(RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTORS).onInstance(clazz).returnValue(result).build()); + } + return result; + } + + public static Constructor classgetConstructor(Class clazz, Class[] parameterTypes) throws NoSuchMethodException { + Constructor result = null; + try { + result = clazz.getConstructor(parameterTypes); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTOR) + .onInstance(clazz).withArgument(parameterTypes).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Constructor[] classgetDeclaredConstructors(Class clazz) throws SecurityException { + Constructor[] result = null; + try { + result = clazz.getDeclaredConstructors(); + } + finally { + RecordedInvocationsPublisher.publish(RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS).onInstance(clazz).returnValue(result).build()); + } + return result; + } + + public static Constructor classgetDeclaredConstructor(Class clazz, Class[] parameterTypes) throws NoSuchMethodException { + Constructor result = null; + try { + result = clazz.getDeclaredConstructor(parameterTypes); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR) + .onInstance(clazz).withArgument(parameterTypes).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Method[] classgetMethods(Class clazz) throws SecurityException { + Method[] result = null; + try { + result = clazz.getMethods(); + } + finally { + RecordedInvocationsPublisher.publish(RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS) + .onInstance(clazz).returnValue(result).build()); + } + return result; + } + + public static Method classgetMethod(Class clazz, String name, Class... parameterTypes) throws NoSuchMethodException, SecurityException { + Method result = null; + try { + result = clazz.getMethod(name, parameterTypes); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHOD) + .onInstance(clazz).withArguments(name, parameterTypes).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Method classgetDeclaredMethod(Class clazz, String name, Class... params) + throws SecurityException, NoSuchMethodException { + Method result = null; + try { + result = clazz.getDeclaredMethod(name, params); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDMETHOD) + .onInstance(clazz).withArguments(name, params).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Method[] classgetDeclaredMethods(Class clazz) { + Method[] result = clazz.getDeclaredMethods(); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDMETHODS) + .onInstance(clazz).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Class[] classgetDeclaredClasses(Class clazz) { + Class[] result = clazz.getDeclaredClasses(); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCLASSES) + .onInstance(clazz).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Class[] classgetClasses(Class clazz) { + Class[] result = clazz.getClasses(); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCLASSES) + .onInstance(clazz).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Field[] classgetDeclaredFields(Class clazz) { + Field[] result = clazz.getDeclaredFields(); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELDS) + .onInstance(clazz).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Field classgetDeclaredField(Class clazz, String name) throws NoSuchFieldException { + Field result = null; + try { + result = clazz.getDeclaredField(name); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELD) + .onInstance(clazz).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + public static Field[] classgetFields(Class clazz) { + Field[] result = clazz.getFields(); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELDS) + .onInstance(clazz).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Field classgetField(Class clazz, String name) throws NoSuchFieldException { + Field result = null; + try { + result = clazz.getField(name); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) + .onInstance(clazz).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + @Nullable + public static URL classgetResource(Class clazz, String name) { + URL result = clazz.getResource(name); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) + .onInstance(clazz).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + @Nullable + public static InputStream classgetResourceAsStream(Class clazz, String name) { + InputStream result = clazz.getResourceAsStream(name); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCEASSTREAM) + .onInstance(clazz).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + /* + * Bridge methods for java.lang.ClassLoader + */ + + public static Class classloaderloadClass(ClassLoader classLoader, String name) throws ClassNotFoundException { + Class result = null; + try { + result = classLoader.loadClass(name); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_LOADCLASS) + .onInstance(classLoader).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + @Nullable + public static URL classloadergetResource(ClassLoader classLoader, String name) { + URL result = classLoader.getResource(name); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_GETRESOURCE) + .onInstance(classLoader).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + @Nullable + public static InputStream classloadergetResourceAsStream(ClassLoader classLoader, String name) { + InputStream result = classLoader.getResourceAsStream(name); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_GETRESOURCEASSTREAM) + .onInstance(classLoader).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Stream classloaderresources(ClassLoader classLoader, String name) { + Stream result = classLoader.resources(name); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_RESOURCES) + .onInstance(classLoader).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + public static Enumeration classloadergetResources(ClassLoader classLoader, String name) throws IOException { + Enumeration result = null; + try { + result = classLoader.getResources(name); + } + finally { + RecordedInvocationsPublisher.publish(RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_GETRESOURCES) + .onInstance(classLoader).withArgument(name).returnValue(result).build()); + } + return result; + } + + /* + * Bridge methods for java.lang.Constructor + */ + public static Object constructornewInstance(Constructor constructor, Object... arguments) throws InvocationTargetException, InstantiationException, IllegalAccessException { + Object result = null; + boolean accessibilityChanged = false; + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE) + .onInstance(constructor).withArguments(arguments); + try { + if (!Modifier.isPublic(constructor.getModifiers()) || + !Modifier.isPublic(constructor.getDeclaringClass().getModifiers()) || !constructor.canAccess(null)) { + constructor.setAccessible(true); + accessibilityChanged = true; + } + result = constructor.newInstance(arguments); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + if (accessibilityChanged) { + constructor.setAccessible(false); + } + } + return result; + } + + /* + * Bridge methods for java.lang.reflect.Method + */ + + public static Object methodinvoke(Method method, Object object, Object... arguments) throws InvocationTargetException, IllegalAccessException { + Object result = null; + try { + result = method.invoke(object, arguments); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_INVOKE) + .onInstance(method).withArguments(object, arguments).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + } + return result; + } + + /* + * Bridge methods for java.lang.reflect.Field + */ + + public static Object fieldget(Field field, Object object) throws IllegalArgumentException, IllegalAccessException { + Object result = null; + boolean accessibilityChanged = false; + try { + if ((!Modifier.isPublic(field.getModifiers()) || + !Modifier.isPublic(field.getDeclaringClass().getModifiers())) && !field.canAccess(object)) { + field.setAccessible(true); + accessibilityChanged = true; + } + result = field.get(object); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.FIELD_GET) + .onInstance(field).withArguments(object).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + if (accessibilityChanged) { + field.setAccessible(false); + } + } + return result; + } + + public static void fieldset(Field field, Object object, Object value) throws IllegalArgumentException, IllegalAccessException { + boolean accessibilityChanged = false; + try { + if ((!Modifier.isPublic(field.getModifiers()) || + !Modifier.isPublic(field.getDeclaringClass().getModifiers())) && !field.canAccess(object)) { + field.setAccessible(true); + accessibilityChanged = true; + } + field.set(object, value); + } + finally { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.FIELD_SET) + .onInstance(field).withArguments(object, value).build(); + RecordedInvocationsPublisher.publish(invocation); + if (accessibilityChanged) { + field.setAccessible(false); + } + } + } + + + /* + * Bridge methods for java.lang.Module + */ + + public static InputStream modulegetResourceAsStream(Module module, String name) throws IOException { + InputStream result = module.getResourceAsStream(name); + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.MODULE_GETRESOURCEASSTREAM) + .onInstance(module).withArgument(name).returnValue(result).build(); + RecordedInvocationsPublisher.publish(invocation); + return result; + } + + /* + * Bridge methods for java.util.ResourceBundle + */ + + public static ResourceBundle resourcebundlegetBundle(String baseName) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArgument(baseName); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle(String baseName, ResourceBundle.Control control) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, control); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, control); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle(String baseName, Locale locale) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, locale); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, locale); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle(String baseName, Module module) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, module); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, module); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle(String baseName, Locale targetLocale, Module module) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, targetLocale, module); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, targetLocale, module); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle( String baseName, Locale targetLocale, ResourceBundle.Control control) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, targetLocale, control); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, targetLocale, control); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle(String baseName, Locale locale, ClassLoader loader) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, locale, loader); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, locale, loader); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + public static ResourceBundle resourcebundlegetBundle(String baseName, Locale targetLocale, ClassLoader loader, ResourceBundle.Control control) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE).withArguments(baseName, targetLocale, loader, control); + ResourceBundle result = null; + try { + result = ResourceBundle.getBundle(baseName, targetLocale, loader, control); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + + /* + * Bridge methods for JDK Proxies + */ + + public static Object proxynewProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) { + RecordedInvocation.Builder builder = RecordedInvocation.of(InstrumentedMethod.PROXY_NEWPROXYINSTANCE) + .withArguments(loader, interfaces, h); + Object result = null; + try { + result = Proxy.newProxyInstance(loader, interfaces, h); + } + finally { + RecordedInvocationsPublisher.publish(builder.returnValue(result).build()); + } + return result; + } + +} 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 new file mode 100644 index 00000000000..f61ef5286f9 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java @@ -0,0 +1,397 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ResourceBundle; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsPredicates; +import org.springframework.aot.hint.TypeReference; + +/** + * Java method that is instrumented by the {@link RuntimeHintsAgent}. + *

All their {@link RecordedInvocation invocations are recorded} by the agent at runtime. + * We can then verify that the {@link RuntimeHints} configuration + * {@link #matcher(RecordedInvocation) is matching} the runtime behavior of the codebase. + * + * @author Brian Clozel + * @see org.springframework.aot.hint.RuntimeHintsPredicates + */ +enum InstrumentedMethod { + + /* + * Reflection calls + */ + + /** + * {@link Class#forName(String)} and {@link Class#forName(String, boolean, ClassLoader)}. + */ + CLASS_FORNAME(Class.class, "forName", HintType.REFLECTION, + invocation -> { + String className = invocation.getArgument(0); + return RuntimeHintsPredicates.reflection().onType(TypeReference.of(className)); + }), + + /** + * {@link Class#getClasses()}. + */ + CLASS_GETCLASSES(Class.class, "getClasses", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType) + .withAnyMemberCategory(MemberCategory.DECLARED_CLASSES, MemberCategory.PUBLIC_CLASSES); + } + ), + + /** + * {@link Class#getConstructor(Class[])}. + */ + CLASS_GETCONSTRUCTOR(Class.class, "getConstructor", HintType.REFLECTION, + invocation -> { + Constructor constructor = invocation.getReturnValue(); + if (constructor == null) { + return runtimeHints -> false; + } + return RuntimeHintsPredicates.reflection().onConstructor(constructor).introspect(); + } + ), + + /** + * {@link Class#getConstructors()}. + */ + CLASS_GETCONSTRUCTORS(Class.class, "getConstructors", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType).withAnyMemberCategory( + MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + ), + + /** + * {@link Class#getDeclaredClasses()}. + */ + CLASS_GETDECLAREDCLASSES(Class.class, "getDeclaredClasses", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_CLASSES); + } + ), + + /** + * {@link Class#getDeclaredConstructor(Class[])}. + */ + CLASS_GETDECLAREDCONSTRUCTOR(Class.class, "getDeclaredConstructor", HintType.REFLECTION, + invocation -> { + Constructor constructor = invocation.getReturnValue(); + if (constructor == null) { + return runtimeHints -> false; + } + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS) + .or(RuntimeHintsPredicates.reflection().onConstructor(constructor).introspect()); + } + ), + + /** + * {@link Class#getDeclaredConstructors()}. + */ + CLASS_GETDECLAREDCONSTRUCTORS(Class.class, "getDeclaredConstructors", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType) + .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + }), + + /** + * {@link Class#getDeclaredField(String)}. + */ + CLASS_GETDECLAREDFIELD(Class.class, "getDeclaredField", HintType.REFLECTION, + invocation -> { + Field field = invocation.getReturnValue(); + if (field == null) { + return runtimeHints -> false; + } + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS) + .or(RuntimeHintsPredicates.reflection().onField(field)); + } + ), + + /** + * {@link Class#getDeclaredFields()}. + */ + CLASS_GETDECLAREDFIELDS(Class.class, "getDeclaredFields", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS); + } + ), + + /** + * {@link Class#getDeclaredMethod(String, Class[])}. + */ + CLASS_GETDECLAREDMETHOD(Class.class, "getDeclaredMethod", HintType.REFLECTION, + invocation -> { + Method method = invocation.getReturnValue(); + if (method == null) { + return runtimeHints -> false; + } + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType) + .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) + .or(RuntimeHintsPredicates.reflection().onMethod(method).introspect()); + } + ), + + /** + * {@link Class#getDeclaredMethods()}. + */ + CLASS_GETDECLAREDMETHODS(Class.class, "getDeclaredMethods", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType) + .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS); + } + ), + + /** + * {@link Class#getField(String)}. + */ + CLASS_GETFIELD(Class.class, "getField", HintType.REFLECTION, + invocation -> { + Field field = invocation.getReturnValue(); + if (field == null) { + return runtimeHints -> false; + } + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.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())); + }), + + /** + * {@link Class#getFields()}. + */ + CLASS_GETFIELDS(Class.class, "getFields", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType) + .withAnyMemberCategory(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS); + } + ), + + /** + * {@link Class#getMethod(String, Class[])}. + */ + CLASS_GETMETHOD(Class.class, "getMethod", HintType.REFLECTION, + invocation -> { + Method method = invocation.getReturnValue(); + if (method == null) { + return runtimeHints -> false; + } + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.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()); + } + ), + + /** + * {@link Class#getMethods()}. + */ + CLASS_GETMETHODS(Class.class, "getMethods", HintType.REFLECTION, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + return RuntimeHintsPredicates.reflection().onType(thisType).withAnyMemberCategory( + MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS); + } + ), + + /** + * {@link ClassLoader#loadClass(String)}. + */ + CLASSLOADER_LOADCLASS(ClassLoader.class, "loadClass", HintType.REFLECTION, + invocation -> { + Class klass = invocation.getReturnValue(); + if (klass == null) { + return runtimeHints -> false; + } + return RuntimeHintsPredicates.reflection().onType(klass); + }), + + /** + * {@link Constructor#newInstance(Object...)}. + */ + CONSTRUCTOR_NEWINSTANCE(Constructor.class, "newInstance", HintType.REFLECTION, + invocation -> RuntimeHintsPredicates.reflection().onConstructor(invocation.getInstance()).invoke()), + + /** + * {@link Method#invoke(Object, Object...)}. + */ + METHOD_INVOKE(Method.class, "invoke", HintType.REFLECTION, + invocation -> RuntimeHintsPredicates.reflection().onMethod(invocation.getInstance()).invoke()), + + /** + * {@link Field#get(Object)}. + */ + FIELD_GET(Field.class, "get", HintType.REFLECTION, + invocation -> RuntimeHintsPredicates.reflection().onField(invocation.getInstance())), + + /** + * {@link Field#set(Object, Object)}. + */ + FIELD_SET(Field.class, "set", HintType.REFLECTION, + invocation -> RuntimeHintsPredicates.reflection().onField(invocation.getInstance()).allowWrite()), + + + /* + * Resource bundle calls + */ + + /** + * {@link java.util.ResourceBundle#getBundle(String)}. + */ + RESOURCEBUNDLE_GETBUNDLE(ResourceBundle.class, "getBundle", HintType.RESOURCE_BUNDLE, + invocation -> { + String bundleName = invocation.getArgument(0); + return RuntimeHintsPredicates.resource().forBundle(bundleName); + }), + + /* + * Resource pattern calls + */ + + /** + * {@link Class#getResource(String)}. + */ + CLASS_GETRESOURCE(Class.class, "getResource", HintType.RESOURCE_PATTERN, + invocation -> { + TypeReference thisType = invocation.getInstanceTypeReference(); + String resourceName = invocation.getArgument(0); + return RuntimeHintsPredicates.resource().forResource(thisType, resourceName); + }), + + /** + * {@link Class#getResourceAsStream(String)}. + */ + CLASS_GETRESOURCEASSTREAM(Class.class, "getResourceAsStream", HintType.RESOURCE_PATTERN, + CLASS_GETRESOURCE.hintsMatcherGenerator), + + + /** + * {@link java.lang.ClassLoader#getResource(String)}. + */ + CLASSLOADER_GETRESOURCE(ClassLoader.class, "getResource", HintType.RESOURCE_PATTERN, + invocation -> { + String resourceName = invocation.getArgument(0); + return RuntimeHintsPredicates.resource().forResource(resourceName); + }), + + /** + * {@link java.lang.ClassLoader#getResourceAsStream(String)}. + */ + CLASSLOADER_GETRESOURCEASSTREAM(ClassLoader.class, "getResourceAsStream", HintType.RESOURCE_PATTERN, + CLASSLOADER_GETRESOURCE.hintsMatcherGenerator), + + /** + * {@link java.lang.ClassLoader#getResources(String)}. + */ + CLASSLOADER_GETRESOURCES(ClassLoader.class, "getResources", HintType.RESOURCE_PATTERN, + CLASSLOADER_GETRESOURCE.hintsMatcherGenerator), + + /** + * {@link java.lang.Module#getResourceAsStream(String)}. + */ + MODULE_GETRESOURCEASSTREAM(Module.class, "getResourceAsStream", HintType.RESOURCE_PATTERN, + CLASSLOADER_GETRESOURCE.hintsMatcherGenerator), + + /** + * {@link java.lang.ClassLoader#resources(String)}. + */ + CLASSLOADER_RESOURCES(ClassLoader.class, "resources", HintType.RESOURCE_PATTERN, + CLASSLOADER_GETRESOURCE.hintsMatcherGenerator), + + /* + * JDK Proxy calls + */ + + /** + * {@link Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)}. + */ + PROXY_NEWPROXYINSTANCE(Proxy.class, "newProxyInstance", HintType.JDK_PROXIES, + invocation -> { + Class[] classes = invocation.getArgument(1); + return RuntimeHintsPredicates.proxies().forInterfaces(classes); + }); + + + private final MethodReference methodReference; + + private final HintType hintType; + + private final Function> hintsMatcherGenerator; + + InstrumentedMethod(Class klass, String methodName, HintType hintType, Function> hintsMatcherGenerator) { + this.methodReference = MethodReference.of(klass, methodName); + this.hintType = hintType; + this.hintsMatcherGenerator = hintsMatcherGenerator; + } + + /** + * Return a {@link MethodReference reference} to the method being instrumented. + */ + MethodReference methodReference() { + return this.methodReference; + } + + /** + * Return the type of {@link RuntimeHints hint} needed ofr the current instrumented method. + */ + HintType getHintType() { + return this.hintType; + } + + /** + * Return a predicate that matches if the current invocation is covered by the given hints. + *

A runtime invocation for reflection, resources, etc. can be backed by different hints. + * For example, {@code MyClass.class.getMethod("sample", null)} can be backed by a reflection + * hint on this method only, or a reflection hint on all public/declared methods of the class. + * @param invocation the current invocation of the instrumented method + */ + Predicate matcher(RecordedInvocation invocation) { + return this.hintsMatcherGenerator.apply(invocation); + } + + private static Predicate hasReturnValue(RecordedInvocation invocation) { + return runtimeHints -> invocation.getReturnValue() != null; + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java new file mode 100644 index 00000000000..c49f7456a0d --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; +import java.util.Arrays; + +import org.springframework.asm.ClassReader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * ASM {@link ClassFileTransformer} that delegates bytecode transformations + * to a {@link InvocationsRecorderClassVisitor class visitor} if and only + * if the class is in the list of packages considered for instrumentation. + * + * @author Brian Clozel + * @see InvocationsRecorderClassVisitor + */ +class InvocationsRecorderClassTransformer implements ClassFileTransformer { + + private static final String AGENT_PACKAGE = InvocationsRecorderClassTransformer.class.getPackageName().replace('.', '/'); + + private static final String AOT_DYNAMIC_CLASSLOADER = "org/springframework/aot/test/generator/compile/DynamicClassLoader"; + + private final String[] instrumentedPackages; + + private final String[] ignoredPackages; + + public InvocationsRecorderClassTransformer(String[] instrumentedPackages, String[] ignoredPackages) { + Assert.notNull(instrumentedPackages, "instrumentedPackages should not be null"); + Assert.notNull(ignoredPackages, "ignoredPackages should not be null"); + this.instrumentedPackages = rewriteToAsmFormat(instrumentedPackages); + this.ignoredPackages = rewriteToAsmFormat(ignoredPackages); + } + + private String[] rewriteToAsmFormat(String[] packages) { + return Arrays.stream(packages).map(pack -> pack.replace('.', '/')) + .toArray(String[]::new); + } + + @Override + public byte[] transform(@Nullable ClassLoader classLoader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + if (isTransformationCandidate(classLoader, className)) { + return attemptClassTransformation(classfileBuffer); + } + return classfileBuffer; + } + + private boolean isTransformationCandidate(@Nullable ClassLoader classLoader, String className) { + // Ignore system classes + if (classLoader == null) { + return false; + } + // Ignore agent classes and spring-core-test DynamicClassLoader + else if (className.startsWith(AGENT_PACKAGE) || className.equals(AOT_DYNAMIC_CLASSLOADER)) { + return false; + } + // Do not instrument CGlib classes + else if (className.contains("$$")) { + return false; + } + // Only some packages are instrumented + else { + for (String ignoredPackage : this.ignoredPackages) { + if (className.startsWith(ignoredPackage)) { + return false; + } + } + for (String instrumentedPackage : this.instrumentedPackages) { + if (className.startsWith(instrumentedPackage)) { + return true; + } + } + } + return false; + } + + private byte[] attemptClassTransformation(byte[] classfileBuffer) { + ClassReader fileReader = new ClassReader(classfileBuffer); + InvocationsRecorderClassVisitor classVisitor = new InvocationsRecorderClassVisitor(); + try { + fileReader.accept(classVisitor, 0); + } + catch (Exception ex) { + ex.printStackTrace(); + return classfileBuffer; + } + if (classVisitor.isTransformed()) { + return classVisitor.getTransformedClassBuffer(); + } + return classfileBuffer; + } +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java new file mode 100644 index 00000000000..32cb0ddc0a4 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.ClassWriter; +import org.springframework.asm.Handle; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; + +/** + * ASM {@link ClassVisitor} that rewrites a known set of method invocations + * to call instrumented bridge methods for {@link RecordedInvocationsPublisher recording purposes}. + *

The bridge methods are located in the {@link InstrumentedBridgeMethods} class. + * + * @author Brian Clozel + * @see InstrumentedMethod + */ +class InvocationsRecorderClassVisitor extends ClassVisitor implements Opcodes { + + private boolean isTransformed; + + private final ClassWriter classWriter; + + public InvocationsRecorderClassVisitor() { + this(new ClassWriter(ClassWriter.COMPUTE_MAXS)); + } + + private InvocationsRecorderClassVisitor(ClassWriter classWriter) { + super(SpringAsmInfo.ASM_VERSION, classWriter); + this.classWriter = classWriter; + } + + public boolean isTransformed() { + return this.isTransformed; + } + + public byte[] getTransformedClassBuffer() { + return this.classWriter.toByteArray(); + } + + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + return new InvocationsRecorderMethodVisitor(mv); + } + + @SuppressWarnings("deprecation") + class InvocationsRecorderMethodVisitor extends MethodVisitor implements Opcodes { + + private static final String INSTRUMENTED_CLASS = InstrumentedBridgeMethods.class.getName().replace('.', '/'); + + private static final Set instrumentedMethods = new HashSet<>(); + + static { + for (InstrumentedMethod method : InstrumentedMethod.values()) { + MethodReference methodReference = method.methodReference(); + instrumentedMethods.add(methodReference.getClassName().replace('.', '/') + + "#" + methodReference.getMethodName()); + } + } + + public InvocationsRecorderMethodVisitor(MethodVisitor mv) { + super(SpringAsmInfo.ASM_VERSION, mv); + } + + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (isOpcodeSupported(opcode) && shouldRecordMethodCall(owner, name)) { + String instrumentedMethodName = rewriteMethodName(owner, name); + mv.visitMethodInsn(INVOKESTATIC, INSTRUMENTED_CLASS, instrumentedMethodName, + rewriteDescriptor(opcode, owner, name, descriptor), false); + isTransformed = true; + } + else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + + private boolean isOpcodeSupported(int opcode) { + return Opcodes.INVOKEVIRTUAL == opcode || Opcodes.INVOKESTATIC == opcode; + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + for (int i = 0; i < bootstrapMethodArguments.length; i++) { + if (bootstrapMethodArguments[i] instanceof Handle argumentHandle) { + if (shouldRecordMethodCall(argumentHandle.getOwner(), argumentHandle.getName())) { + String instrumentedMethodName = rewriteMethodName(argumentHandle.getOwner(), argumentHandle.getName()); + String newDescriptor = rewriteDescriptor(argumentHandle.getTag(), argumentHandle.getOwner(), argumentHandle.getName(), argumentHandle.getDesc()); + bootstrapMethodArguments[i] = new Handle(H_INVOKESTATIC, INSTRUMENTED_CLASS, instrumentedMethodName, newDescriptor, false); + isTransformed = true; + } + } + } + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + + private boolean shouldRecordMethodCall(String owner, String method) { + String methodReference = owner + "#" + method; + return instrumentedMethods.contains(methodReference); + } + + private String rewriteMethodName(String owner, String methodName) { + int classIndex = owner.lastIndexOf('/'); + return owner.substring(classIndex + 1).toLowerCase() + methodName; + } + + private String rewriteDescriptor(int opcode, String owner, String name, String descriptor) { + return (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.H_INVOKESTATIC) ? descriptor : "(L" + owner + ";" + descriptor.substring(1); + } + + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java b/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java new file mode 100644 index 00000000000..d1d790d71ce --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.util.Objects; + +/** + * Reference to a Java method, identified by its owner class and the method name. + * + *

This implementation is ignoring parameters on purpose, as the goal here is + * to inform developers on invocations requiring additional + * {@link org.springframework.aot.hint.RuntimeHints} configuration, not + * precisely identifying a method. + * + * @author Brian Clozel + * @since 6.0 + */ +public final class MethodReference { + + private final String className; + + private final String methodName; + + private MethodReference(String className, String methodName) { + this.className = className; + this.methodName = methodName; + } + + public static MethodReference of(Class klass, String methodName) { + return new MethodReference(klass.getCanonicalName(), methodName); + } + + /** + * Return the declaring class for this method. + * @return the declaring class name + */ + public String getClassName() { + return this.className; + } + + /** + * Return the name of the method. + * @return the method name + */ + public String getMethodName() { + return this.methodName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MethodReference that = (MethodReference) o; + return this.className.equals(that.className) && this.methodName.equals(that.methodName); + } + + @Override + public int hashCode() { + return Objects.hash(this.className, this.methodName); + } + + @Override + public String toString() { + return this.className + '#' + this.methodName; + } +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java new file mode 100644 index 00000000000..cf31f53fc0d --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Record of an invocation of a method relevant to {@link org.springframework.aot.hint.RuntimeHints}. + *

The {@link RuntimeHintsAgent} instruments bytecode and intercepts invocations of + * {@link InstrumentedMethod specific methods}; invocations are recorded during test execution + * to match them against an existing {@link org.springframework.aot.hint.RuntimeHints} configuration. + * + * @author Brian Clozel + * @since 6.0 + */ +public final class RecordedInvocation { + + @Nullable + private final Object instance; + + private final InstrumentedMethod instrumentedMethod; + + private final Object[] arguments; + + @Nullable + private final Object returnValue; + + private final List stackFrames; + + private RecordedInvocation(InstrumentedMethod instrumentedMethod, @Nullable Object instance, + Object[] arguments, @Nullable Object returnValue, List stackFrames) { + this.instance = instance; + this.instrumentedMethod = instrumentedMethod; + this.arguments = arguments; + this.returnValue = returnValue; + this.stackFrames = stackFrames; + } + + /** + * Initialize a builder for the given {@link InstrumentedMethod}. + * @param instrumentedMethod the instrumented method + * @return a builder + */ + public static Builder of(InstrumentedMethod instrumentedMethod) { + Assert.notNull(instrumentedMethod, "InstrumentedMethod must not be null"); + return new Builder(instrumentedMethod); + } + + /** + * Return the category of {@link RuntimeHints} this invocation relates to. + * @return the hint type + */ + public HintType getHintType() { + return this.instrumentedMethod.getHintType(); + } + + /** + * Return a simple representation of the method invoked here. + * @return the method reference + */ + public MethodReference getMethodReference() { + return this.instrumentedMethod.methodReference(); + } + + /** + * Return the stack trace of the current invocation. + * @return the stack frames + */ + public Stream getStackFrames() { + return this.stackFrames.stream(); + } + + /** + * Return the instance of the object being invoked. + * @return the object instance + * @throws IllegalStateException in case of static invocations (there is no {@code this}) + */ + @SuppressWarnings("unchecked") + public T getInstance() { + Assert.notNull(this.instance, "Cannot resolve 'this' for static invocations"); + return (T) this.instance; + } + + /** + * Return the Type reference of the object being invoked. + * @return the instance type reference, or {@code null} + * @throws IllegalStateException in case of static invocations (there is no {@code this}) + */ + public TypeReference getInstanceTypeReference() { + Assert.notNull(this.instance, "Cannot resolve 'this' for static invocations"); + if (this.instance instanceof Class) { + return TypeReference.of((Class) this.instance); + } + return TypeReference.of(this.instance.getClass()); + } + + /** + * Return the argument values used for the current reflection invocation. + * @return the invocation arguments + */ + public List getArguments() { + return Arrays.asList(this.arguments); + } + + /** + * Return the argument value at the given index used for the current reflection invocation. + * @param index the parameter index + * @return the argument at the given index + */ + @SuppressWarnings("unchecked") + public T getArgument(int index) { + return (T) this.arguments[index]; + } + + /** + * Return the types of the arguments used for the current reflection invocation. + * @return the argument types + */ + public List getArgumentTypes() { + return getArgumentTypes(0); + } + + /** + * Return the types of the arguments used for the current reflection invocation, + * starting from the given index. + * @return the argument types, starting at the given index + */ + public List getArgumentTypes(int index) { + return Arrays.stream(this.arguments).skip(index).map(param -> TypeReference.of(param.getClass())).collect(Collectors.toList()); + } + + /** + * Return the value actually returned by the invoked method. + * @return the value returned by the invocation + */ + @SuppressWarnings("unchecked") + @Nullable + public T getReturnValue() { + return (T) this.returnValue; + } + + /** + * Whether the given hints cover the current invocation. + *

If the given hint doesn't match this invocation might fail at execution time depending on the target runtime. + * @return whether the given hints match + */ + public boolean matches(RuntimeHints hints) { + return this.instrumentedMethod.matcher(this).test(hints); + } + + /** + * Builder for {@link RecordedInvocation}. + */ + public static class Builder { + + @Nullable + private Object instance; + + private final InstrumentedMethod instrumentedMethod; + + private Object[] arguments = new Object[0]; + + @Nullable + private Object returnValue; + + + Builder(InstrumentedMethod instrumentedMethod) { + this.instrumentedMethod = instrumentedMethod; + } + + /** + * Set the {@code this} object instance used for this invocation. + * @param instance the current object instance, {@code null} in case of static invocations + * @return {@code this}, to facilitate method chaining + */ + public Builder onInstance(Object instance) { + this.instance = instance; + return this; + } + + /** + * Use the given object as the unique argument. + * @param argument the invocation argument + * @return {@code this}, to facilitate method chaining + */ + public Builder withArgument(@Nullable Object argument) { + if (argument != null) { + this.arguments = new Object[] {argument}; + } + return this; + } + + /** + * Use the given objects as the invocation arguments. + * @param arguments the invocation arguments + * @return {@code this}, to facilitate method chaining + */ + public Builder withArguments(@Nullable Object... arguments) { + if (arguments != null) { + this.arguments = arguments; + } + return this; + } + + /** + * Use the given object as the return value for the invocation. + * @param returnValue the return value + * @return {@code this}, to facilitate method chaining + */ + public Builder returnValue(@Nullable Object returnValue) { + this.returnValue = returnValue; + return this; + } + + /** + * Create a {@link RecordedInvocation} based on the state of this builder. + * @return a recorded invocation + */ + public RecordedInvocation build() { + List stackFrames = StackWalker.getInstance().walk(stream -> stream + .dropWhile(stackFrame -> stackFrame.getClassName().startsWith(getClass().getPackageName())) + .collect(Collectors.toList())); + return new RecordedInvocation(this.instrumentedMethod, this.instance, this.arguments, this.returnValue, stackFrames); + } + + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsListener.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsListener.java new file mode 100644 index 00000000000..4c80b4e28d7 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +/** + * Listener for {@link RecordedInvocation invocations recorded} by the {@link RuntimeHintsAgent}. + * + * @author Brian Clozel + * @since 6.0 + */ +@FunctionalInterface +public interface RecordedInvocationsListener { + + /** + * Called when an {@link RecordedInvocation invocation has been recorded} by the Java Agent. + * @param invocation the recorded invocation. + */ + void onInvocation(RecordedInvocation invocation); + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsPublisher.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsPublisher.java new file mode 100644 index 00000000000..715ccf7f371 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsPublisher.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; + +/** + * Publishes invocations on method relevant to {@link RuntimeHints}, + * as they are recorded by the {@link RuntimeHintsAgent}. + *

Components interested in this can {@link #addListener(RecordedInvocationsListener) register} + * and {@link #removeListener(RecordedInvocationsListener) deregister} themselves at any point at runtime. + * + * @author Brian Clozel + * @since 6.0 + */ +public abstract class RecordedInvocationsPublisher { + + private static final Deque LISTENERS = new ArrayDeque<>(); + + private RecordedInvocationsPublisher() { + + } + + /** + * Register the given invocations listener. + * @param listener the listener to be notified about recorded invocations + */ + public static void addListener(RecordedInvocationsListener listener) { + LISTENERS.addLast(listener); + } + + /** + * Deregister the given invocations listener. + * @param listener the listener that was notified about recorded invocations + */ + public static void removeListener(RecordedInvocationsListener listener) { + LISTENERS.remove(listener); + } + + /** + * Record an invocation on reflection methods covered by {@link ReflectionHints}. + */ + static void publish(RecordedInvocation invocation) { + LISTENERS.forEach(listener -> listener.onInvocation(invocation)); + } + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java new file mode 100644 index 00000000000..a0978531a50 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Java Agent that records method invocations related to {@link RuntimeHints} metadata. + *

This agent uses {@link java.lang.instrument.ClassFileTransformer class transformers} + * that modify bytecode to intercept and record method invocations at runtime. + *

By default, this agent only instruments code in the {@code org.springframework} package. + * Instrumented packages can be configured by passing an argument string to the {@code -javaagent} + * option, as a comma-separated list of packages to instrument prefixed with {@code "+"} + * and packages to ignore prefixed with {@code "-"}: + *

+ *   -javaagent:/path/to/spring-runtimehints-agent.jar=+org.springframework,-io.spring,+org.example")
+ * 
+ * + * @author Brian Clozel + * @since 6.0 + * @see InvocationsRecorderClassTransformer + */ +public final class RuntimeHintsAgent { + + private static boolean loaded = false; + + private RuntimeHintsAgent() { + + } + + public static void premain(@Nullable String agentArgs, Instrumentation inst) { + loaded = true; + ParsedArguments arguments = ParsedArguments.parse(agentArgs); + InvocationsRecorderClassTransformer transformer = new InvocationsRecorderClassTransformer( + arguments.getInstrumentedPackages(), arguments.getIgnoredPackages()); + inst.addTransformer(transformer); + } + + /** + * Static accessor for detecting whether the agent is loaded in the current JVM. + * @return whether the agent is active for the current JVM + */ + public static boolean isLoaded() { + return loaded; + } + + private final static class ParsedArguments { + + List instrumentedPackages; + + List ignoredPackages; + + private ParsedArguments(List instrumentedPackages, List ignoredPackages) { + this.instrumentedPackages = instrumentedPackages; + this.ignoredPackages = ignoredPackages; + } + + public String[] getInstrumentedPackages() { + return this.instrumentedPackages.toArray(new String[0]); + } + + public String[] getIgnoredPackages() { + return this.ignoredPackages.toArray(new String[0]); + } + + static ParsedArguments parse(@Nullable String agentArgs) { + List included = new ArrayList<>(); + List excluded = new ArrayList<>(); + if (StringUtils.hasText(agentArgs)) { + for(String argument : agentArgs.split(",")) { + if (argument.startsWith("+")) { + included.add(argument.substring(1)); + } + else if (argument.startsWith("-")) { + excluded.add(argument.substring(1)); + } + else { + throw new IllegalArgumentException("Cannot parse agent arguments ["+agentArgs+"]"); + } + } + } + else { + included.add("org.springframework"); + } + return new ParsedArguments(included, excluded); + } + + } +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java b/spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java new file mode 100644 index 00000000000..1ea09811ae1 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for recording method invocations relevant to {@link org.springframework.aot.hint.RuntimeHints} metadata. + */ +@NonNullApi +@NonNullFields +package org.springframework.aot.agent; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; 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 new file mode 100644 index 00000000000..b80f1ec4a93 --- /dev/null +++ b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java @@ -0,0 +1,650 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.agent; + +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InstrumentedMethod}. + * + * @author Brian Clozel + */ +class InstrumentedMethodTests { + + private RuntimeHints hints = new RuntimeHints(); + + + @Nested + class ClassReflectionInstrumentationTests { + + RecordedInvocation stringGetClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCLASSES) + .onInstance(String.class).returnValue(String.class.getClasses()).build(); + + RecordedInvocation stringGetDeclaredClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCLASSES) + .onInstance(String.class).returnValue(String.class.getDeclaredClasses()).build(); + + @Test + void classForNameShouldMatchReflectionOnType() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_FORNAME) + .withArgument("java.lang.String").returnValue(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> { + }); + assertThatInvocationMatches(InstrumentedMethod.CLASS_FORNAME, invocation); + } + + @Test + void classGetClassesShouldNotMatchReflectionOnType() { + hints.reflection().registerType(String.class, typeHint -> { + }); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses); + } + + @Test + void classGetClassesShouldMatchPublicClasses() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_CLASSES)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses); + } + + @Test + void classGetClassesShouldMatchDeclaredClasses() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_CLASSES)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses); + } + + @Test + void classGetDeclaredClassesShouldMatchDeclaredClassesHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_CLASSES)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCLASSES, this.stringGetDeclaredClasses); + } + + @Test + void classGetDeclaredClassesShouldNotMatchPublicClassesHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_CLASSES)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCLASSES, this.stringGetDeclaredClasses); + } + + @Test + void classLoaderLoadClassShouldMatchReflectionHintOnType() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_LOADCLASS) + .onInstance(ClassLoader.getSystemClassLoader()) + .withArgument(PublicField.class.getCanonicalName()).returnValue(PublicField.class).build(); + hints.reflection().registerType(PublicField.class, builder -> { + }); + assertThatInvocationMatches(InstrumentedMethod.CLASSLOADER_LOADCLASS, invocation); + } + + } + + @Nested + class ConstructorReflectionInstrumentationTests { + + RecordedInvocation stringGetConstructor; + + RecordedInvocation stringGetConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTORS) + .onInstance(String.class).returnValue(String.class.getConstructors()).build(); + + RecordedInvocation stringGetDeclaredConstructor; + + RecordedInvocation stringGetDeclaredConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS) + .onInstance(String.class).returnValue(String.class.getDeclaredConstructors()).build(); + + @BeforeEach + public void setup() throws Exception { + this.stringGetConstructor = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTOR) + .onInstance(String.class).withArgument(new Class[0]).returnValue(String.class.getConstructor()).build(); + this.stringGetDeclaredConstructor = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR) + .onInstance(String.class).withArgument(new Class[] {byte[].class, byte.class}) + .returnValue(String.class.getDeclaredConstructor(byte[].class, byte.class)).build(); + } + + @Test + void classGetConstructorShouldMatchInstrospectPublicConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + } + + @Test + void classGetConstructorShouldMatchInvokePublicConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + } + + @Test + void classGetConstructorShouldMatchIntrospectDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + } + + @Test + void classGetConstructorShouldMatchInvokeDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + } + + @Test + void classGetConstructorShouldMatchInstrospectConstructorHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), + constructorHint -> constructorHint.setModes(ExecutableMode.INTROSPECT))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + } + + @Test + void classGetConstructorShouldMatchInvokeConstructorHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), + constructorHint -> constructorHint.setModes(ExecutableMode.INVOKE))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + } + + @Test + void classGetConstructorsShouldMatchIntrospectPublicConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + } + + @Test + void classGetConstructorsShouldMatchInvokePublicConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + } + + @Test + void classGetConstructorsShouldMatchIntrospectDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + } + + @Test + void classGetConstructorsShouldMatchInvokeDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + } + + @Test + void classGetDeclaredConstructorShouldMatchIntrospectDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); + } + + @Test + void classGetDeclaredConstructorShouldNotMatchIntrospectPublicConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); + } + + @Test + void classGetDeclaredConstructorShouldMatchInstrospectConstructorHint() { + List parameterTypes = List.of(TypeReference.of(byte[].class), TypeReference.of(byte.class)); + hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(parameterTypes, constructorHint -> constructorHint.setModes(ExecutableMode.INTROSPECT))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); + } + + @Test + void classGetDeclaredConstructorShouldMatchInvokeConstructorHint() { + List parameterTypes = List.of(TypeReference.of(byte[].class), TypeReference.of(byte.class)); + hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(parameterTypes, constructorHint -> constructorHint.setModes(ExecutableMode.INVOKE))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); + } + + @Test + void classGetDeclaredConstructorsShouldMatchIntrospectDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); + } + + @Test + void classGetDeclaredConstructorsShouldMatchInvokeDeclaredConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); + } + + @Test + void classGetDeclaredConstructorsShouldNotMatchIntrospectPublicConstructorsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); + } + + + @Test + void constructorNewInstanceShouldMatchInvokeHintOnConstructor() throws NoSuchMethodException { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE) + .onInstance(String.class.getConstructor()).returnValue("").build(); + hints.reflection().registerType(String.class, typeHint -> + typeHint.withConstructor(Collections.emptyList(), constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE))); + assertThatInvocationMatches(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE, invocation); + } + + @Test + void constructorNewInstanceShouldNotMatchIntrospectHintOnConstructor() throws NoSuchMethodException { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE) + .onInstance(String.class.getConstructor()).returnValue("").build(); + hints.reflection().registerType(String.class, typeHint -> + typeHint.withConstructor(Collections.emptyList(), constructorHint -> constructorHint.withMode(ExecutableMode.INTROSPECT))); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE, invocation); + } + + } + + @Nested + class MethodReflectionInstrumentationTests { + + RecordedInvocation stringGetToStringMethod; + + RecordedInvocation stringGetScaleMethod; + + @BeforeEach + void setup() throws Exception { + this.stringGetToStringMethod = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHOD) + .onInstance(String.class).withArguments("toString", new Class[0]) + .returnValue(String.class.getMethod("toString")).build(); + 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(); + } + + @Test + void classGetDeclaredMethodShouldMatchIntrospectDeclaredMethodsHint() throws NoSuchMethodException { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); + } + + @Test + void classGetDeclaredMethodShouldNotMatchIntrospectPublicMethodsHint() throws NoSuchMethodException { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); + } + + @Test + void classGetDeclaredMethodShouldMatchIntrospectMethodHint() throws NoSuchMethodException { + List parameterTypes = List.of(TypeReference.of(int.class), TypeReference.of(float.class)); + hints.reflection().registerType(String.class, typeHint -> + typeHint.withMethod("scale", parameterTypes, methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); + } + + @Test + void classGetDeclaredMethodShouldMatchInvokeMethodHint() throws NoSuchMethodException { + List parameterTypes = List.of(TypeReference.of(int.class), TypeReference.of(float.class)); + hints.reflection().registerType(String.class, typeHint -> + typeHint.withMethod("scale", parameterTypes, methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); + } + + @Test + void classGetDeclaredMethodsShouldMatchIntrospectDeclaredMethodsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); + } + + @Test + void classGetDeclaredMethodsShouldMatchInvokeDeclaredMethodsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, invocation); + } + + @Test + void classGetDeclaredMethodsShouldMatchIntrospectPublicMethodsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); + } + + @Test + void classGetMethodsShouldMatchInstrospectDeclaredMethodsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + } + + @Test + void classGetMethodsShouldMatchInstrospectPublicMethodsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + } + + @Test + void classGetMethodsShouldMatchInvokeDeclaredMethodsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + } + + @Test + void classGetMethodsShouldMatchInvokePublicMethodsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, invocation); + } + + @Test + void classGetMethodsShouldNotMatchForWrongType() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETMETHODS).onInstance(String.class).build(); + hints.reflection().registerType(Integer.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, invocation); + } + + @Test + void classGetMethodShouldMatchInstrospectPublicMethodsHint() throws NoSuchMethodException { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void classGetMethodShouldMatchInvokePublicMethodsHint() throws NoSuchMethodException { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void classGetMethodShouldMatchInstrospectDeclaredMethodsHint() throws NoSuchMethodException { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void classGetMethodShouldMatchInvokeDeclaredMethodsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void classGetMethodShouldMatchIntrospectMethodHint() { + hints.reflection().registerType(String.class, typeHint -> + typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.setModes(ExecutableMode.INTROSPECT))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void classGetMethodShouldMatchInvokeMethodHint() throws Exception { + hints.reflection().registerType(String.class, typeHint -> + typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.setModes(ExecutableMode.INVOKE))); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void classGetMethodShouldNotMatchInstrospectPublicMethodsHintWhenPrivate() throws Exception { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod); + } + + @Test + void classGetMethodShouldMatchInstrospectDeclaredMethodsHintWhenPrivate() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod); + } + + @Test + void classGetMethodShouldNotMatchForWrongType() { + hints.reflection().registerType(Integer.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); + } + + @Test + void methodInvokeShouldMatchInvokeHintOnMethod() throws NoSuchMethodException { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_INVOKE) + .onInstance(String.class.getMethod("startsWith", String.class)).withArguments("testString", new Object[] {"test"}).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMethod("startsWith", + List.of(TypeReference.of(String.class)), methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); + assertThatInvocationMatches(InstrumentedMethod.METHOD_INVOKE, invocation); + } + + @Test + void methodInvokeShouldNotMatchIntrospectHintOnMethod() throws NoSuchMethodException { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_INVOKE) + .onInstance(String.class.getMethod("toString")).withArguments("", new Object[0]).build(); + hints.reflection().registerType(String.class, typeHint -> + typeHint.withMethod("toString", Collections.emptyList(), methodHint -> methodHint.withMode(ExecutableMode.INTROSPECT))); + assertThatInvocationDoesNotMatch(InstrumentedMethod.METHOD_INVOKE, invocation); + } + + } + + @Nested + class FieldReflectionInstrumentationTests { + + RecordedInvocation getPublicField; + + RecordedInvocation stringGetDeclaredField; + + @BeforeEach + void setup() throws Exception { + this.getPublicField = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) + .onInstance(PublicField.class).withArgument("field") + .returnValue(PublicField.class.getField("field")).build(); + this.stringGetDeclaredField = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELD) + .onInstance(String.class).withArgument("value").returnValue(String.class.getDeclaredField("value")).build(); + } + + @Test + void classGetDeclaredFieldShouldMatchDeclaredFieldsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELD, this.stringGetDeclaredField); + } + + @Test + void classGetDeclaredFieldShouldNotMatchPublicFieldsHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELD, this.stringGetDeclaredField); + } + + @Test + void classGetDeclaredFieldShouldMatchFieldHint() { + hints.reflection().registerType(String.class, typeHint -> typeHint.withField("value", builder -> { + })); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELD, this.stringGetDeclaredField); + } + + @Test + void classGetDeclaredFieldsShouldMatchDeclaredFieldsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELDS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, invocation); + } + + @Test + void classGetDeclaredFieldsShouldNotMatchPublicFieldsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDFIELDS).onInstance(String.class).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, invocation); + } + + @Test + void classGetFieldShouldMatchPublicFieldsHint() { + hints.reflection().registerType(PublicField.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELD, this.getPublicField); + } + + @Test + void classGetFieldShouldMatchDeclaredFieldsHint() { + hints.reflection().registerType(PublicField.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELD, this.getPublicField); + } + + @Test + void classGetFieldShouldMatchFieldHint() { + hints.reflection().registerType(PublicField.class, typeHint -> typeHint.withField("field", builder -> { + })); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELD, this.getPublicField); + } + + @Test + void classGetFieldShouldNotMatchPublicFieldsHintWhenPrivate() throws NoSuchFieldException { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) + .onInstance(String.class).withArgument("value").returnValue(null).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELD, invocation); + } + + @Test + void classGetFieldShouldMatchDeclaredFieldsHintWhenPrivate() throws NoSuchFieldException { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) + .onInstance(String.class).withArgument("value").returnValue(String.class.getDeclaredField("value")).build(); + hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELD, invocation); + } + + @Test + void classGetFieldShouldNotMatchForWrongType() throws Exception { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) + .onInstance(String.class).withArgument("value").returnValue(null).build(); + hints.reflection().registerType(Integer.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELD, invocation); + } + + @Test + void classGetFieldsShouldMatchPublicFieldsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELDS) + .onInstance(PublicField.class).build(); + hints.reflection().registerType(PublicField.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, invocation); + } + + @Test + void classGetFieldsShouldMatchDeclaredFieldsHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELDS) + .onInstance(PublicField.class).build(); + hints.reflection().registerType(PublicField.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, invocation); + } + + } + + + @Nested + class ResourcesInstrumentationTests { + + @Test + void resourceBundleGetBundleShouldMatchBundleNameHint() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE) + .withArgument("bundleName").build(); + hints.resources().registerResourceBundle("bundleName"); + assertThatInvocationMatches(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE, invocation); + } + + @Test + void resourceBundleGetBundleShouldNotMatchBundleNameHintWhenWrongName() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE) + .withArgument("bundleName").build(); + hints.resources().registerResourceBundle("wrongBundleName"); + assertThatInvocationDoesNotMatch(InstrumentedMethod.RESOURCEBUNDLE_GETBUNDLE, invocation); + } + + @Test + void classGetResourceShouldMatchResourcePatternWhenAbsolute() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) + .onInstance(InstrumentedMethodTests.class).withArgument("/some/path/resource.txt").build(); + hints.resources().registerPattern("/some/*"); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETRESOURCE, invocation); + } + + @Test + void classGetResourceShouldMatchResourcePatternWhenRelative() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) + .onInstance(InstrumentedMethodTests.class).withArgument("resource.txt").build(); + hints.resources().registerPattern("/org/springframework/aot/agent/*"); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETRESOURCE, invocation); + } + + @Test + void classGetResourceShouldNotMatchResourcePatternWhenInvalid() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) + .onInstance(InstrumentedMethodTests.class).withArgument("/some/path/resource.txt").build(); + hints.resources().registerPattern("/other/*"); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETRESOURCE, invocation); + } + + @Test + void classGetResourceShouldNotMatchResourcePatternWhenExcluded() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) + .onInstance(InstrumentedMethodTests.class).withArgument("/some/path/resource.txt").build(); + hints.resources().registerPattern(resourceHint -> resourceHint.includes("/some/*").excludes("/some/path/*")); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETRESOURCE, invocation); + } + + } + + + @Nested + class ProxiesInstrumentationTests { + + RecordedInvocation newProxyInstance; + + @BeforeEach + void setup() { + this.newProxyInstance = RecordedInvocation.of(InstrumentedMethod.PROXY_NEWPROXYINSTANCE) + .withArguments(ClassLoader.getSystemClassLoader(), new Class[] {AutoCloseable.class, Comparator.class}, null) + .returnValue(Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[] {AutoCloseable.class, Comparator.class}, (proxy, method, args) -> null)) + .build(); + } + + @Test + void proxyNewProxyInstanceShouldMatchWhenInterfacesMatch() { + hints.proxies().registerJdkProxy(AutoCloseable.class, Comparator.class); + assertThatInvocationMatches(InstrumentedMethod.PROXY_NEWPROXYINSTANCE, this.newProxyInstance); + } + + @Test + void proxyNewProxyInstanceShouldNotMatchWhenInterfacesDoNotMatch() { + hints.proxies().registerJdkProxy(Comparator.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.PROXY_NEWPROXYINSTANCE, this.newProxyInstance); + } + + @Test + void proxyNewProxyInstanceShouldNotMatchWhenWrongOrder() { + hints.proxies().registerJdkProxy(Comparator.class, AutoCloseable.class); + assertThatInvocationDoesNotMatch(InstrumentedMethod.PROXY_NEWPROXYINSTANCE, this.newProxyInstance); + } + } + + + private void assertThatInvocationMatches(InstrumentedMethod method, RecordedInvocation invocation) { + assertThat(method.matcher(invocation)).accepts(this.hints); + } + + private void assertThatInvocationDoesNotMatch(InstrumentedMethod method, RecordedInvocation invocation) { + assertThat(method.matcher(invocation)).rejects(this.hints); + } + + private static class PrivateConstructor { + + private PrivateConstructor() { + + } + } + + static class PublicField { + + public String field; + + } + +}