Browse Source
With the introduction of `RuntimeHints`, we can now contribute reflection, resources and proxies hints that describe the expected runtime behavior of the application. While this can be verified at runtime with smoke tests, managing such tests and compiling to native there is not very efficient. This commit introduces the new `RuntimeHintsAgent`, a Java agent that instruments JDK methods related to `RuntimeHints`. It is different from the GraalVM agent, which aims at collecting all the required hints for the runtime behavior of an application and dump those in the expected format. Here, the `RuntimeHintsAgent` can collect the related invocations only for a delimited scope (typically, a lambda within a test) and later check those against a `RuntimeHints` instance. In the case of testing `RuntimeHintsRegistrar` implementations, the process is reversed: instead of manually checking for registered hints in a `RuntimeHints` instance, tests should exercise the use cases and then check that the recorded behavior is in line with the prepared hints. This first commit adds the agent infrastructure that collects the invocations for all relevant JDK methods. See gh-27981pull/28739/head
13 changed files with 2459 additions and 0 deletions
@ -0,0 +1,76 @@
@@ -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}. |
||||
* <p>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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,527 @@
@@ -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}. |
||||
* <p>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<URL> classloaderresources(ClassLoader classLoader, String name) { |
||||
Stream<URL> 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<URL> classloadergetResources(ClassLoader classLoader, String name) throws IOException { |
||||
Enumeration<URL> 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,397 @@
@@ -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}. |
||||
* <p>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<RecordedInvocation, Predicate<RuntimeHints>> hintsMatcherGenerator; |
||||
|
||||
InstrumentedMethod(Class<?> klass, String methodName, HintType hintType, Function<RecordedInvocation, Predicate<RuntimeHints>> 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. |
||||
* <p>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<RuntimeHints> matcher(RecordedInvocation invocation) { |
||||
return this.hintsMatcherGenerator.apply(invocation); |
||||
} |
||||
|
||||
private static Predicate<RuntimeHints> hasReturnValue(RecordedInvocation invocation) { |
||||
return runtimeHints -> invocation.getReturnValue() != null; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,112 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,136 @@
@@ -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}. |
||||
* <p>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<String> 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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,84 @@
@@ -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. |
||||
* |
||||
* <p>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; |
||||
} |
||||
} |
||||
@ -0,0 +1,250 @@
@@ -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}. |
||||
* <p>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<StackWalker.StackFrame> stackFrames; |
||||
|
||||
private RecordedInvocation(InstrumentedMethod instrumentedMethod, @Nullable Object instance, |
||||
Object[] arguments, @Nullable Object returnValue, List<StackWalker.StackFrame> 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<StackWalker.StackFrame> 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> 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<Object> 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> 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<TypeReference> 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<TypeReference> 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> T getReturnValue() { |
||||
return (T) this.returnValue; |
||||
} |
||||
|
||||
/** |
||||
* Whether the given hints cover the current invocation. |
||||
* <p>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<StackWalker.StackFrame> 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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,34 @@
@@ -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); |
||||
|
||||
} |
||||
@ -0,0 +1,65 @@
@@ -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}. |
||||
* <p>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<RecordedInvocationsListener> 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)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,109 @@
@@ -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. |
||||
* <p>This agent uses {@link java.lang.instrument.ClassFileTransformer class transformers} |
||||
* that modify bytecode to intercept and record method invocations at runtime. |
||||
* <p>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 "-"}: |
||||
* <pre class="code"> |
||||
* -javaagent:/path/to/spring-runtimehints-agent.jar=+org.springframework,-io.spring,+org.example") |
||||
* </pre> |
||||
* |
||||
* @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<String> instrumentedPackages; |
||||
|
||||
List<String> ignoredPackages; |
||||
|
||||
private ParsedArguments(List<String> instrumentedPackages, List<String> 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<String> included = new ArrayList<>(); |
||||
List<String> 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); |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -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; |
||||
@ -0,0 +1,650 @@
@@ -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<TypeReference> 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<TypeReference> 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<TypeReference> 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<TypeReference> 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; |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue