Browse Source

Add RuntimeHintsAgent Java agent

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-27981
pull/28739/head
Brian Clozel 4 years ago
parent
commit
c86e678788
  1. 10
      spring-core-test/spring-core-test.gradle
  2. 76
      spring-core-test/src/main/java/org/springframework/aot/agent/HintType.java
  3. 527
      spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java
  4. 397
      spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java
  5. 112
      spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java
  6. 136
      spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java
  7. 84
      spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java
  8. 250
      spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java
  9. 34
      spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsListener.java
  10. 65
      spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsPublisher.java
  11. 109
      spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java
  12. 9
      spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java
  13. 650
      spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java

10
spring-core-test/spring-core-test.gradle

@ -2,9 +2,19 @@ description = "Spring Core Test"
dependencies { dependencies {
api(project(":spring-core")) api(project(":spring-core"))
api("org.junit.jupiter:junit-jupiter-api")
api("org.assertj:assertj-core") api("org.assertj:assertj-core")
api("com.thoughtworks.qdox:qdox") api("com.thoughtworks.qdox:qdox")
compileOnly("org.junit.jupiter:junit-jupiter") compileOnly("org.junit.jupiter:junit-jupiter")
compileOnly("org.junit.platform:junit-platform-engine") compileOnly("org.junit.platform:junit-platform-engine")
compileOnly("org.junit.platform:junit-platform-launcher") compileOnly("org.junit.platform:junit-platform-launcher")
} }
jar {
manifest {
attributes(
'Premain-Class': 'org.springframework.aot.agent.RuntimeHintsAgent',
'Can-Redefine-Classes': 'true'
)
}
}

76
spring-core-test/src/main/java/org/springframework/aot/agent/HintType.java

@ -0,0 +1,76 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import org.springframework.aot.hint.ClassProxyHint;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.ResourceBundleHint;
import org.springframework.aot.hint.ResourcePatternHint;
/**
* Main types of {@link org.springframework.aot.hint.RuntimeHints}.
* <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();
}
}

527
spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java

@ -0,0 +1,527 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.stream.Stream;
import org.springframework.lang.Nullable;
/**
* Instrumented version of JDK methods to be used by bytecode rewritten by the {@link RuntimeHintsAgent}.
* <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;
}
}

397
spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java

@ -0,0 +1,397 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ResourceBundle;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsPredicates;
import org.springframework.aot.hint.TypeReference;
/**
* Java method that is instrumented by the {@link RuntimeHintsAgent}.
* <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;
}
}

112
spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java

@ -0,0 +1,112 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import org.springframework.asm.ClassReader;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* ASM {@link ClassFileTransformer} that delegates bytecode transformations
* to a {@link InvocationsRecorderClassVisitor class visitor} if and only
* if the class is in the list of packages considered for instrumentation.
*
* @author Brian Clozel
* @see InvocationsRecorderClassVisitor
*/
class InvocationsRecorderClassTransformer implements ClassFileTransformer {
private static final String AGENT_PACKAGE = InvocationsRecorderClassTransformer.class.getPackageName().replace('.', '/');
private static final String AOT_DYNAMIC_CLASSLOADER = "org/springframework/aot/test/generator/compile/DynamicClassLoader";
private final String[] instrumentedPackages;
private final String[] ignoredPackages;
public InvocationsRecorderClassTransformer(String[] instrumentedPackages, String[] ignoredPackages) {
Assert.notNull(instrumentedPackages, "instrumentedPackages should not be null");
Assert.notNull(ignoredPackages, "ignoredPackages should not be null");
this.instrumentedPackages = rewriteToAsmFormat(instrumentedPackages);
this.ignoredPackages = rewriteToAsmFormat(ignoredPackages);
}
private String[] rewriteToAsmFormat(String[] packages) {
return Arrays.stream(packages).map(pack -> pack.replace('.', '/'))
.toArray(String[]::new);
}
@Override
public byte[] transform(@Nullable ClassLoader classLoader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (isTransformationCandidate(classLoader, className)) {
return attemptClassTransformation(classfileBuffer);
}
return classfileBuffer;
}
private boolean isTransformationCandidate(@Nullable ClassLoader classLoader, String className) {
// Ignore system classes
if (classLoader == null) {
return false;
}
// Ignore agent classes and spring-core-test DynamicClassLoader
else if (className.startsWith(AGENT_PACKAGE) || className.equals(AOT_DYNAMIC_CLASSLOADER)) {
return false;
}
// Do not instrument CGlib classes
else if (className.contains("$$")) {
return false;
}
// Only some packages are instrumented
else {
for (String ignoredPackage : this.ignoredPackages) {
if (className.startsWith(ignoredPackage)) {
return false;
}
}
for (String instrumentedPackage : this.instrumentedPackages) {
if (className.startsWith(instrumentedPackage)) {
return true;
}
}
}
return false;
}
private byte[] attemptClassTransformation(byte[] classfileBuffer) {
ClassReader fileReader = new ClassReader(classfileBuffer);
InvocationsRecorderClassVisitor classVisitor = new InvocationsRecorderClassVisitor();
try {
fileReader.accept(classVisitor, 0);
}
catch (Exception ex) {
ex.printStackTrace();
return classfileBuffer;
}
if (classVisitor.isTransformed()) {
return classVisitor.getTransformedClassBuffer();
}
return classfileBuffer;
}
}

