From 741ee960e26ecf2f0b7302e122866904e98e7e63 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 30 Aug 2022 15:27:19 +0200 Subject: [PATCH] Register runtime hints for TestContext framework classes and annotations This commit introduces TestContextRuntimeHints which is a RuntimeHintsRegistrar implementation that makes core types and annotations from the Spring TestContext Framework available at runtime within a GraalVM native image. TestContextRuntimeHints is registered automatically via the "META-INF/spring/aot.factories" file in spring-test. This commit also modifies TestContextAotGeneratorTests to assert the expected runtime hints registered by TestContextRuntimeHints as well as runtime hints for TestExecutionListener and ContextCustomizerFactory implementations registered by SpringFactoriesLoaderRuntimeHints. Closes gh-29028 Closes gh-29044 --- .../context/aot/TestContextRuntimeHints.java | 162 ++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 2 + .../aot/TestContextAotGeneratorTests.java | 111 +++++++++++- 3 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java create mode 100644 spring-test/src/main/resources/META-INF/spring/aot.factories diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java new file mode 100644 index 00000000000..90dbc813f45 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java @@ -0,0 +1,162 @@ +/* + * 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.test.context.aot; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.support.RuntimeHintsUtils; +import org.springframework.util.ClassUtils; + +/** + * {@link RuntimeHintsRegistrar} implementation that makes types and annotations + * from the Spring TestContext Framework available at runtime. + * + * @author Sam Brannen + * @since 6.0 + */ +public class TestContextRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints runtimeHints, ClassLoader classLoader) { + ReflectionHints reflectionHints = runtimeHints.reflection(); + boolean txPresent = ClassUtils.isPresent("org.springframework.transaction.annotation.Transactional", classLoader); + boolean servletPresent = ClassUtils.isPresent("jakarta.servlet.Servlet", classLoader); + boolean groovyPresent = ClassUtils.isPresent("groovy.lang.Closure", classLoader); + + registerPublicConstructors(reflectionHints, + org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class, + org.springframework.test.context.support.DefaultBootstrapContext.class, + org.springframework.test.context.support.DelegatingSmartContextLoader.class + ); + + registerDeclaredConstructors(reflectionHints, + org.springframework.test.context.support.DefaultTestContextBootstrapper.class + ); + + if (servletPresent) { + registerPublicConstructors(reflectionHints, + "org.springframework.test.context.web.WebDelegatingSmartContextLoader" + ); + registerDeclaredConstructors(reflectionHints, + "org.springframework.test.context.web.WebTestContextBootstrapper" + ); + } + + if (groovyPresent) { + registerDeclaredConstructors(reflectionHints, + "org.springframework.test.context.support.GenericGroovyXmlContextLoader" + ); + if (servletPresent) { + registerDeclaredConstructors(reflectionHints, + "org.springframework.test.context.web.GenericGroovyXmlWebContextLoader" + ); + } + } + + registerSynthesizedAnnotation(runtimeHints, + // Legacy and JUnit 4 + org.springframework.test.annotation.Commit.class, + org.springframework.test.annotation.DirtiesContext.class, + org.springframework.test.annotation.IfProfileValue.class, + org.springframework.test.annotation.ProfileValueSourceConfiguration.class, + org.springframework.test.annotation.Repeat.class, + org.springframework.test.annotation.Rollback.class, + org.springframework.test.annotation.Timed.class, + + // Core TestContext framework + org.springframework.test.context.ActiveProfiles.class, + org.springframework.test.context.BootstrapWith.class, + org.springframework.test.context.ContextConfiguration.class, + org.springframework.test.context.ContextHierarchy.class, + org.springframework.test.context.DynamicPropertySource.class, + org.springframework.test.context.NestedTestConfiguration.class, + org.springframework.test.context.TestConstructor.class, + org.springframework.test.context.TestExecutionListeners.class, + org.springframework.test.context.TestPropertySource.class, + org.springframework.test.context.TestPropertySources.class, + + // Application Events + org.springframework.test.context.event.RecordApplicationEvents.class, + + // JUnit Jupiter + org.springframework.test.context.junit.jupiter.EnabledIf.class, + org.springframework.test.context.junit.jupiter.DisabledIf.class, + org.springframework.test.context.junit.jupiter.SpringJUnitConfig.class, + org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig.class, + + // Web + org.springframework.test.context.web.WebAppConfiguration.class + ); + + if (txPresent) { + registerSynthesizedAnnotation(runtimeHints, + org.springframework.test.context.jdbc.Sql.class, + org.springframework.test.context.jdbc.SqlConfig.class, + org.springframework.test.context.jdbc.SqlGroup.class, + org.springframework.test.context.jdbc.SqlMergeMode.class, + org.springframework.test.context.transaction.AfterTransaction.class, + org.springframework.test.context.transaction.BeforeTransaction.class + ); + } + } + + private static void registerPublicConstructors(ReflectionHints reflectionHints, Class... types) { + reflectionHints.registerTypes(TypeReference.listOf(types), + builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + } + + private static void registerPublicConstructors(ReflectionHints reflectionHints, String... classNames) { + reflectionHints.registerTypes(listOf(classNames), + builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + } + + private static void registerDeclaredConstructors(ReflectionHints reflectionHints, Class... types) { + reflectionHints.registerTypes(TypeReference.listOf(types), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + } + + private static void registerDeclaredConstructors(ReflectionHints reflectionHints, String... classNames) { + reflectionHints.registerTypes(listOf(classNames), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + } + + private static List listOf(String... classNames) { + return Arrays.stream(classNames).map(TypeReference::of).toList(); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + private static void registerSynthesizedAnnotation(RuntimeHints runtimeHints, Class... annotationTypes) { + for (Class annotationType : annotationTypes) { + registerAnnotation(runtimeHints.reflection(), annotationType); + RuntimeHintsUtils.registerSynthesizedAnnotation(runtimeHints, annotationType); + } + } + + private static void registerAnnotation(ReflectionHints reflectionHints, Class annotationType) { + reflectionHints.registerType(annotationType, + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + } + +} diff --git a/spring-test/src/main/resources/META-INF/spring/aot.factories b/spring-test/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..97ad484d8b2 --- /dev/null +++ b/spring-test/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.test.context.aot.TestContextRuntimeHints diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java index 775921c7f4a..9f1bfbc1639 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java @@ -16,23 +16,27 @@ package org.springframework.test.context.aot; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.hint.JdkProxyHint; import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.test.generator.compile.CompileWithTargetClassAccess; import org.springframework.aot.test.generator.compile.TestCompiler; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.SynthesizedAnnotation; import org.springframework.javapoet.ClassName; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests; @@ -52,6 +56,11 @@ import org.springframework.web.context.WebApplicationContext; import static java.util.Comparator.comparing; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; +import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_METHODS; +import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS; +import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_METHODS; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -83,10 +92,7 @@ class TestContextAotGeneratorTests extends AbstractAotTests { generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName))); - ReflectionHints reflectionHints = generator.getRuntimeHints().reflection(); - assertThat(reflectionHints.getTypeHint(TypeReference.of(AotTestMappings.GENERATED_MAPPINGS_CLASS_NAME))) - .satisfies(typeHint -> - assertThat(typeHint.getMemberCategories()).containsExactly(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertRuntimeHints(generator.getRuntimeHints()); List sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList(); assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringTests); @@ -105,6 +111,101 @@ class TestContextAotGeneratorTests extends AbstractAotTests { })); } + private static void assertRuntimeHints(RuntimeHints runtimeHints) { + assertReflectionRegistered(runtimeHints, AotTestMappings.GENERATED_MAPPINGS_CLASS_NAME, INVOKE_PUBLIC_METHODS); + + Set.of( + org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class, + org.springframework.test.context.support.DefaultBootstrapContext.class, + org.springframework.test.context.support.DelegatingSmartContextLoader.class, + org.springframework.test.context.web.WebDelegatingSmartContextLoader.class + ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_PUBLIC_CONSTRUCTORS)); + + Set.of( + org.springframework.test.context.support.DefaultTestContextBootstrapper.class, + org.springframework.test.context.web.WebTestContextBootstrapper.class, + org.springframework.test.context.support.GenericGroovyXmlContextLoader.class, + org.springframework.test.context.web.GenericGroovyXmlWebContextLoader.class + ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS)); + + Set.of( + // Legacy and JUnit 4 + org.springframework.test.annotation.Commit.class, + org.springframework.test.annotation.DirtiesContext.class, + org.springframework.test.annotation.IfProfileValue.class, + org.springframework.test.annotation.ProfileValueSourceConfiguration.class, + org.springframework.test.annotation.Repeat.class, + org.springframework.test.annotation.Rollback.class, + org.springframework.test.annotation.Timed.class, + + // Core TestContext framework + org.springframework.test.context.ActiveProfiles.class, + org.springframework.test.context.BootstrapWith.class, + org.springframework.test.context.ContextConfiguration.class, + org.springframework.test.context.ContextHierarchy.class, + org.springframework.test.context.DynamicPropertySource.class, + org.springframework.test.context.NestedTestConfiguration.class, + org.springframework.test.context.TestConstructor.class, + org.springframework.test.context.TestExecutionListeners.class, + org.springframework.test.context.TestPropertySource.class, + org.springframework.test.context.TestPropertySources.class, + + // Application Events + org.springframework.test.context.event.RecordApplicationEvents.class, + + // JUnit Jupiter + org.springframework.test.context.junit.jupiter.EnabledIf.class, + org.springframework.test.context.junit.jupiter.DisabledIf.class, + org.springframework.test.context.junit.jupiter.SpringJUnitConfig.class, + org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig.class, + + // Web + org.springframework.test.context.web.WebAppConfiguration.class + ).forEach(type -> assertAnnotationRegistered(runtimeHints, type)); + + // TestExecutionListener + Set.of( + org.springframework.test.context.event.ApplicationEventsTestExecutionListener.class, + org.springframework.test.context.event.EventPublishingTestExecutionListener.class, + org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.class, + org.springframework.test.context.support.DependencyInjectionTestExecutionListener.class, + org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener.class, + org.springframework.test.context.support.DirtiesContextTestExecutionListener.class, + org.springframework.test.context.transaction.TransactionalTestExecutionListener.class, + org.springframework.test.context.web.ServletTestExecutionListener.class + ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS)); + + // ContextCustomizerFactory + Set.of( + "org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory", + "org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory" + ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS)); + } + + private static void assertReflectionRegistered(RuntimeHints runtimeHints, String type, MemberCategory memberCategory) { + assertThat(reflection().onType(TypeReference.of(type)).withMemberCategory(memberCategory)) + .as("Reflection hint for %s with category %s", type, memberCategory) + .accepts(runtimeHints); + } + + private static void assertReflectionRegistered(RuntimeHints runtimeHints, Class type, MemberCategory memberCategory) { + assertThat(reflection().onType(type).withMemberCategory(memberCategory)) + .as("Reflection hint for %s with category %s", type.getSimpleName(), memberCategory) + .accepts(runtimeHints); + } + + private static void assertAnnotationRegistered(RuntimeHints runtimeHints, Class annotationType) { + assertReflectionRegistered(runtimeHints, annotationType, INVOKE_DECLARED_METHODS); + assertThat(runtimeHints.proxies().jdkProxies()) + .as("Proxy hint for annotation @%s", annotationType.getSimpleName()) + .anySatisfy(annotationProxy(annotationType)); + } + + private static Consumer annotationProxy(Class type) { + return jdkProxyHint -> assertThat(jdkProxyHint.getProxiedInterfaces()) + .containsExactly(TypeReference.of(type), TypeReference.of(SynthesizedAnnotation.class)); + } + @Test void processAheadOfTimeWithBasicTests() { // We cannot parameterize with the test classes, since @CompileWithTargetClassAccess