As of Spring Framework 6.0, the new RuntimeHints
API helps you to contribute hints for runtime reflection, resources and proxies with GraalVM native.
It can be hard to find out about the required hints without compilingan application to native and seeing it fail.
The new spring-core-test
module ships a Java agent that will help you with that.
This agent records all method invocations that are related to such hints and helps you to assert that a given RuntimeHints
instance
covers all recorded invocations.
Let's consider a piece of infrastructure for which we'd like to test the hints we're contributing during the AOT phase.
public class SampleReflection {
private final Log logger = LogFactory.getLog(SampleReflection.class);
public void performReflection() {
try {
Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null);
Method getVersion = ClassUtils.getMethod(springVersion, "getVersion");
String version = (String) getVersion.invoke(null);
logger.info("Spring version:" + version);
}
catch (Exception exc) {
logger.error("reflection failed", exc);
}
}
}
We can then write a Java test (no native compilation required!) that checks our contributed hints:
// this annotation conditions the execution of tests only if the agent is loaded in the current JVM
// it also tags tests with the "RuntimeHints" JUnit tag
@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {
@Test
void shouldRegisterReflectionHints() {
RuntimeHints runtimeHints = new RuntimeHints();
// Call a RuntimeHintsRegistrar that contributes hints like:
runtimeHints.reflection().registerType(SpringVersion.class, typeHint -> {
typeHint.withMethod("getVersion", List.of(), methodHint -> methodHint.withMode(ExecutableMode.INVOKE));
});
// Invoke the relevant piece of code we want to test within a recording lambda
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
SampleReflection sample = new SampleReflection();
sample.performReflection();
});
// assert that the recorded invocations are covered by the contributed hints
assertThat(invocations).match(runtimeHints);
}
}
If you forgot to contribute a hint, the test will fail and give some details on the invocation:
Aug 04, 2022 7:06:48 PM io.spring.runtimehintstesting.SampleReflection performReflection
INFO: Spring version:6.0.0-SNAPSHOT
Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
with arguments ["org.springframework.core.SpringVersion",
false,
jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
Stacktrace:
<"org.springframework.util.ClassUtils#forName, Line 284
io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19
io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25
This project shows how to configure the RuntimeHints Java agent for your tests.
The strategy chosen here is to configure a new :runtimeHintsTest
task that only runs tests tagged "RuntimeHints
.
This allows to load the Java agent only for the relevant tests and avoid polluting other tests.
You can run those with ./gradlew :runtimeHintsTest
and they are included with ./gradlew :check
.
The agent itself can be configured to instrument some packages (by default, only org.springframework
is instrumented).
The Spring Framework project provides a Gradle DSL extension for this, copied in this project.
You'll find more details in the Spring Framework buildSrc README.
With Maven, this project also shows how to configure the Java agent in an isolated test suite.
For this, a specific profile configures surefire to run the test runtimehints test suite with ./mvnw test -P runtimehints
.