diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java new file mode 100644 index 0000000000..c68e7254ac --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java @@ -0,0 +1,135 @@ +/* + * 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.reflect.Method; +import java.util.Arrays; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ClassUtils; + +import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; +import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; + +/** + * {@code MergedContextConfigurationRuntimeHints} registers run-time hints for + * standard functionality in the Spring TestContext Framework based on + * {@link MergedContextConfiguration}. + * + *

This class interacts with {@code org.springframework.test.context.web.WebMergedContextConfiguration} + * via reflection to avoid a package cycle. + * + * @author Sam Brannen + * @since 6.0 + */ +class MergedContextConfigurationRuntimeHints { + + private static final String SLASH = "/"; + + private static final String WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME = + "org.springframework.test.context.web.WebMergedContextConfiguration"; + + private static final String GET_RESOURCE_BASE_PATH_METHOD_NAME = "getResourceBasePath"; + + private static final Class webMergedContextConfigurationClass = loadWebMergedContextConfigurationClass(); + + private static final Method getResourceBasePathMethod = loadGetResourceBasePathMethod(); + + + public void registerHints(RuntimeHints runtimeHints, MergedContextConfiguration mergedConfig, ClassLoader classLoader) { + // @ContextConfiguration(loader = ...) + ContextLoader contextLoader = mergedConfig.getContextLoader(); + if (contextLoader != null) { + registerDeclaredConstructors(contextLoader.getClass(), runtimeHints); + } + + // @ContextConfiguration(initializers = ...) + mergedConfig.getContextInitializerClasses() + .forEach(clazz -> registerDeclaredConstructors(clazz, runtimeHints)); + + // @ContextConfiguration(locations = ...) + registerClasspathResources(mergedConfig.getLocations(), runtimeHints, classLoader); + + // @TestPropertySource(locations = ... ) + registerClasspathResources(mergedConfig.getPropertySourceLocations(), runtimeHints, classLoader); + + // @WebAppConfiguration(value = ...) + if (webMergedContextConfigurationClass.isInstance(mergedConfig)) { + String resourceBasePath = null; + try { + resourceBasePath = (String) getResourceBasePathMethod.invoke(mergedConfig); + } + catch (Exception ex) { + throw new IllegalStateException( + "Failed to invoke WebMergedContextConfiguration#getResourceBasePath()", ex); + } + registerClasspathResourceDirectoryStructure(resourceBasePath, runtimeHints); + } + } + + private void registerDeclaredConstructors(Class type, RuntimeHints runtimeHints) { + runtimeHints.reflection().registerType(type, INVOKE_DECLARED_CONSTRUCTORS); + } + + private void registerClasspathResources(String[] paths, RuntimeHints runtimeHints, ClassLoader classLoader) { + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader); + Arrays.stream(paths) + .filter(path -> path.startsWith(CLASSPATH_URL_PREFIX)) + .map(resourceLoader::getResource) + .forEach(runtimeHints.resources()::registerResource); + } + + private void registerClasspathResourceDirectoryStructure(String directory, RuntimeHints runtimeHints) { + if (directory.startsWith(CLASSPATH_URL_PREFIX)) { + String pattern = directory.substring(CLASSPATH_URL_PREFIX.length()); + if (pattern.startsWith(SLASH)) { + pattern = pattern.substring(1); + } + if (!pattern.endsWith(SLASH)) { + pattern += SLASH; + } + pattern += "*"; + runtimeHints.resources().registerPattern(pattern); + } + } + + @SuppressWarnings("unchecked") + private static Class loadWebMergedContextConfigurationClass() { + try { + return ClassUtils.forName(WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, + MergedContextConfigurationRuntimeHints.class.getClassLoader()); + } + catch (ClassNotFoundException | LinkageError ex) { + throw new IllegalStateException( + "Failed to load class " + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ex); + } + } + + private static Method loadGetResourceBasePathMethod() { + try { + return webMergedContextConfigurationClass.getMethod(GET_RESOURCE_BASE_PATH_METHOD_NAME); + } + catch (Exception ex) { + throw new IllegalStateException( + "Failed to load method WebMergedContextConfiguration#getResourceBasePath()", ex); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index 004f7a597a..c71ef727e0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -16,7 +16,6 @@ package org.springframework.test.context.aot; -import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -67,6 +66,9 @@ public class TestContextAotGenerator { private final AotServices testRuntimeHintsRegistrars; + private final MergedContextConfigurationRuntimeHints mergedConfigRuntimeHints = + new MergedContextConfigurationRuntimeHints(); + private final AtomicInteger sequence = new AtomicInteger(); private final GeneratedFiles generatedFiles; @@ -115,7 +117,14 @@ public class TestContextAotGenerator { resetAotFactories(); MultiValueMap> mergedConfigMappings = new LinkedMultiValueMap<>(); - testClasses.forEach(testClass -> mergedConfigMappings.add(buildMergedContextConfiguration(testClass), testClass)); + ClassLoader classLoader = getClass().getClassLoader(); + testClasses.forEach(testClass -> { + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + mergedConfigMappings.add(mergedConfig, testClass); + this.testRuntimeHintsRegistrars.forEach(registrar -> + registrar.registerHints(this.runtimeHints, testClass, classLoader)); + this.mergedConfigRuntimeHints.registerHints(this.runtimeHints, mergedConfig, classLoader); + }); MultiValueMap> initializerClassMappings = processAheadOfTime(mergedConfigMappings); generateTestAotMappings(initializerClassMappings); @@ -137,9 +146,6 @@ public class TestContextAotGenerator { logger.debug(LogMessage.format("Generating AOT artifacts for test classes %s", testClasses.stream().map(Class::getName).toList())); try { - this.testRuntimeHintsRegistrars.forEach(registrar -> registrar.registerHints(mergedConfig, - Collections.unmodifiableList(testClasses), this.runtimeHints, getClass().getClassLoader())); - // Use first test class discovered for a given unique MergedContextConfiguration. Class testClass = testClasses.get(0); DefaultGenerationContext generationContext = createGenerationContext(testClass); diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestRuntimeHintsRegistrar.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestRuntimeHintsRegistrar.java index 0595a7e915..e347d8cd65 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestRuntimeHintsRegistrar.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestRuntimeHintsRegistrar.java @@ -16,10 +16,7 @@ package org.springframework.test.context.aot; -import java.util.List; - import org.springframework.aot.hint.RuntimeHints; -import org.springframework.test.context.MergedContextConfiguration; /** * Contract for registering {@link RuntimeHints} for integration tests run with @@ -34,8 +31,8 @@ import org.springframework.test.context.MergedContextConfiguration; *

This API serves as a companion to the core * {@link org.springframework.aot.hint.RuntimeHintsRegistrar RuntimeHintsRegistrar} * API. If you need to register global hints for testing support that are not - * specific to a particular test class or {@link MergedContextConfiguration}, favor - * implementing {@code RuntimeHintsRegistrar} over this API. + * specific to particular test classes, favor implementing {@code RuntimeHintsRegistrar} + * over this API. * * @author Sam Brannen * @since 6.0 @@ -45,13 +42,10 @@ public interface TestRuntimeHintsRegistrar { /** * Contribute hints to the given {@link RuntimeHints} instance. - * @param mergedConfig the merged context configuration to process - * @param testClasses the test classes that share the supplied merged context - * configuration * @param runtimeHints the {@code RuntimeHints} to use + * @param testClass the test class to process * @param classLoader the classloader to use */ - void registerHints(MergedContextConfiguration mergedConfig, List> testClasses, - RuntimeHints runtimeHints, ClassLoader classLoader); + void registerHints(RuntimeHints runtimeHints, Class testClass, ClassLoader classLoader); } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java index fbc2c70651..7cf688f4eb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java @@ -16,23 +16,15 @@ package org.springframework.test.context.aot.hint; -import java.util.Arrays; -import java.util.List; - import org.springframework.aot.hint.RuntimeHints; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.io.DefaultResourceLoader; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; -import org.springframework.test.context.ContextLoader; -import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.aot.TestRuntimeHintsRegistrar; -import org.springframework.test.context.web.WebMergedContextConfiguration; import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; -import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; /** * {@link TestRuntimeHintsRegistrar} implementation that registers run-time hints @@ -44,43 +36,8 @@ import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; */ class StandardTestRuntimeHints implements TestRuntimeHintsRegistrar { - private static final String SLASH = "/"; - - @Override - public void registerHints(MergedContextConfiguration mergedConfig, List> testClasses, - RuntimeHints runtimeHints, ClassLoader classLoader) { - - registerHintsForMergedContextConfiguration(mergedConfig, runtimeHints, classLoader); - testClasses.forEach(testClass -> registerHintsForActiveProfilesResolvers(testClass, runtimeHints)); - } - - private void registerHintsForMergedContextConfiguration( - MergedContextConfiguration mergedConfig, RuntimeHints runtimeHints, ClassLoader classLoader) { - - // @ContextConfiguration(loader = ...) - ContextLoader contextLoader = mergedConfig.getContextLoader(); - if (contextLoader != null) { - registerDeclaredConstructors(contextLoader.getClass(), runtimeHints); - } - - // @ContextConfiguration(initializers = ...) - mergedConfig.getContextInitializerClasses() - .forEach(clazz -> registerDeclaredConstructors(clazz, runtimeHints)); - - // @ContextConfiguration(locations = ...) - registerClasspathResources(mergedConfig.getLocations(), runtimeHints, classLoader); - - // @TestPropertySource(locations = ... ) - registerClasspathResources(mergedConfig.getPropertySourceLocations(), runtimeHints, classLoader); - - // @WebAppConfiguration(value = ...) - if (mergedConfig instanceof WebMergedContextConfiguration webConfig) { - registerClasspathResourceDirectoryStructure(webConfig.getResourceBasePath(), runtimeHints); - } - } - - private void registerHintsForActiveProfilesResolvers(Class testClass, RuntimeHints runtimeHints) { + public void registerHints(RuntimeHints runtimeHints, Class testClass, ClassLoader classLoader) { // @ActiveProfiles(resolver = ...) MergedAnnotations.search(TYPE_HIERARCHY) .withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass) @@ -95,26 +52,4 @@ class StandardTestRuntimeHints implements TestRuntimeHintsRegistrar { runtimeHints.reflection().registerType(type, INVOKE_DECLARED_CONSTRUCTORS); } - private void registerClasspathResources(String[] paths, RuntimeHints runtimeHints, ClassLoader classLoader) { - DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader); - Arrays.stream(paths) - .filter(path -> path.startsWith(CLASSPATH_URL_PREFIX)) - .map(resourceLoader::getResource) - .forEach(runtimeHints.resources()::registerResource); - } - - private void registerClasspathResourceDirectoryStructure(String directory, RuntimeHints runtimeHints) { - if (directory.startsWith(CLASSPATH_URL_PREFIX)) { - String pattern = directory.substring(CLASSPATH_URL_PREFIX.length()); - if (pattern.startsWith(SLASH)) { - pattern = pattern.substring(1); - } - if (!pattern.endsWith(SLASH)) { - pattern += SLASH; - } - pattern += "*"; - runtimeHints.resources().registerPattern(pattern); - } - } - }