136
spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java

@ -0,0 +1,136 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.util.HashSet;
import java.util.Set;
import org.springframework.asm.ClassVisitor;
import org.springframework.asm.ClassWriter;
import org.springframework.asm.Handle;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
import org.springframework.asm.SpringAsmInfo;
/**
* ASM {@link ClassVisitor} that rewrites a known set of method invocations
* to call instrumented bridge methods for {@link RecordedInvocationsPublisher recording purposes}.
* <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);
}
}
}

84
spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java

@ -0,0 +1,84 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.util.Objects;
/**
* Reference to a Java method, identified by its owner class and the method name.
*
* <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;
}
}

250
spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java

@ -0,0 +1,250 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Record of an invocation of a method relevant to {@link org.springframework.aot.hint.RuntimeHints}.
* <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);
}
}
}

34
spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsListener.java

@ -0,0 +1,34 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
/**
* Listener for {@link RecordedInvocation invocations recorded} by the {@link RuntimeHintsAgent}.
*
* @author Brian Clozel
* @since 6.0
*/
@FunctionalInterface
public interface RecordedInvocationsListener {
/**
* Called when an {@link RecordedInvocation invocation has been recorded} by the Java Agent.
* @param invocation the recorded invocation.
*/
void onInvocation(RecordedInvocation invocation);
}

65
spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocationsPublisher.java

@ -0,0 +1,65 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.util.ArrayDeque;
import java.util.Deque;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
/**
* Publishes invocations on method relevant to {@link RuntimeHints},
* as they are recorded by the {@link RuntimeHintsAgent}.
* <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));
}
}

109
spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java

@ -0,0 +1,109 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.List;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Java Agent that records method invocations related to {@link RuntimeHints} metadata.
* <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);
}
}
}

9
spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java

@ -0,0 +1,9 @@
/**
* Support for recording method invocations relevant to {@link org.springframework.aot.hint.RuntimeHints} metadata.
*/
@NonNullApi
@NonNullFields
package org.springframework.aot.agent;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

650
spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java

