From 91e32733329544289b0aed1e587c97fa123ed096 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 | 120 +++++++++++++++ .../DynamicPropertySourceMethodsImporter.java | 145 ++++++++++++++++++ .../TestcontainerFieldBeanDefinition.java | 89 ++++++++++- .../TestcontainersPropertySource.java | 11 ++ .../resources/META-INF/spring/aot.factories | 11 +- 5 files changed, 373 insertions(+), 3 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..35fa9d29d3ce 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,27 @@ 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.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.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 +53,8 @@ @DisabledIfDockerUnavailable class ImportTestcontainersTests { + private final TestGenerationContext generationContext = new TestGenerationContext(); + private AnnotationConfigApplicationContext applicationContext; @AfterEach @@ -122,6 +134,87 @@ 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 importTestcontainersWithDynamicPropertySourceAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ContainerDefinitionsWithDynamicPropertySource.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container); + assertThat(freshContext.getEnvironment().getProperty("container.port", Integer.class)) + .isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort()); + }); + } + + @Test + @CompileWithForkedClassLoader + void importTestcontainersWithCustomPostgreSQLContainerAotContribution() { + 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 importTestcontainersWithNotAccessibleContainerAotContribution() { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(ImportNotAccessibleContainer.class); + compile((freshContext, compiled) -> { + PostgreSQLContainer container = freshContext.getBean(PostgreSQLContainer.class); + assertThat(container).isSameAs(ImportNotAccessibleContainer.container); + assertThat(freshContext.getEnvironment().getProperty("container.port", Integer.class)) + .isEqualTo(ImportNotAccessibleContainer.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 { @@ -196,4 +289,31 @@ void containerProperties() { } + @ImportTestcontainers + static class CustomPostgreSQLContainerDefinitions { + + static CustomPostgreSQLContainer container = new CustomPostgreSQLContainer(); + + } + + static class CustomPostgreSQLContainer extends PostgreSQLContainer { + + CustomPostgreSQLContainer() { + super("postgres:14"); + } + + } + + @ImportTestcontainers + static class ImportNotAccessibleContainer { + + private static final PostgreSQLContainer container = TestImage.container(PostgreSQLContainer.class); + + @DynamicPropertySource + private static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("container.port", container::getFirstMappedPort); + } + + } + } 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..32bc024799a5 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,13 +18,31 @@ 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.CodeBlock; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.util.Assert; @@ -56,6 +74,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 +99,121 @@ private void assertValid(Method method) { + "' must accept a single DynamicPropertyRegistry argument"); } + private record DynamicPropertySourceMetadata(Class definitionClass, Set methods) { + } + + static class DynamicPropertySourceMetadataBeanRegistrationExcludeFilter implements BeanRegistrationExcludeFilter { + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return registeredBean.getMergedBeanDefinition().hasAttribute(DynamicPropertySourceMetadata.class.getName()); + } + + } + + /** + * {@link BeanFactoryInitializationAotProcessor} that generates methods for each + * annotated {@link DynamicPropertySource} method. + */ + static class DynamicPropertySourceBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + Map metadata = beanFactory + .getBeansOfType(DynamicPropertySourceMetadata.class); + 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, + "dynamicPropertyRegistry"))); + }); + }); + 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 '$L'", + definitionClass.getName()); + code.addParameter(DynamicPropertyRegistry.class, "dynamicPropertyRegistry"); + code.addModifiers(javax.lang.model.element.Modifier.PUBLIC, + javax.lang.model.element.Modifier.STATIC); + metadata.methods().forEach((method) -> { + GeneratedMethod generateMethod = generateMethod(generationContext, generatedClass, + definitionClass, method); + code.addStatement(generateMethod.toMethodReference() + .toInvokeCodeBlock(ArgumentCodeGenerator.of(DynamicPropertyRegistry.class, + "dynamicPropertyRegistry"))); + }); + }); + } + + // 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, Class definitionClass, Method method) { + return generatedClass.getMethods().add(method.getName(), (code) -> { + code.addJavadoc("Register {@code @DynamicPropertySource} for method '$L.$L'", + method.getDeclaringClass().getName(), method.getName()); + code.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); + code.addParameter(DynamicPropertyRegistry.class, "dynamicPropertyRegistry"); + if (AccessControl.forMember(method).isAccessibleFrom(generatedClass.getName())) { + code.addStatement( + CodeBlock.of("$T.$L(dynamicPropertyRegistry)", definitionClass, method.getName())); + } + else { + generationContext.getRuntimeHints().reflection().registerMethod(method, ExecutableMode.INVOKE); + code.addStatement("$T method = $T.findMethod($T.class, $S, $T.class)", Method.class, + ReflectionUtils.class, definitionClass, method.getName(), + DynamicPropertyRegistry.class); + code.addStatement("$T.notNull(method, $S)", Assert.class, + "Method '" + method.getName() + "' is not found"); + code.addStatement("$T.makeAccessible(method)", ReflectionUtils.class); + code.addStatement("$T.invokeMethod(method, null, dynamicPropertyRegistry)", + ReflectionUtils.class); + } + }); + + } + + } + + } + } 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..526fc844ab9b 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.GeneratedMethod; +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.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,73 @@ 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 (AccessControl.forMember(this.field).isAccessibleFrom(beanRegistrationCode.getClassName())) { + return CodeBlock.of("() -> $T.$L", this.field.getDeclaringClass(), this.field.getName()); + } + generationContext.getRuntimeHints().reflection().registerField(this.field); + GeneratedMethod generatedMethod = 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()) + .addStatement("$T field = $T.findField($T.class, $S)", Field.class, ReflectionUtils.class, + this.field.getDeclaringClass(), 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())); + return generatedMethod.toMethodReference().toCodeBlock(); + } + + } + + } + } 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..6e412ad1128c 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,13 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } + 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