From 44c583392e792f47deb2cb7c6be88f704d5f19f7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 18 Nov 2022 18:11:35 +0100 Subject: [PATCH] Document RuntimeHints testing strategies Closes gh-29523 --- framework-docs/framework-docs.gradle | 8 ++ .../src/docs/asciidoc/attributes.adoc | 2 + .../src/docs/asciidoc/core/core-aot.adoc | 84 +++++++++++-------- .../importruntimehints/SpellCheckService.java | 44 ++++++++++ .../aot/hints/testing/SampleReflection.java | 41 +++++++++ .../SampleReflectionRuntimeHintsTests.java | 54 ++++++++++++ .../hints/testing/SpellCheckServiceTests.java | 47 +++++++++++ .../core/aot/refresh/AotProcessingSample.java | 52 ++++++++++++ 8 files changed, 298 insertions(+), 34 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 2b13aaaa1157..f04161c9c8d1 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -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 } diff --git a/framework-docs/src/docs/asciidoc/attributes.adoc b/framework-docs/src/docs/asciidoc/attributes.adoc index 041f12bfbe1f..8f94a35c0ac6 100644 --- a/framework-docs/src/docs/asciidoc/attributes.adoc +++ b/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} @@ -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 diff --git a/framework-docs/src/docs/asciidoc/core/core-aot.adoc b/framework-docs/src/docs/asciidoc/core/core-aot.adoc index af8d420dba47..56e955c29906 100644 --- a/framework-docs/src/docs/asciidoc/core/core-aot.adoc +++ b/framework-docs/src/docs/asciidoc/core/core-aot.adoc @@ -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 <>. 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, <> are invoked as usual. This includes configuration class parsing, import selectors, classpath scanning, etc. @@ -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. @@ -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 +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]. diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java new file mode 100644 index 000000000000..88fd79becf0a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java @@ -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/*"); + } + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java new file mode 100644 index 000000000000..342f0691b7ee --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java @@ -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); + } + } +} \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java new file mode 100644 index 000000000000..0d9ec56e285c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java @@ -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); + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java new file mode 100644 index 000000000000..d04617fa4751 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java @@ -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/*"); + } + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java new file mode 100644 index 000000000000..f381ad89594e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java @@ -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 { + + } +}