From bca35dc0a235eb43b161a7c71ab11e8aa420b686 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 10 Oct 2022 12:09:17 +0200 Subject: [PATCH] Generate direct mappings from AOT initializers to test classes See gh-29289 --- .../aot/AotTestContextInitializers.java | 29 ++++++++-- ...tTestContextInitializersCodeGenerator.java | 58 +++++++++++++++---- .../AotTestContextInitializersFactory.java | 27 ++++++++- ...efaultCacheAwareContextLoaderDelegate.java | 3 +- 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java index 7dbf09f45d..a377ce7c09 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java @@ -32,8 +32,8 @@ import org.springframework.lang.Nullable; * *

If we are not running in {@linkplain AotDetector#useGeneratedArtifacts() * AOT mode} or if a test class is not {@linkplain #isSupportedTestClass(Class) - * supported} in AOT mode, {@link #getContextInitializer(Class)} will return - * {@code null}. + * supported} in AOT mode, {@link #getContextInitializer(Class)} and + * {@link #getContextInitializerClass(Class)} will return {@code null}. * * @author Sam Brannen * @since 6.0 @@ -42,13 +42,20 @@ public class AotTestContextInitializers { private final Map>> contextInitializers; + private final Map>> contextInitializerClasses; + public AotTestContextInitializers() { - this(AotTestContextInitializersFactory.getContextInitializers()); + this(AotTestContextInitializersFactory.getContextInitializers(), + AotTestContextInitializersFactory.getContextInitializerClasses()); } - AotTestContextInitializers(Map>> contextInitializers) { + AotTestContextInitializers( + Map>> contextInitializers, + Map>> contextInitializerClasses) { + this.contextInitializers = contextInitializers; + this.contextInitializerClasses = contextInitializerClasses; } @@ -67,6 +74,7 @@ public class AotTestContextInitializers { * @return the AOT context initializer, or {@code null} if there is no AOT context * initializer for the specified test class * @see #isSupportedTestClass(Class) + * @see #getContextInitializerClass(Class) */ @Nullable public ApplicationContextInitializer getContextInitializer(Class testClass) { @@ -75,4 +83,17 @@ public class AotTestContextInitializers { return (supplier != null ? supplier.get() : null); } + /** + * Get the AOT {@link ApplicationContextInitializer} {@link Class} for the + * specified test class. + * @return the AOT context initializer class, or {@code null} if there is no + * AOT context initializer for the specified test class + * @see #isSupportedTestClass(Class) + * @see #getContextInitializer(Class) + */ + @Nullable + public Class> getContextInitializerClass(Class testClass) { + return this.contextInitializerClasses.get(testClass.getName()); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersCodeGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersCodeGenerator.java index 80944e69ec..6dfff83829 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersCodeGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersCodeGenerator.java @@ -50,24 +50,38 @@ class AotTestContextInitializersCodeGenerator { private static final Log logger = LogFactory.getLog(AotTestContextInitializersCodeGenerator.class); + // ApplicationContextInitializer private static final ParameterizedTypeName CONTEXT_INITIALIZER = ParameterizedTypeName.get( ClassName.get(ApplicationContextInitializer.class), WildcardTypeName.subtypeOf(ConfigurableApplicationContext.class)); + // Supplier> private static final ParameterizedTypeName CONTEXT_INITIALIZER_SUPPLIER = ParameterizedTypeName .get(ClassName.get(Supplier.class), CONTEXT_INITIALIZER); // Map>> - private static final TypeName CONTEXT_SUPPLIER_MAP = ParameterizedTypeName + private static final TypeName CONTEXT_INITIALIZER_SUPPLIER_MAP = ParameterizedTypeName .get(ClassName.get(Map.class), ClassName.get(String.class), CONTEXT_INITIALIZER_SUPPLIER); + // Class + private static final ParameterizedTypeName CONTEXT_INITIALIZER_CLASS = ParameterizedTypeName + .get(ClassName.get(Class.class), WildcardTypeName.subtypeOf( + ParameterizedTypeName.get(ClassName.get(ApplicationContextInitializer.class), + WildcardTypeName.subtypeOf(Object.class)))); + + // Map>> + private static final TypeName CONTEXT_INITIALIZER_CLASS_MAP = ParameterizedTypeName + .get(ClassName.get(Map.class), ClassName.get(String.class), CONTEXT_INITIALIZER_CLASS); + private static final String GENERATED_SUFFIX = "Generated"; // TODO Consider an alternative means for specifying the name of the generated class. // Ideally we would generate a class named: org.springframework.test.context.aot.GeneratedAotTestContextInitializers static final String GENERATED_MAPPINGS_CLASS_NAME = AotTestContextInitializers.class.getName() + "__" + GENERATED_SUFFIX; - static final String GENERATED_MAPPINGS_METHOD_NAME = "getContextInitializers"; + static final String GET_CONTEXT_INITIALIZERS_METHOD_NAME = "getContextInitializers"; + + static final String GET_CONTEXT_INITIALIZER_CLASSES_METHOD_NAME = "getContextInitializerClasses"; private final MultiValueMap> initializerClassMappings; @@ -92,24 +106,25 @@ class AotTestContextInitializersCodeGenerator { this.generatedClass.getName().reflectionName())); type.addJavadoc("Generated mappings for {@link $T}.", AotTestContextInitializers.class); type.addModifiers(Modifier.PUBLIC); - type.addMethod(generateMappingMethod()); + type.addMethod(contextInitializersMappingMethod()); + type.addMethod(contextInitializerClassesMappingMethod()); } - private MethodSpec generateMappingMethod() { - MethodSpec.Builder method = MethodSpec.methodBuilder(GENERATED_MAPPINGS_METHOD_NAME); + private MethodSpec contextInitializersMappingMethod() { + MethodSpec.Builder method = MethodSpec.methodBuilder(GET_CONTEXT_INITIALIZERS_METHOD_NAME); method.addModifiers(Modifier.PUBLIC, Modifier.STATIC); - method.returns(CONTEXT_SUPPLIER_MAP); - method.addCode(generateMappingCode()); + method.returns(CONTEXT_INITIALIZER_SUPPLIER_MAP); + method.addCode(generateContextInitializersMappingCode()); return method.build(); } - private CodeBlock generateMappingCode() { + private CodeBlock generateContextInitializersMappingCode() { CodeBlock.Builder code = CodeBlock.builder(); - code.addStatement("$T map = new $T<>()", CONTEXT_SUPPLIER_MAP, HashMap.class); + code.addStatement("$T map = new $T<>()", CONTEXT_INITIALIZER_SUPPLIER_MAP, HashMap.class); this.initializerClassMappings.forEach((className, testClasses) -> { List testClassNames = testClasses.stream().map(Class::getName).toList(); logger.debug(LogMessage.format( - "Generating mapping from AOT context initializer [%s] to test classes %s", + "Generating mapping from AOT context initializer supplier [%s] to test classes %s", className.reflectionName(), testClassNames)); testClassNames.forEach(testClassName -> code.addStatement("map.put($S, () -> new $T())", testClassName, className)); @@ -118,4 +133,27 @@ class AotTestContextInitializersCodeGenerator { return code.build(); } + private MethodSpec contextInitializerClassesMappingMethod() { + MethodSpec.Builder method = MethodSpec.methodBuilder(GET_CONTEXT_INITIALIZER_CLASSES_METHOD_NAME); + method.addModifiers(Modifier.PUBLIC, Modifier.STATIC); + method.returns(CONTEXT_INITIALIZER_CLASS_MAP); + method.addCode(generateContextInitializerClassesMappingCode()); + return method.build(); + } + + private CodeBlock generateContextInitializerClassesMappingCode() { + CodeBlock.Builder code = CodeBlock.builder(); + code.addStatement("$T map = new $T<>()", CONTEXT_INITIALIZER_CLASS_MAP, HashMap.class); + this.initializerClassMappings.forEach((className, testClasses) -> { + List testClassNames = testClasses.stream().map(Class::getName).toList(); + logger.debug(LogMessage.format( + "Generating mapping from AOT context initializer class [%s] to test classes %s", + className.reflectionName(), testClassNames)); + testClassNames.forEach(testClassName -> + code.addStatement("map.put($S, $T.class)", testClassName, className)); + }); + code.addStatement("return map"); + return code.build(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java index e714dcf6db..8700ced840 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java @@ -35,6 +35,9 @@ final class AotTestContextInitializersFactory { @Nullable private static volatile Map>> contextInitializers; + @Nullable + private static volatile Map>> contextInitializerClasses; + private AotTestContextInitializersFactory() { } @@ -59,6 +62,20 @@ final class AotTestContextInitializersFactory { return initializers; } + static Map>> getContextInitializerClasses() { + Map>> initializerClasses = contextInitializerClasses; + if (initializerClasses == null) { + synchronized (AotTestContextInitializersFactory.class) { + initializerClasses = contextInitializerClasses; + if (initializerClasses == null) { + initializerClasses = (AotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of()); + contextInitializerClasses = initializerClasses; + } + } + } + return initializerClasses; + } + /** * Reset the factory. *

Only for internal use. @@ -66,13 +83,21 @@ final class AotTestContextInitializersFactory { static void reset() { synchronized (AotTestContextInitializersFactory.class) { contextInitializers = null; + contextInitializerClasses = null; } } @SuppressWarnings("unchecked") private static Map>> loadContextInitializersMap() { String className = AotTestContextInitializersCodeGenerator.GENERATED_MAPPINGS_CLASS_NAME; - String methodName = AotTestContextInitializersCodeGenerator.GENERATED_MAPPINGS_METHOD_NAME; + String methodName = AotTestContextInitializersCodeGenerator.GET_CONTEXT_INITIALIZERS_METHOD_NAME; + return GeneratedMapUtils.loadMap(className, methodName); + } + + @SuppressWarnings("unchecked") + private static Map>> loadContextInitializerClassesMap() { + String className = AotTestContextInitializersCodeGenerator.GENERATED_MAPPINGS_CLASS_NAME; + String methodName = AotTestContextInitializersCodeGenerator.GET_CONTEXT_INITIALIZER_CLASSES_METHOD_NAME; return GeneratedMapUtils.loadMap(className, methodName); } diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java index 4f9f6784f2..6566ed4dfd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java @@ -209,8 +209,7 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext Class testClass = mergedConfig.getTestClass(); if (this.aotTestContextInitializers.isSupportedTestClass(testClass)) { Class> contextInitializerClass = - (Class>) - this.aotTestContextInitializers.getContextInitializer(testClass).getClass(); + this.aotTestContextInitializers.getContextInitializerClass(testClass); return new AotMergedContextConfiguration(testClass, contextInitializerClass, mergedConfig, this); } return mergedConfig;