Support [package-]private init/destroy methods in AOT mode

Prior to this commit, private (and non-visible package-private)
init/destroy methods were not supported in AOT mode. The reason is that
such methods are tracked using their fully-qualified method names, and
the AOT support for init/destroy methods previously did not take
fully-qualified method names into account. In addition, the invocation
order of init/destroy methods differed vastly between standard JVM mode
and AOT mode.

This commit addresses these issues in the following ways.

- AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(),
  DisposableBeanAdapter.determineDestroyMethod(), and
  BeanDefinitionPropertiesCodeGenerator.addInitDestroyHint() now parse
  fully-qualified method names to locate the correct init/destroy
  methods.

- AbstractAutowireCapableBeanFactory and DisposableBeanAdapter delegate
  to a new MethodDescriptor record which encapsulates the parsing of
  fully-qualified method names; however,
  BeanDefinitionPropertiesCodeGenerator duplicates this logic since it
  resides in a different package, and we do not currently want to make
  MethodDescriptor public.

- Init/destroy methods detected via annotations (such as @PostConstruct
  and @PreDestroy) are now invoked prior to init/destroy methods that
  are explicitly configured by name or convention. This aligns with the
  invocation order in standard JVM mode; however,
  InitializingBean#afterPropertiesSet() and DisposableBean#destroy()
  are still invoked before annotated init/destroy methods in AOT mode
  which differs from standard JVM mode.

- Unit and integration tests have been updated to test the revised
  behavior.

Closes gh-30692
This commit is contained in:
Sam Brannen
2023-06-20 17:47:01 +02:00
parent f86a69ebfb
commit 3181dca5ef
9 changed files with 252 additions and 28 deletions

View File

@@ -18,18 +18,26 @@ package org.springframework.context.annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.lifecyclemethods.InitDestroyBean;
import org.springframework.context.annotation.lifecyclemethods.PackagePrivateInitDestroyBean;
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 static org.assertj.core.api.Assertions.assertThat;
@@ -156,6 +164,79 @@ class InitDestroyMethodLifecycleTests {
);
}
/**
* @see org.springframework.context.aot.ApplicationContextAotGeneratorTests#processAheadOfTimeWhenHasMultipleInitDestroyMethods
*/
@Test
@CompileWithForkedClassLoader
void jakartaAnnotationsWithCustomSameMethodNamesWithAotProcessingAndAotRuntime() {
Class<CustomAnnotatedPrivateSameNameInitDestroyBean> beanClass = CustomAnnotatedPrivateSameNameInitDestroyBean.class;
GenericApplicationContext applicationContext = new GenericApplicationContext();
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setInitMethodName("customInit");
beanDefinition.setDestroyMethodName("customDestroy");
beanFactory.registerBeanDefinition("lifecycleTestBean", beanDefinition);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext aotApplicationContext = createApplicationContext(initializer);
CustomAnnotatedPrivateSameNameInitDestroyBean bean = aotApplicationContext.getBean("lifecycleTestBean", beanClass);
assertThat(bean.initMethods).as("init-methods").containsExactly(
"afterPropertiesSet",
"@PostConstruct.privateCustomInit1",
"@PostConstruct.sameNameCustomInit1",
"customInit"
);
aotApplicationContext.close();
assertThat(bean.destroyMethods).as("destroy-methods").containsExactly(
"destroy",
"@PreDestroy.sameNameCustomDestroy1",
"@PreDestroy.privateCustomDestroy1",
"customDestroy"
);
});
}
@Test
@CompileWithForkedClassLoader
void jakartaAnnotationsWithPackagePrivateInitDestroyMethodsWithAotProcessingAndAotRuntime() {
Class<SubPackagePrivateInitDestroyBean> beanClass = SubPackagePrivateInitDestroyBean.class;
GenericApplicationContext applicationContext = new GenericApplicationContext();
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setInitMethodName("initMethod");
beanDefinition.setDestroyMethodName("destroyMethod");
beanFactory.registerBeanDefinition("lifecycleTestBean", beanDefinition);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext aotApplicationContext = createApplicationContext(initializer);
SubPackagePrivateInitDestroyBean bean = aotApplicationContext.getBean("lifecycleTestBean", beanClass);
assertThat(bean.initMethods).as("init-methods").containsExactly(
"InitializingBean.afterPropertiesSet",
"PackagePrivateInitDestroyBean.postConstruct",
"SubPackagePrivateInitDestroyBean.postConstruct",
"initMethod"
);
aotApplicationContext.close();
assertThat(bean.destroyMethods).as("destroy-methods").containsExactly(
"DisposableBean.destroy",
"SubPackagePrivateInitDestroyBean.preDestroy",
"PackagePrivateInitDestroyBean.preDestroy",
"destroyMethod"
);
});
}
@Test
void allLifecycleMechanismsAtOnce() {
Class<?> beanClass = AllInOneBean.class;
@@ -188,6 +269,31 @@ class InitDestroyMethodLifecycleTests {
return beanFactory;
}
private static GenericApplicationContext createApplicationContext(
ApplicationContextInitializer<GenericApplicationContext> initializer) {
GenericApplicationContext context = new GenericApplicationContext();
initializer.initialize(context);
context.refresh();
return context;
}
@SuppressWarnings("unchecked")
private static void testCompiledResult(GenericApplicationContext applicationContext,
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
TestCompiler.forSystem().with(processAheadOfTime(applicationContext)).compile(compiled ->
result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
}
private static TestGenerationContext processAheadOfTime(GenericApplicationContext applicationContext) {
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
TestGenerationContext generationContext = new TestGenerationContext();
generator.processAheadOfTime(applicationContext, generationContext);
generationContext.writeGeneratedContent();
return generationContext;
}
static class InitializingDisposableWithShadowedMethodsBean extends InitDestroyBean implements
InitializingBean, DisposableBean {

View File

@@ -257,9 +257,9 @@ class ApplicationContextAotGeneratorTests {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent");
InitDestroyComponent bean = freshApplicationContext.getBean(InitDestroyComponent.class);
assertThat(bean.events).containsExactly("customInit", "init");
assertThat(bean.events).containsExactly("init", "customInit");
freshApplicationContext.close();
assertThat(bean.events).containsExactly("customInit", "init", "customDestroy", "destroy");
assertThat(bean.events).containsExactly("init", "customInit", "destroy", "customDestroy");
});
}