From 65e3548ef5226972001adf3a6339dcca97b3c44e Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Fri, 25 Oct 2024 15:00:10 +0300 Subject: [PATCH] TestcontainersBeanRegistrationAotProcessor that replaces InstanceSupplier of Container by either direct field usage or a reflection equivalent. If the field is private, the reflection will be used; otherwise, direct access to the field will be used DynamicPropertySourceBeanFactoryInitializationAotProcessor that generates methods for each annotated @DynamicPropertySource method --- .../ImportTestcontainersTests.java | 175 +++++++++++++++++- .../DynamicPropertySourceMethodsImporter.java | 162 ++++++++++++++++ .../TestcontainerFieldBeanDefinition.java | 106 ++++++++++- .../TestcontainersPropertySource.java | 15 ++ .../resources/META-INF/spring/aot.factories | 11 +- 5 files changed, 464 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java index c3d0bd43703b..044a4edd0895 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java @@ -18,17 +18,29 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.function.BiConsumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; +import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.containers.PostgreSQLContainer; +import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.boot.testcontainers.beans.TestcontainerBeanDefinition; import org.springframework.boot.testcontainers.context.ImportTestcontainers; +import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer; import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable; import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.ClassName; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @@ -43,6 +55,8 @@ @DisabledIfDockerUnavailable class ImportTestcontainersTests { + private final TestGenerationContext generationContext = new TestGenerationContext(); + private AnnotationConfigApplicationContext applicationContext; @AfterEach @@ -102,7 +116,7 @@ void importWhenHasNonStaticContainerFieldThrowsException() { @Test void importWhenHasContainerDefinitionsWithDynamicPropertySource() { this.applicationContext = new AnnotationConfigApplicationContext( - ContainerDefinitionsWithDynamicPropertySource.class); + ImportWithoutValueWithDynamicPropertySource.class); assertThat(this.applicationContext.getEnvironment().containsProperty("container.port")).isTrue(); } @@ -122,6 +136,119 @@ void importWhenHasBadArgsDynamicPropertySourceMethod() { .withMessage("@DynamicPropertySource method 'containerProperties' must be static"); } + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithoutValueAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportWithoutValue.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ImportWithoutValue.container); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithValueAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportWithValue.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ContainerDefinitions.container); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithoutValueWithDynamicPropertySourceAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportWithoutValueWithDynamicPropertySource.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ImportWithoutValueWithDynamicPropertySource.container); + assertThat(freshContext.getEnvironment().getProperty("container.port", Integer.class)) + .isEqualTo(ImportWithoutValueWithDynamicPropertySource.container.getFirstMappedPort()); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersCustomPostgreSQLContainerDefinitionsAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(CustomPostgreSQLContainerDefinitions.class); + compile((freshContext, compiled) -> { + CustomPostgreSQLContainer container = freshContext.getBean(CustomPostgreSQLContainer.class); + assertThat(container).isSameAs(CustomPostgreSQLContainerDefinitions.container); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithoutValueNotAccessibleContainerAndDynamicPropertySourceAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class); + compile((freshContext, compiled) -> { + MongoDBContainer container = freshContext.getBean(MongoDBContainer.class); + assertThat(container).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container); + assertThat(freshContext.getEnvironment().getProperty("mongo.port", Integer.class)).isEqualTo( + ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort()); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersWithNotAccessibleContainerAndDynamicPropertySourceAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container); + assertThat(freshContext.getEnvironment().getProperty("postgres.port", Integer.class)) + .isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort()); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersMultipleContainersAndDynamicPropertySourcesAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class); + this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class); + compile((freshContext, compiled) -> { + MongoDBContainer mongo = freshContext.getBean(MongoDBContainer.class); + PostgreSQLContainer postgres = freshContext.getBean(PostgreSQLContainer.class); + assertThat(mongo).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container); + assertThat(postgres).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container); + ConfigurableEnvironment environment = freshContext.getEnvironment(); + assertThat(environment.getProperty("postgres.port", Integer.class)) + .isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort()); + assertThat(environment.getProperty("mongo.port", Integer.class)).isEqualTo( + ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort()); + }); + } + + @SuppressWarnings("unchecked") + private void compile(BiConsumer result) { + ClassName className = processAheadOfTime(); + TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> { + try (GenericApplicationContext context = new GenericApplicationContext()) { + new TestcontainersLifecycleApplicationContextInitializer().initialize(context); + ApplicationContextInitializer initializer = compiled + .getInstance(ApplicationContextInitializer.class, className.toString()); + initializer.initialize(context); + context.refresh(); + result.accept(context, compiled); + } + }); + } + + private ClassName processAheadOfTime() { + ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext, + this.generationContext); + this.generationContext.writeGeneratedContent(); + return className; + } + @ImportTestcontainers static class ImportWithoutValue { @@ -161,13 +288,25 @@ interface ContainerDefinitions { } + private interface ContainerDefinitionsWithDynamicPropertySource { + + @ContainerAnnotation + PostgreSQLContainer container = TestImage.container(PostgreSQLContainer.class); + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("postgres.port", container::getFirstMappedPort); + } + + } + @Retention(RetentionPolicy.RUNTIME) @interface ContainerAnnotation { } @ImportTestcontainers - static class ContainerDefinitionsWithDynamicPropertySource { + static class ImportWithoutValueWithDynamicPropertySource { static PostgreSQLContainer container = TestImage.container(PostgreSQLContainer.class); @@ -196,4 +335,36 @@ void containerProperties() { } + @ImportTestcontainers + static class CustomPostgreSQLContainerDefinitions { + + private static final CustomPostgreSQLContainer container = new CustomPostgreSQLContainer(); + + } + + static class CustomPostgreSQLContainer extends PostgreSQLContainer { + + CustomPostgreSQLContainer() { + super("postgres:14"); + } + + } + + @ImportTestcontainers + static class ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource { + + private static final MongoDBContainer container = TestImage.container(MongoDBContainer.class); + + @DynamicPropertySource + private static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("mongo.port", container::getFirstMappedPort); + } + + } + + @ImportTestcontainers(ContainerDefinitionsWithDynamicPropertySource.class) + static class ImportWithValueAndDynamicPropertySource { + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java index d680f7504c81..d11ba194571f 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java @@ -18,16 +18,37 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Map; import java.util.Set; +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource; import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; /** @@ -56,6 +77,16 @@ void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistr ReflectionUtils.makeAccessible(method); ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry); }); + + String beanName = "importTestContainer.%s.%s".formatted(DynamicPropertySource.class.getName(), definitionClass); + if (!beanDefinitionRegistry.containsBeanDefinition(beanName)) { + RootBeanDefinition bd = new RootBeanDefinition(DynamicPropertySourceMetadata.class); + bd.setInstanceSupplier(() -> new DynamicPropertySourceMetadata(definitionClass, methods)); + bd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + bd.setAutowireCandidate(false); + bd.setAttribute(DynamicPropertySourceMetadata.class.getName(), true); + beanDefinitionRegistry.registerBeanDefinition(beanName, bd); + } } private boolean isAnnotated(Method method) { @@ -71,4 +102,135 @@ private void assertValid(Method method) { + "' must accept a single DynamicPropertyRegistry argument"); } + private record DynamicPropertySourceMetadata(Class definitionClass, Set methods) { + } + + /** + * {@link BeanRegistrationExcludeFilter} to exclude + * {@link DynamicPropertySourceMetadata} from AOT bean registrations. + */ + static class DynamicPropertySourceMetadataBeanRegistrationExcludeFilter implements BeanRegistrationExcludeFilter { + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return registeredBean.getMergedBeanDefinition().hasAttribute(DynamicPropertySourceMetadata.class.getName()); + } + + } + + /** + * The {@link BeanFactoryInitializationAotProcessor} generates methods for each + * {@code @DynamicPropertySource-annotated} method. + * + */ + static class DynamicPropertySourceBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor { + + private static final String DYNAMIC_PROPERTY_REGISTRY = "dynamicPropertyRegistry"; + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + Map metadata = beanFactory + .getBeansOfType(DynamicPropertySourceMetadata.class, false, false); + if (metadata.isEmpty()) { + return null; + } + return new AotContibution(metadata); + } + + private static final class AotContibution implements BeanFactoryInitializationAotContribution { + + private final Map metadata; + + private AotContibution(Map metadata) { + this.metadata = metadata; + } + + @Override + public void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode) { + GeneratedMethod initializerMethod = beanFactoryInitializationCode.getMethods() + .add("registerDynamicPropertySources", (code) -> { + code.addJavadoc("Registers {@code @DynamicPropertySource} properties"); + code.addParameter(ConfigurableEnvironment.class, "environment"); + code.addParameter(DefaultListableBeanFactory.class, "beanFactory"); + code.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); + code.addStatement("$T dynamicPropertyRegistry = $T.attach(environment, beanFactory)", + DynamicPropertyRegistry.class, TestcontainersPropertySource.class); + this.metadata.forEach((name, metadata) -> { + GeneratedMethod dynamicPropertySourceMethod = generateMethods(generationContext, metadata); + code.addStatement(dynamicPropertySourceMethod.toMethodReference() + .toInvokeCodeBlock(ArgumentCodeGenerator.of(DynamicPropertyRegistry.class, + DYNAMIC_PROPERTY_REGISTRY))); + }); + }); + beanFactoryInitializationCode.addInitializer(initializerMethod.toMethodReference()); + } + + // Generates a new class in definition class package and invokes + // all @DynamicPropertySource methods. + private GeneratedMethod generateMethods(GenerationContext generationContext, + DynamicPropertySourceMetadata metadata) { + Class definitionClass = metadata.definitionClass(); + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .addForFeatureComponent(DynamicPropertySource.class.getSimpleName(), definitionClass, + (code) -> code.addModifiers(javax.lang.model.element.Modifier.PUBLIC)); + return generatedClass.getMethods().add("registerDynamicPropertySource", (code) -> { + code.addJavadoc("Registers {@code @DynamicPropertySource} properties for class '$T'", + definitionClass); + code.addParameter(DynamicPropertyRegistry.class, DYNAMIC_PROPERTY_REGISTRY); + code.addModifiers(javax.lang.model.element.Modifier.PUBLIC, + javax.lang.model.element.Modifier.STATIC); + metadata.methods().forEach((method) -> { + GeneratedMethod generateMethod = generateMethod(generationContext, generatedClass, method); + code.addStatement(generateMethod.toMethodReference() + .toInvokeCodeBlock(ArgumentCodeGenerator.of(DynamicPropertyRegistry.class, + DYNAMIC_PROPERTY_REGISTRY))); + }); + }); + } + + // If the method is inaccessible, the reflection will be used; otherwise, + // direct call to the method will be used. + private static GeneratedMethod generateMethod(GenerationContext generationContext, + GeneratedClass generatedClass, Method method) { + return generatedClass.getMethods().add(method.getName(), (code) -> { + code.addJavadoc("Register {@code @DynamicPropertySource} for method '$T.$L'", + method.getDeclaringClass(), method.getName()); + code.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); + code.addParameter(DynamicPropertyRegistry.class, DYNAMIC_PROPERTY_REGISTRY); + if (isMethodAccessible(generatedClass, method)) { + code.addStatement(CodeBlock.of("$T.$L($L)", method.getDeclaringClass(), method.getName(), + DYNAMIC_PROPERTY_REGISTRY)); + } + else { + generationContext.getRuntimeHints().reflection().registerMethod(method, ExecutableMode.INVOKE); + code.beginControlFlow("try"); + code.addStatement("$T clazz = $T.forName($S, $T.class.getClassLoader())", Class.class, + ClassUtils.class, ClassName.get(method.getDeclaringClass()), generatedClass.getName()); + // ReflectionTestUtils can be used here because + // @DynamicPropertyRegistry in a test module. + code.addStatement("$T.invokeMethod(clazz, $S, $L)", ReflectionTestUtils.class, method.getName(), + DYNAMIC_PROPERTY_REGISTRY); + code.nextControlFlow("catch ($T ex)", ClassNotFoundException.class); + code.addStatement("throw new $T(ex)", RuntimeException.class); + code.endControlFlow(); + } + }); + + } + + private static boolean isMethodAccessible(GeneratedClass generatedClass, Method method) { + ClassName className = generatedClass.getName(); + return AccessControl.forClass(method.getDeclaringClass()).isAccessibleFrom(className) + && AccessControl.forMember(method).isAccessibleFrom(className); + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/TestcontainerFieldBeanDefinition.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/TestcontainerFieldBeanDefinition.java index c5cf32d4b1aa..cb181bab9b7c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/TestcontainerFieldBeanDefinition.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/TestcontainerFieldBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -18,11 +18,26 @@ import java.lang.reflect.Field; +import javax.lang.model.element.Modifier; + import org.testcontainers.containers.Container; +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.testcontainers.beans.TestcontainerBeanDefinition; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * {@link RootBeanDefinition} used for testcontainer bean definitions. @@ -38,9 +53,10 @@ class TestcontainerFieldBeanDefinition extends RootBeanDefinition implements Tes TestcontainerFieldBeanDefinition(Field field, Container container) { this.container = container; this.annotations = MergedAnnotations.from(field); - this.setBeanClass(container.getClass()); + setBeanClass(container.getClass()); setInstanceSupplier(() -> container); setRole(ROLE_INFRASTRUCTURE); + setAttribute(TestcontainerFieldBeanDefinition.class.getName(), field); } @Override @@ -53,4 +69,90 @@ public MergedAnnotations getAnnotations() { return this.annotations; } + /** + * {@link BeanRegistrationAotProcessor} that replaces InstanceSupplier of + * {@link Container} by either direct field usage or a reflection equivalent. + *

