Simplify TestRuntimeHintsRegistrar API

Prior to this commit, the TestRuntimeHintsRegistrar API combined
processing of MergedContextConfiguration and test classes. However, it
appears that only spring-test internals have a need for registering
hints based on the MergedContextConfiguration. For example, Spring
Boot's AOT testing support has not had such a need, and it is assumed
that third parties likely will not have such a need.

In light of that, this commit simplifies the TestRuntimeHintsRegistrar
API so that it focuses on processing of a single test class.

In addition, this commit moves the hint registration code specific to
MergedContextConfiguration to an internal mechanism.

Closes gh-29264
This commit is contained in:
Sam Brannen 2022-10-05 18:37:01 +02:00
parent 7ad65b8dff
commit 935265048a
4 changed files with 151 additions and 81 deletions

View File

@ -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 <em>Spring TestContext Framework</em> based on
* {@link MergedContextConfiguration}.
*
* <p>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);
}
}
}

View File

@ -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<TestRuntimeHintsRegistrar> 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<MergedContextConfiguration, Class<?>> 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<ClassName, Class<?>> 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);

View File

@ -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;
* <p>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<Class<?>> testClasses,
RuntimeHints runtimeHints, ClassLoader classLoader);
void registerHints(RuntimeHints runtimeHints, Class<?> testClass, ClassLoader classLoader);
}

View File

@ -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<Class<?>> 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);
}
}
}