Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@ImportTestcontainers doesn't work with AOT #42891

Open
wants to merge 2 commits into
base: 3.2.x
Choose a base branch
from

Conversation

nosan
Copy link
Contributor

@nosan nosan commented Oct 25, 2024

#42875

Since Bean's InstanceSupplier cannot be used during the AOT process, I added BeanRegistrationAotProcessor to replace it.

If a container's field is inaccessible, the generated code will be:

@Generated
public class GraalmavenApplicationTests__TestContext001_BeanDefinitions {

	/**
	 * Get the bean instance for 'importTestContainer.task.graalmaven.GraalmavenApplicationTests.mongoDbContainer'.
	 */
	private static MongoDBContainer getMongoDbContainerInstance() {
		try {
			Class<?> clazz = ClassUtils.forName("task.graalmaven.GraalmavenApplicationTests",
					GraalmavenApplicationTests__TestContext001_BeanDefinitions.class.getClassLoader());
			Field field = ReflectionUtils.findField(clazz, "mongoDbContainer");
			Assert.notNull(field, "Field 'mongoDbContainer' is not found");
			ReflectionUtils.makeAccessible(field);
			Object container = ReflectionUtils.getField(field, null);
			Assert.notNull(container, "Container field 'mongoDbContainer' must not have a null value");
			return (MongoDBContainer) container;
		} catch (ClassNotFoundException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * Get the bean definition for 'mongoDbContainer'.
	 */
	public static BeanDefinition getMongoDbContainerBeanDefinition() {
		RootBeanDefinition beanDefinition = new RootBeanDefinition(MongoDBContainer.class);
		beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
		beanDefinition.setInstanceSupplier(
				GraalmavenApplicationTests__TestContext001_BeanDefinitions::getMongoDbContainerInstance);
		return beanDefinition;
	}

}

If a container's field is accessible, the generated code will be:

@Generated
public class GraalmavenApplicationTests__TestContext001_BeanDefinitions {
  /**
   * Get the bean definition for 'mongoDbContainer'.
   */
  public static BeanDefinition getMongoDbContainerBeanDefinition() {
    RootBeanDefinition beanDefinition = new RootBeanDefinition(MyContainer.class);
    beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    beanDefinition.setInstanceSupplier(() -> GraalmavenApplicationTests.mongoDbContainer);
    return beanDefinition;
  }
}

An alternative way is FactoryBean instead of InstanceSupplier.

The target branch is 3.2.x

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 25, 2024
@nosan nosan force-pushed the 42875 branch 3 times, most recently from 86d62e3 to 2b1535f Compare October 26, 2024 12:32
@nosan
Copy link
Contributor Author

nosan commented Oct 26, 2024

Another idea that came to my mind is to replace ImportTestcontainersRegistrar with ContextCustomizerFactory.
In that case only BeanRegistrationExcludeFilter is needed to exclude TestcontainerFieldBeanDefinition.

Similar to ServiceConnectionContextCustomizerFactory

@nosan
Copy link
Contributor Author

nosan commented Oct 27, 2024

Unfortunately, handling the bean instance supplier for container fields only addresses
half of the issue. The more problematic part involves handling @DynamicPropertySource
methods. These @DynamicPropertySource methods need to be generated and invoked within
the AOT environment. To achieve this, I introduced
DynamicPropertySourceBeanFactoryInitializationAotProcessor

@nosan
Copy link
Contributor Author

nosan commented Oct 28, 2024

Initially, my idea was to generate code similar to ImportTestcontainersRegistrar. However,
I realized that instead of generating all bean definitions and @DynamicPropertySource methods, I could create a class delegate that delegates bean definition registration to the existing ImportTestcontainersRegistrar.

public class TestA__TestContext001_ApplicationContextInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
  @Override
  public void initialize(GenericApplicationContext applicationContext) {
    DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
    beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
    beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
    new TestA__TestContext001_BeanFactoryRegistrations().registerBeanDefinitions(beanFactory);
    new TestA__TestContext001_BeanFactoryRegistrations().registerAliases(beanFactory);
    ImportTestcontainersRegistrar__TestContext001_ImportTestcontainers.registerBeanDefinitions(applicationContext.getEnvironment(), beanFactory);
  }
}

@Generated
public class ImportTestcontainersRegistrar__TestContext001_ImportTestcontainers {
  /**
   * Register bean definitions for 'ImportTestcontainers'
   */
  public static void registerBeanDefinitions(ConfigurableEnvironment environment,
      DefaultListableBeanFactory beanFactory) {
    Set<Class<?>> definitionClasses = new LinkedHashSet<>();
    definitionClasses.add(ClassUtils.resolveClassName("task.graalmaven.TestcontainersConfiguration", beanFactory.getBeanClassLoader()));
    definitionClasses.add(ClassUtils.resolveClassName("task.graalmaven.GraalmavenApplicationTests", beanFactory.getBeanClassLoader()));
    new ImportTestcontainersRegistrar(environment).registerBeanDefinitions(beanFactory, definitionClasses.toArray(new Class<?>[0]));
  }
}

If it is wrong, the previous af6ce70 commit generates bean definitions and @DynamicPropertySource methods

@Generated
public class TestA__TestContext001_ApplicationContextInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
  @Override
  public void initialize(GenericApplicationContext applicationContext) {
    DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
    beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
    beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
    new TestA__TestContext001_BeanFactoryRegistrations().registerBeanDefinitions(beanFactory);
    new TestA__TestContext001_BeanFactoryRegistrations().registerAliases(beanFactory);
    registerDynamicPropertySources(applicationContext.getEnvironment(), beanFactory);
  }