+ * If the field is private, the reflection will be used; otherwise, direct access to + * the field will be used. + * + */ + static class TestcontainersBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + RootBeanDefinition bd = registeredBean.getMergedBeanDefinition(); + String attributeName = TestcontainerFieldBeanDefinition.class.getName(); + Object field = bd.getAttribute(attributeName); + if (field != null) { + Assert.isInstanceOf(Field.class, field, "BeanDefinition attribute '" + attributeName + + "' value must be a type of '" + Field.class + "'"); + return BeanRegistrationAotContribution.withCustomCodeFragments( + (codeFragments) -> new AotContribution(codeFragments, registeredBean, ((Field) field))); + } + return null; + } + + private static final class AotContribution extends BeanRegistrationCodeFragmentsDecorator { + + private final RegisteredBean registeredBean; + + private final Field field; + + private AotContribution(BeanRegistrationCodeFragments delegate, RegisteredBean registeredBean, + Field field) { + super(delegate); + this.registeredBean = registeredBean; + this.field = field; + } + + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(this.field.getDeclaringClass()); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + + if (isFieldAccessible(beanRegistrationCode, this.field)) { + return CodeBlock.of("() -> $T.$L", this.field.getDeclaringClass(), this.field.getName()); + } + + generationContext.getRuntimeHints().reflection().registerField(this.field); + + return beanRegistrationCode.getMethods() + .add("getInstance", (method) -> method.addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()) + .returns(this.registeredBean.getBeanClass()) + .beginControlFlow("try") + .addStatement("$T clazz = $T.forName($S, $T.class.getClassLoader())", Class.class, + ClassUtils.class, ClassName.get(this.field.getDeclaringClass()), + beanRegistrationCode.getClassName()) + .addStatement("$T field = $T.findField(clazz, $S)", Field.class, ReflectionUtils.class, + this.field.getName()) + .addStatement("$T.notNull(field, $S)", Assert.class, + "Field '" + this.field.getName() + "' is not found") + .addStatement("$T.makeAccessible(field)", ReflectionUtils.class) + .addStatement("$T container = $T.getField(field, null)", Object.class, ReflectionUtils.class) + .addStatement("$T.notNull(container, $S)", Assert.class, + "Container field '" + this.field.getName() + "' must not have a null value") + .addStatement("return ($T) container", this.registeredBean.getBeanClass()) + .nextControlFlow("catch ($T ex)", ClassNotFoundException.class) + .addStatement("throw new $T(ex)", RuntimeException.class) + .endControlFlow()) + .toMethodReference() + .toCodeBlock(); + } + + private static boolean isFieldAccessible(BeanRegistrationCode beanRegistrationCode, Field field) { + ClassName className = beanRegistrationCode.getClassName(); + return AccessControl.forClass(field.getDeclaringClass()).isAccessibleFrom(className) + && AccessControl.forMember(field).isAccessibleFrom(className); + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java index f1ecfe878c80..995cc47ec9de 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java @@ -26,9 +26,11 @@ import org.testcontainers.containers.Container; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -166,4 +168,17 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } + /** + * {@link BeanRegistrationExcludeFilter} to exclude {@link EventPublisherRegistrar} + * from AOT bean registration. + */ + static class EventPublisherRegistrarBeanRegistrationExcludeFilter implements BeanRegistrationExcludeFilter { + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return EventPublisherRegistrar.NAME.equals(registeredBean.getBeanName()); + } + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories index 5b3d49bd5020..1fd859d34a6a 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories @@ -1,5 +1,14 @@ org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\ -org.springframework.boot.testcontainers.service.connection.ConnectionDetailsRegistrar.ServiceConnectionBeanRegistrationExcludeFilter +org.springframework.boot.testcontainers.service.connection.ConnectionDetailsRegistrar.ServiceConnectionBeanRegistrationExcludeFilter,\ +org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.EventPublisherRegistrarBeanRegistrationExcludeFilter,\ +org.springframework.boot.testcontainers.context.DynamicPropertySourceMethodsImporter.DynamicPropertySourceMetadataBeanRegistrationExcludeFilter org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory.ContainerConnectionDetailsFactoriesRuntimeHints + +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.boot.testcontainers.context.TestcontainerFieldBeanDefinition.TestcontainersBeanRegistrationAotProcessor + + +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ +org.springframework.boot.testcontainers.context.DynamicPropertySourceMethodsImporter.DynamicPropertySourceBeanFactoryInitializationAotProcessor