From b39e937d1d9736e8fb38c3514cabba4b2f2be667 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Sat, 11 Jan 2025 10:48:46 +0200 Subject: [PATCH] ImportTestcontainers doesn't work with AOT Add TestcontainersBeanRegistrationAotProcessor that replaces InstanceSupplier of Container by either direct field usage or a reflection equivalent. Add DynamicPropertySourceBeanFactoryInitializationAotProcessor that generates methods for each annotated @DynamicPropertySource method See gh-42891 Signed-off-by: Dmytro Nosan --- .../ImportTestcontainersAotTests.java | 241 ++++++++++++++++++ .../DynamicPropertySourceMethodsImporter.java | 160 +++++++++++- .../TestcontainerFieldBeanDefinition.java | 106 +++++++- .../TestcontainersPropertySource.java | 17 +- .../resources/META-INF/spring/aot.factories | 11 +- 5 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersAotTests.java diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersAotTests.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersAotTests.java new file mode 100644 index 000000000000..35b0e6cebb6c --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersAotTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2012-2025 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.boot.testcontainers; + +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.MongoDBContainer; +import org.testcontainers.containers.PostgreSQLContainer; + +import org.springframework.aot.test.generate.TestGenerationContext; +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; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * AoT Tests for {@link ImportTestcontainers}. + * + * @author Dmytro Nosan + */ +@DisabledIfDockerUnavailable +class ImportTestcontainersAotTests { + + private final TestGenerationContext generationContext = new TestGenerationContext(); + + private final AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + + @AfterEach + void teardown() { + this.applicationContext.close(); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithoutValue() { + this.applicationContext.register(ImportWithoutValue.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ImportWithoutValue.container); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithoutValueWithDynamicPropertySource() { + 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 importTestcontainersCustomPostgreSQLContainerDefinitions() { + this.applicationContext.register(CustomPostgresqlContainerDefinitions.class); + compile((freshContext, compiled) -> { + CustomPostgreSQLContainer container = freshContext.getBean(CustomPostgreSQLContainer.class); + assertThat(container).isSameAs(CustomPostgresqlContainerDefinitions.container); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersImportWithoutValueNotAccessibleContainerAndDynamicPropertySource() { + 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 importTestcontainersWithNotAccessibleContainerAndDynamicPropertySource() { + 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 importTestcontainersMultipleContainersAndDynamicPropertySources() { + 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 { + + @ContainerAnnotation + static PostgreSQLContainer container = TestImage.container(PostgreSQLContainer.class); + + } + + @ImportTestcontainers(ContainerDefinitions.class) + static class ImportWithValue { + + } + + static class ContainerDefinitions { + + @ContainerAnnotation + PostgreSQLContainer container = TestImage.container(PostgreSQLContainer.class); + + } + + 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 ImportWithoutValueWithDynamicPropertySource { + + static PostgreSQLContainer container = TestImage.container(PostgreSQLContainer.class); + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("container.port", container::getFirstMappedPort); + } + + } + + @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..2ec5ec774d2b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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,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,15 @@ void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistr ReflectionUtils.makeAccessible(method); ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry); }); + String beanName = "%s.%s".formatted(DynamicPropertySourceMetadata.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 +101,132 @@ 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 AotContribution(metadata); + } + + private static final class AotContribution implements BeanFactoryInitializationAotContribution { + + private final Map metadata; + + private AotContribution(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.addStatement("$T clazz = $T.resolveClassName($S, $T.class.getClassLoader())", + Class.class, ClassUtils.class, method.getDeclaringClass().getTypeName(), + 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); + } + }); + + } + + 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..e5aac6a3c7e7 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-2025 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,27 @@ 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.AnnotationSpec; +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 +54,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 +70,89 @@ 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 inaccessible, 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()) + .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "rawtypes") + .build()) + .returns(this.registeredBean.getBeanClass()) + .addStatement("$T clazz = $T.resolveClassName($S, $T.class.getClassLoader())", Class.class, + ClassUtils.class, this.field.getDeclaringClass().getTypeName(), + 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())) + .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 751dc7c67981..f77d7a1b53ea 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -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; @@ -157,4 +159,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