  /**
   * Registers {@code @DynamicPropertySource} properties
   */
  private static void registerDynamicPropertySources(ConfigurableEnvironment environment,
      DefaultListableBeanFactory beanFactory) {
    DynamicPropertyRegistry dynamicPropertyRegistry = TestcontainersPropertySource.attach(environment, beanFactory);
    GraalmavenApplicationTests__TestContext001_DynamicPropertySource.registerDynamicPropertySource(dynamicPropertyRegistry);
  }
}

@Generated
public class GraalmavenApplicationTests__TestContext001_DynamicPropertySource {
  /**
   * Register {@code @DynamicPropertySource} for method 'GraalmavenApplicationTests.mongoProperties'
   */
  private static void mongoProperties(DynamicPropertyRegistry dynamicPropertyRegistry) {
    Class<?> clazz = ClassUtils.resolveClassName("task.graalmaven.GraalmavenApplicationTests", GraalmavenApplicationTests__TestContext001_DynamicPropertySource.class.getClassLoader());
    ReflectionTestUtils.invokeMethod(clazz, "mongoProperties", dynamicPropertyRegistry);
  }

  /**
   * Registers {@code @DynamicPropertySource} properties for class 'GraalmavenApplicationTests'
   */
  public static void registerDynamicPropertySource(
      DynamicPropertyRegistry dynamicPropertyRegistry) {
    GraalmavenApplicationTests__TestContext001_DynamicPropertySource.mongoProperties(dynamicPropertyRegistry);
  }
}

@Generated
public class GraalmavenApplicationTests__TestContext001_BeanDefinitions {

	/**
	 * Get the bean instance for 'importTestContainer.task.graalmaven.GraalmavenApplicationTests.mongoDbContainer'.
	 */
	private static MongoDBContainer getMongoDbContainerInstance() {
		Class<?> clazz = ClassUtils.resolveClassName("task.graalmaven.GraalmavenApplicationTests",
				GraalmavenApplicationTests__TestContext001_BeanDefinitions.class.getClassLoader());
		Field field = ReflectionUtils.findField(clazz, "mongoDbContainer");
		Assert.notNull(field, "Field 'mongoDbContainer' is not found");
		ReflectionUtils.makeAccessible(field);
		Object container = ReflectionUtils.getField(field, null);
		Assert.notNull(container, "Container field 'mongoDbContainer' must not have a null value");
		return (MongoDBContainer) container;
	}

	/**
	 * Get the bean definition for 'mongoDbContainer'.
	 */
	public static BeanDefinition getMongoDbContainerBeanDefinition() {
		RootBeanDefinition beanDefinition = new RootBeanDefinition(MongoDBContainer.class);
		beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
		beanDefinition.setInstanceSupplier(
				GraalmavenApplicationTests__TestContext001_BeanDefinitions::getMongoDbContainerInstance);
		return beanDefinition;
	}

}

@nosan nosan marked this pull request as ready for review October 28, 2024 16:46
@nosan
Copy link
Contributor Author

nosan commented Oct 28, 2024

@philwebb
Could I kindly ask you to do a quick review of this PR, and let me know if I'm in the right direction?

@nosan nosan force-pushed the 42875 branch 3 times, most recently from aeb137a to 7c38b50 Compare October 29, 2024 11:33
…lier 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
…hat collects all importing classes and then generates an initializer method that invokes ImportTestcontainersRegistrar.registerBeanDefinitions(...) for those classes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants