Skip to content

Commit

Permalink
Document RuntimeHints testing strategies
Browse files Browse the repository at this point in the history
Closes gh-29523
  • Loading branch information
bclozel committed Nov 18, 2022
1 parent 9249dc3 commit 44c5833
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 34 deletions.
8 changes: 8 additions & 0 deletions framework-docs/framework-docs.gradle
Expand Up @@ -10,6 +10,14 @@ configurations {
asciidoctorExtensions
}

dependencies {
api(project(":spring-context"))
api(project(":spring-web"))

implementation(project(":spring-core-test"))
implementation("org.assertj:assertj-core")
}

jar {
enabled = false
}
Expand Down
2 changes: 2 additions & 0 deletions framework-docs/src/docs/asciidoc/attributes.adoc
@@ -1,3 +1,4 @@
:chomp: default headers packages
:docs-site: https://docs.spring.io
// Spring Framework
:docs-spring-framework: {docs-site}/spring-framework/docs/{spring-version}
Expand All @@ -13,3 +14,4 @@
:gh-rsocket: https://github.com/rsocket
:gh-rsocket-extensions: {gh-rsocket}/rsocket/blob/master/Extensions
:gh-rsocket-java: {gh-rsocket}/rsocket-java
:doc-graalvm: https://www.graalvm.org/22.3/reference-manual
84 changes: 50 additions & 34 deletions framework-docs/src/docs/asciidoc/core/core-aot.adoc
Expand Up @@ -52,27 +52,13 @@ An application context is created with any number of entry points, usually in th

Let's look at a basic example:

[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}
----
include::code:AotProcessingSample[tag=myapplication]

Starting this application with the regular runtime involves a number of steps including classpath scanning, configuration class parsing, bean instantiation, and lifecycle callback handling.
Refresh for AOT processing only applies a subset of what happens with a <<beans-introduction,regular `refresh`>>.
AOT processing can be triggered as follows:

[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
GenericApplicationContext applicationContext = new AnnotatedConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing();
----
include::code:AotProcessingSample[tag=aotcontext]

In this mode, <<beans-factory-extension-factory-postprocessors,`BeanFactoryPostProcessor` implementations>> are invoked as usual.
This includes configuration class parsing, import selectors, classpath scanning, etc.
Expand Down Expand Up @@ -229,24 +215,7 @@ A number of convenient annotations are also provided for common use cases.
Implementations of this interface can be registered using `@ImportRuntimeHints` on any Spring bean or `@Bean` factory method.
`RuntimeHintsRegistrar` implementations are detected and invoked at build time.

[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Component
@ImportRuntimeHints(MyComponentRuntimeHints.class)
public class MyComponent {
// ...
private static class MyComponentRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// ...
}
}
}
----
include::code:SpellCheckService[]

If at all possible, `@ImportRuntimeHints` should be used as close as possible to the component that requires the hints.
This way, if the component is not contributed to the `BeanFactory`, the hints won't be contributed either.
Expand Down Expand Up @@ -290,3 +259,50 @@ The following example registers `Account` for serialization.
}
----

[[core.aot.hints.testing]]
=== Testing Runtime Hints

Spring Core also ships `RuntimeHintsPredicates`, an utility for checking that existing hints match a particular use case.
This can be used in your own tests, for validating that a `RuntimeHintsRegistrar` has the expected result.
We can write a test for our `SpellCheckService` and ensure that we can load a dictionary at runtime:

include::code:SpellCheckServiceTests[tag=hintspredicates]

With `RuntimeHintsPredicates`, we can check for reflection, resource, serialization or proxy generation hints.
This approach works well for unit tests but implies that the runtime behavior of a component is well known.

You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {doc-graalvm}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent].
This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files.

For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, `"org.springframework:spring-core-test"`.
This module contains the RuntimeHints Agent, a Java agent that all method invocations that are related to runtime 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.


include::code:SampleReflection[]

We can then write a Java test (no native compilation required!) that checks our contributed hints:

include::code:SampleReflectionRuntimeHintsTests[]

If you forgot to contribute a hint, the test will fail and give some details on the invocation:

[source,txt,indent=0,subs="verbatim,quotes"]
----
org.springframework.docs.core.aot.hints.testing.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
----

There are various ways to configure this Java agent into your build, please refer to your build tool and test execution plugin documentation.
The agent itself can be configured to instrument some packages (by default, only `org.springframework` is instrumented).
You'll find more details in the https://github.com/spring-projects/spring-framework/blob/main/buildSrc/README.md[Spring Framework buildSrc README].
@@ -0,0 +1,44 @@
/*
* 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.docs.core.aot.hints.importruntimehints;

import java.util.Locale;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

@Component
@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class)
public class SpellCheckService {

public void loadDictionary(Locale locale) {
ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt");
//...
}

static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {

@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("dicts/*");
}
}

}
@@ -0,0 +1,41 @@
/*
* 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.docs.core.aot.hints.testing;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.util.ClassUtils;

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);
}
}
}
@@ -0,0 +1,54 @@
/*
* 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.docs.core.aot.hints.testing;

import java.util.List;

import org.junit.jupiter.api.Test;

import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import org.springframework.core.SpringVersion;

import static org.assertj.core.api.Assertions.assertThat;

// 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(), 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);
}

}
@@ -0,0 +1,47 @@
/*
* 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.docs.core.aot.hints.testing;

import org.junit.jupiter.api.Test;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;

import static org.assertj.core.api.Assertions.assertThat;

public class SpellCheckServiceTests {

// tag::hintspredicates[]
@Test
void shouldRegisterResourceHints() {
RuntimeHints hints = new RuntimeHints();
new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
.accepts(hints);
}
// end::hintspredicates[]

// Copied here because it is package private in SpellCheckService
static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {

@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("dicts/*");
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.docs.core.aot.refresh;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

public class AotProcessingSample {

public void createAotContext() {
// tag::aotcontext[]
RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
// end::aotcontext[]
}

// tag::myapplication[]
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {

}
// end::myapplication[]

class DataSourceConfiguration {

}

class ContainerConfiguration {

}
}

0 comments on commit 44c5833

Please sign in to comment.