@ -0,0 +1,650 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.agent;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link InstrumentedMethod}.
*
* @author Brian Clozel
*/
class InstrumentedMethodTests {
private RuntimeHints hints = new RuntimeHints();
@Nested
class ClassReflectionInstrumentationTests {
RecordedInvocation stringGetClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCLASSES)
.onInstance(String.class).returnValue(String.class.getClasses()).build();
RecordedInvocation stringGetDeclaredClasses = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCLASSES)
.onInstance(String.class).returnValue(String.class.getDeclaredClasses()).build();
@Test
void classForNameShouldMatchReflectionOnType() {
RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_FORNAME)
.withArgument("java.lang.String").returnValue(String.class).build();
hints.reflection().registerType(String.class, typeHint -> {
});
assertThatInvocationMatches(InstrumentedMethod.CLASS_FORNAME, invocation);
}
@Test
void classGetClassesShouldNotMatchReflectionOnType() {
hints.reflection().registerType(String.class, typeHint -> {
});
assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses);
}
@Test
void classGetClassesShouldMatchPublicClasses() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_CLASSES));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses);
}
@Test
void classGetClassesShouldMatchDeclaredClasses() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_CLASSES));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses);
}
@Test
void classGetDeclaredClassesShouldMatchDeclaredClassesHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_CLASSES));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCLASSES, this.stringGetDeclaredClasses);
}
@Test
void classGetDeclaredClassesShouldNotMatchPublicClassesHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_CLASSES));
assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCLASSES, this.stringGetDeclaredClasses);
}
@Test
void classLoaderLoadClassShouldMatchReflectionHintOnType() {
RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_LOADCLASS)
.onInstance(ClassLoader.getSystemClassLoader())
.withArgument(PublicField.class.getCanonicalName()).returnValue(PublicField.class).build();
hints.reflection().registerType(PublicField.class, builder -> {
});
assertThatInvocationMatches(InstrumentedMethod.CLASSLOADER_LOADCLASS, invocation);
}
}
@Nested
class ConstructorReflectionInstrumentationTests {
RecordedInvocation stringGetConstructor;
RecordedInvocation stringGetConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTORS)
.onInstance(String.class).returnValue(String.class.getConstructors()).build();
RecordedInvocation stringGetDeclaredConstructor;
RecordedInvocation stringGetDeclaredConstructors = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS)
.onInstance(String.class).returnValue(String.class.getDeclaredConstructors()).build();
@BeforeEach
public void setup() throws Exception {
this.stringGetConstructor = RecordedInvocation.of(InstrumentedMethod.CLASS_GETCONSTRUCTOR)
.onInstance(String.class).withArgument(new Class[0]).returnValue(String.class.getConstructor()).build();
this.stringGetDeclaredConstructor = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR)
.onInstance(String.class).withArgument(new Class[] {byte[].class, byte.class})
.returnValue(String.class.getDeclaredConstructor(byte[].class, byte.class)).build();
}
@Test
void classGetConstructorShouldMatchInstrospectPublicConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
}
@Test
void classGetConstructorShouldMatchInvokePublicConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
}
@Test
void classGetConstructorShouldMatchIntrospectDeclaredConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
}
@Test
void classGetConstructorShouldMatchInvokeDeclaredConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
}
@Test
void classGetConstructorShouldMatchInstrospectConstructorHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(Collections.emptyList(),
constructorHint -> constructorHint.setModes(ExecutableMode.INTROSPECT)));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
}
@Test
void classGetConstructorShouldMatchInvokeConstructorHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withConstructor(Collections.emptyList(),
constructorHint -> constructorHint.setModes(ExecutableMode.INVOKE)));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
}
@Test
void classGetConstructorsShouldMatchIntrospectPublicConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors);
}
@Test
void classGetConstructorsShouldMatchInvokePublicConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors);
}
@Test
void classGetConstructorsShouldMatchIntrospectDeclaredConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors);
}
@Test
void classGetConstructorsShouldMatchInvokeDeclaredConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors);
}
@Test
void classGetDeclaredConstructorShouldMatchIntrospectDeclaredConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS));
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor);
}
@Test
void classGetDeclaredConstructorShouldNotMatchIntrospectPublicConstructorsHint() {
hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS));
assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor);
}
@Test
void classGetDeclaredConstructorShouldMatchInstrospectConstructorHint() {
List<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…
Cancel
Save