From 672555a568445d27ab3d1efc0d87dd9bde779acc Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Mar 2022 11:20:16 +0100 Subject: [PATCH] Add support of init and destroy methods This commit updates InitDestroyBeanPostProcessor so that it contributes init or destroy method names to the `RootBeanDefinition`. This is then used by the generator to provide these methods to the optimized AOT context. Invocation of those init methods still happen using reflection so dedicated hints are contributed for them. Closes gh-28151 --- ...nitDestroyAnnotationBeanPostProcessor.java | 37 ++++++- ...anRegistrationBeanFactoryContribution.java | 46 ++++++++ ...stroyAnnotationBeanPostProcessorTests.java | 101 ++++++++++++++++++ ...istrationBeanFactoryContributionTests.java | 84 ++++++++++++--- .../factory/generator/lifecycle/Destroy.java | 30 ++++++ .../factory/generator/lifecycle/Init.java | 30 ++++++ .../generator/lifecycle/InitDestroyBean.java | 35 ++++++ .../lifecycle/MultiInitDestroyBean.java | 29 +++++ .../ApplicationContextAotGeneratorTests.java | 37 +++++++ .../annotation/InitDestroyComponent.java | 47 ++++++++ 10 files changed, 458 insertions(+), 18 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/generator/annotation/InitDestroyComponent.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index e615931bcc9..c87b6674f7f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -39,6 +40,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.generator.AotContributingBeanPostProcessor; +import org.springframework.beans.factory.generator.BeanInstantiationContribution; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.Ordered; @@ -46,6 +49,7 @@ import org.springframework.core.PriorityOrdered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -72,13 +76,14 @@ import org.springframework.util.ReflectionUtils; * for annotation-driven injection of named beans. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 * @see #setInitAnnotationType * @see #setDestroyAnnotationType */ @SuppressWarnings("serial") -public class InitDestroyAnnotationBeanPostProcessor - implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, Serializable { +public class InitDestroyAnnotationBeanPostProcessor implements DestructionAwareBeanPostProcessor, + MergedBeanDefinitionPostProcessor, AotContributingBeanPostProcessor, PriorityOrdered, Serializable { private final transient LifecycleMetadata emptyLifecycleMetadata = new LifecycleMetadata(Object.class, Collections.emptyList(), Collections.emptyList()) { @@ -146,8 +151,36 @@ public class InitDestroyAnnotationBeanPostProcessor @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + findInjectionMetadata(beanDefinition, beanType); + } + + @Override + public BeanInstantiationContribution contribute(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + LifecycleMetadata metadata = findInjectionMetadata(beanDefinition, beanType); + if (!CollectionUtils.isEmpty(metadata.initMethods)) { + String[] initMethodNames = safeMerge( + beanDefinition.getInitMethodNames(), metadata.initMethods); + beanDefinition.setInitMethodNames(initMethodNames); + } + if (!CollectionUtils.isEmpty(metadata.destroyMethods)) { + String[] destroyMethodNames = safeMerge( + beanDefinition.getDestroyMethodNames(), metadata.destroyMethods); + beanDefinition.setDestroyMethodNames(destroyMethodNames); + } + return null; + } + + private LifecycleMetadata findInjectionMetadata(RootBeanDefinition beanDefinition, Class beanType) { LifecycleMetadata metadata = findLifecycleMetadata(beanType); metadata.checkConfigMembers(beanDefinition); + return metadata; + } + + private String[] safeMerge(@Nullable String[] existingNames, Collection detectedElements) { + Stream detectedNames = detectedElements.stream().map(LifecycleElement::getIdentifier); + Stream mergedNames = (existingNames != null + ? Stream.concat(Stream.of(existingNames), detectedNames) : detectedNames); + return mergedNames.distinct().toArray(String[]::new); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContribution.java index fe47a2521c1..7caf9214289 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContribution.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContribution.java @@ -55,6 +55,7 @@ import org.springframework.javapoet.support.MultiStatement; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -121,6 +122,14 @@ public class BeanRegistrationBeanFactoryContribution implements BeanFactoryContr * @param runtimeHints the runtime hints to use */ void registerRuntimeHints(RuntimeHints runtimeHints) { + String[] initMethodNames = this.beanDefinition.getInitMethodNames(); + if (!ObjectUtils.isEmpty(initMethodNames)) { + registerInitDestroyMethodsRuntimeHints(initMethodNames, runtimeHints); + } + String[] destroyMethodNames = this.beanDefinition.getDestroyMethodNames(); + if (!ObjectUtils.isEmpty(destroyMethodNames)) { + registerInitDestroyMethodsRuntimeHints(destroyMethodNames, runtimeHints); + } registerPropertyValuesRuntimeHints(runtimeHints); } @@ -191,6 +200,15 @@ public class BeanRegistrationBeanFactoryContribution implements BeanFactoryContr return this.beanInstantiationGenerator.generateBeanInstantiation(runtimeHints); } + private void registerInitDestroyMethodsRuntimeHints(String[] methodNames, RuntimeHints runtimeHints) { + for (String methodName : methodNames) { + Method method = ReflectionUtils.findMethod(getUserBeanClass(), methodName); + if (method != null) { + runtimeHints.reflection().registerMethod(method, hint -> hint.withMode(ExecutableMode.INVOKE)); + } + } + } + private void registerPropertyValuesRuntimeHints(RuntimeHints runtimeHints) { if (!this.beanDefinition.hasPropertyValues()) { return; @@ -357,6 +375,14 @@ public class BeanRegistrationBeanFactoryContribution implements BeanFactoryContr private void handleBeanDefinitionMetadata(Builder code) { String bdVariable = determineVariableName("bd"); MultiStatement statements = new MultiStatement(); + String[] initMethodNames = this.beanDefinition.getInitMethodNames(); + if (!ObjectUtils.isEmpty(initMethodNames)) { + handleInitMethodNames(statements, bdVariable, initMethodNames); + } + String[] destroyMethodNames = this.beanDefinition.getDestroyMethodNames(); + if (!ObjectUtils.isEmpty(destroyMethodNames)) { + handleDestroyMethodNames(statements, bdVariable, destroyMethodNames); + } if (this.beanDefinition.isPrimary()) { statements.addStatement("$L.setPrimary(true)", bdVariable); } @@ -399,6 +425,26 @@ public class BeanRegistrationBeanFactoryContribution implements BeanFactoryContr code.add(")"); } + private void handleInitMethodNames(MultiStatement statements, String bdVariable, String[] initMethodNames) { + if (initMethodNames.length == 1) { + statements.addStatement("$L.setInitMethodName($S)", bdVariable, initMethodNames[0]); + } + else { + statements.addStatement("$L.setInitMethodNames($L)", bdVariable, + this.parameterGenerator.generateParameterValue(initMethodNames)); + } + } + + private void handleDestroyMethodNames(MultiStatement statements, String bdVariable, String[] destroyMethodNames) { + if (destroyMethodNames.length == 1) { + statements.addStatement("$L.setDestroyMethodName($S)", bdVariable, destroyMethodNames[0]); + } + else { + statements.addStatement("$L.setDestroyMethodNames($L)", bdVariable, + this.parameterGenerator.generateParameterValue(destroyMethodNames)); + } + } + private void handleArgumentValues(MultiStatement statements, String bdVariable, Map indexedArgumentValues) { if (indexedArgumentValues.size() == 1) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java new file mode 100644 index 00000000000..e8ac4323f26 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2022 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.beans.factory.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.generator.BeanInstantiationContribution; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.Destroy; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.Init; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.InitDestroyBean; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.MultiInitDestroyBean; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link InitDestroyAnnotationBeanPostProcessor}. + * + * @author Stephane Nicoll + */ +class InitDestroyAnnotationBeanPostProcessorTests { + + @Test + void contributeWithNoCallbackDoesNotMutateRootBeanDefinition() { + RootBeanDefinition beanDefinition = mock(RootBeanDefinition.class); + assertThat(createAotContributingBeanPostProcessor().contribute( + beanDefinition, String.class, "test")).isNull(); + verifyNoInteractions(beanDefinition); + } + + @Test + void contributeWithInitDestroyCallback() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyBean.class); + assertThat(createContribution(beanDefinition)).isNull(); + assertThat(beanDefinition.getInitMethodNames()).containsExactly("initMethod"); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("destroyMethod"); + } + + @Test + void contributeWithInitDestroyCallbackRetainCustomMethods() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyBean.class); + beanDefinition.setInitMethodName("customInitMethod"); + beanDefinition.setDestroyMethodNames("customDestroyMethod"); + assertThat(createContribution(beanDefinition)).isNull(); + assertThat(beanDefinition.getInitMethodNames()) + .containsExactly("customInitMethod", "initMethod"); + assertThat(beanDefinition.getDestroyMethodNames()) + .containsExactly("customDestroyMethod", "destroyMethod"); + } + + @Test + void contributeWithInitDestroyCallbackFilterDuplicates() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyBean.class); + beanDefinition.setInitMethodName("initMethod"); + beanDefinition.setDestroyMethodNames("destroyMethod"); + assertThat(createContribution(beanDefinition)).isNull(); + assertThat(beanDefinition.getInitMethodNames()).containsExactly("initMethod"); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("destroyMethod"); + } + + @Test + void contributeWithMultipleInitDestroyCallbacks() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(MultiInitDestroyBean.class); + assertThat(createContribution(beanDefinition)).isNull(); + assertThat(beanDefinition.getInitMethodNames()) + .containsExactly("initMethod", "anotherInitMethod"); + assertThat(beanDefinition.getDestroyMethodNames()) + .containsExactly("anotherDestroyMethod", "destroyMethod"); + } + + @Nullable + private BeanInstantiationContribution createContribution(RootBeanDefinition beanDefinition) { + InitDestroyAnnotationBeanPostProcessor bpp = createAotContributingBeanPostProcessor(); + return bpp.contribute(beanDefinition, beanDefinition.getResolvableType().toClass(), "test"); + } + + private InitDestroyAnnotationBeanPostProcessor createAotContributingBeanPostProcessor() { + InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); + bpp.setInitAnnotationType(Init.class); + bpp.setDestroyAnnotationType(Destroy.class); + return bpp; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContributionTests.java index 689d7360a71..e3451f5ebda 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanRegistrationBeanFactoryContributionTests.java @@ -56,6 +56,7 @@ import org.springframework.beans.testfixture.beans.factory.generator.InnerCompon import org.springframework.beans.testfixture.beans.factory.generator.SimpleConfiguration; import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory; import org.springframework.beans.testfixture.beans.factory.generator.injection.InjectionComponent; +import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.InitDestroyBean; import org.springframework.beans.testfixture.beans.factory.generator.property.ConfigurableBean; import org.springframework.beans.testfixture.beans.factory.generator.visibility.ProtectedConstructorComponent; import org.springframework.beans.testfixture.beans.factory.generator.visibility.ProtectedFactoryMethod; @@ -208,6 +209,30 @@ class BeanRegistrationBeanFactoryContributionTests { PublicFactoryBean.class.getPackageName() + ".Test.registerTest(beanFactory);\n"); } + @Test + void generateWithBeanDefinitionHavingInitMethodName() { + compile(simpleConfigurationRegistration(bd -> bd.setInitMethodName("someMethod")), + hasBeanDefinition(generatedBd -> assertThat(generatedBd.getInitMethodNames()).containsExactly("someMethod"))); + } + + @Test + void generateWithBeanDefinitionHavingInitMethodNames() { + compile(simpleConfigurationRegistration(bd -> bd.setInitMethodNames("i1", "i2")), + hasBeanDefinition(generatedBd -> assertThat(generatedBd.getInitMethodNames()).containsExactly("i1", "i2"))); + } + + @Test + void generateWithBeanDefinitionHavingDestroyMethodName() { + compile(simpleConfigurationRegistration(bd -> bd.setDestroyMethodName("someMethod")), + hasBeanDefinition(generatedBd -> assertThat(generatedBd.getDestroyMethodNames()).containsExactly("someMethod"))); + } + + @Test + void generateWithBeanDefinitionHavingDestroyMethodNames() { + compile(simpleConfigurationRegistration(bd -> bd.setDestroyMethodNames("d1", "d2")), + hasBeanDefinition(generatedBd -> assertThat(generatedBd.getDestroyMethodNames()).containsExactly("d1", "d2"))); + } + @Test void generateWithBeanDefinitionHavingSyntheticFlag() { compile(simpleConfigurationRegistration(bd -> bd.setSynthetic(true)), @@ -392,6 +417,28 @@ class BeanRegistrationBeanFactoryContributionTests { })); } + @Test + void registerRuntimeHintsWithInitMethodNames() { + RootBeanDefinition bd = new RootBeanDefinition(InitDestroyBean.class); + bd.setInitMethodNames("customInitMethod", "initMethod"); + RuntimeHints runtimeHints = new RuntimeHints(); + getDefaultContribution(new DefaultListableBeanFactory(), bd).registerRuntimeHints(runtimeHints); + assertThat(runtimeHints.reflection().getTypeHint(InitDestroyBean.class)).satisfies(hint -> + assertThat(hint.methods()).anySatisfy(invokeMethodHint("customInitMethod")) + .anySatisfy(invokeMethodHint("initMethod")).hasSize(2)); + } + + @Test + void registerRuntimeHintsWithDestroyMethodNames() { + RootBeanDefinition bd = new RootBeanDefinition(InitDestroyBean.class); + bd.setDestroyMethodNames("customDestroyMethod", "destroyMethod"); + RuntimeHints runtimeHints = new RuntimeHints(); + getDefaultContribution(new DefaultListableBeanFactory(), bd).registerRuntimeHints(runtimeHints); + assertThat(runtimeHints.reflection().getTypeHint(InitDestroyBean.class)).satisfies(hint -> + assertThat(hint.methods()).anySatisfy(invokeMethodHint("customDestroyMethod")) + .anySatisfy(invokeMethodHint("destroyMethod")).hasSize(2)); + } + @Test void registerRuntimeHintsWithNoPropertyValuesDoesNotAccessRuntimeHints() { RootBeanDefinition bd = new RootBeanDefinition(String.class); @@ -432,12 +479,12 @@ class BeanRegistrationBeanFactoryContributionTests { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(BaseFactoryBean.class)); assertThat(typeHint.constructors()).isEmpty(); assertThat(typeHint.methods()).singleElement() - .satisfies(methodHint("setName", String.class)); + .satisfies(invokeMethodHint("setName", String.class)); assertThat(typeHint.fields()).isEmpty(); }).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IntegerFactoryBean.class)); assertThat(typeHint.constructors()).singleElement() - .satisfies(constructorHint(Environment.class)); + .satisfies(introspectConstructorHint(Environment.class)); assertThat(typeHint.methods()).isEmpty(); assertThat(typeHint.fields()).isEmpty(); }).hasSize(2); @@ -453,8 +500,8 @@ class BeanRegistrationBeanFactoryContributionTests { assertThat(reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(NameAndCountersComponent.class)); assertThat(typeHint.constructors()).isEmpty(); - assertThat(typeHint.methods()).anySatisfy(methodHint("setName", String.class)) - .anySatisfy(methodHint("setCounter", Integer.class)).hasSize(2); + assertThat(typeHint.methods()).anySatisfy(invokeMethodHint("setName", String.class)) + .anySatisfy(invokeMethodHint("setCounter", Integer.class)).hasSize(2); assertThat(typeHint.fields()).isEmpty(); }); } @@ -473,14 +520,14 @@ class BeanRegistrationBeanFactoryContributionTests { assertThat(reflectionHints.typeHints()).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(NameAndCountersComponent.class)); assertThat(typeHint.constructors()).isEmpty(); - assertThat(typeHint.methods()).singleElement().satisfies(methodHint("setCounter", Integer.class)); + assertThat(typeHint.methods()).singleElement().satisfies(invokeMethodHint("setCounter", Integer.class)); assertThat(typeHint.fields()).isEmpty(); }).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(BaseFactoryBean.class)); - assertThat(typeHint.methods()).singleElement().satisfies(methodHint("setName", String.class)); + assertThat(typeHint.methods()).singleElement().satisfies(invokeMethodHint("setName", String.class)); }).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IntegerFactoryBean.class)); - assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint(Environment.class)); + assertThat(typeHint.constructors()).singleElement().satisfies(introspectConstructorHint(Environment.class)); }).hasSize(3); } @@ -499,32 +546,37 @@ class BeanRegistrationBeanFactoryContributionTests { assertThat(reflectionHints.typeHints()).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(NameAndCountersComponent.class)); assertThat(typeHint.constructors()).isEmpty(); - assertThat(typeHint.methods()).singleElement().satisfies(methodHint("setCounters", List.class)); + assertThat(typeHint.methods()).singleElement().satisfies(invokeMethodHint("setCounters", List.class)); assertThat(typeHint.fields()).isEmpty(); }).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(BaseFactoryBean.class)); - assertThat(typeHint.methods()).singleElement().satisfies(methodHint("setName", String.class)); + assertThat(typeHint.methods()).singleElement().satisfies(invokeMethodHint("setName", String.class)); }).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IntegerFactoryBean.class)); - assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint(Environment.class)); + assertThat(typeHint.constructors()).singleElement().satisfies(introspectConstructorHint(Environment.class)); }).anySatisfy(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(AnotherIntegerFactoryBean.class)); - assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint(Environment.class)); + assertThat(typeHint.constructors()).singleElement().satisfies(introspectConstructorHint(Environment.class)); }).hasSize(4); } - private Consumer methodHint(String name, Class... parameterTypes) { + private Consumer invokeMethodHint(String name, Class... parameterTypes) { + return executableHint(ExecutableMode.INVOKE, name, parameterTypes); + } + + private Consumer introspectConstructorHint(Class... parameterTypes) { + return executableHint(ExecutableMode.INTROSPECT, "", parameterTypes); + } + + private Consumer executableHint(ExecutableMode mode, String name, Class... parameterTypes) { return executableHint -> { assertThat(executableHint.getName()).isEqualTo(name); assertThat(executableHint.getParameterTypes()).containsExactly(Arrays.stream(parameterTypes) .map(TypeReference::of).toArray(TypeReference[]::new)); + assertThat(executableHint.getModes()).containsExactly(mode); }; } - private Consumer constructorHint(Class... parameterTypes) { - return methodHint("", parameterTypes); - } - private Consumer hasBeanDefinition(Consumer bd) { return beanFactory -> { assertThat(beanFactory.getBeanDefinitionNames()).contains("test"); diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java new file mode 100644 index 00000000000..89db8539e28 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Destroy.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2022 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.beans.testfixture.beans.factory.generator.lifecycle; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Destroy { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java new file mode 100644 index 00000000000..8c56616b747 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/Init.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2022 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.beans.testfixture.beans.factory.generator.lifecycle; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Init { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java new file mode 100644 index 00000000000..838b16783e4 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/InitDestroyBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2022 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.beans.testfixture.beans.factory.generator.lifecycle; + +public class InitDestroyBean { + + @Init + public void initMethod() { + } + + public void customInitMethod() { + } + + @Destroy + public void destroyMethod() { + } + + public void customDestroyMethod() { + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java new file mode 100644 index 00000000000..47cbde92628 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/lifecycle/MultiInitDestroyBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2022 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.beans.testfixture.beans.factory.generator.lifecycle; + +public class MultiInitDestroyBean extends InitDestroyBean { + + @Init + void anotherInitMethod() { + } + + @Destroy + void anotherDestroyMethod() { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorTests.java index 4935de29654..38574ef707c 100644 --- a/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorTests.java @@ -43,9 +43,11 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.context.testfixture.context.generator.annotation.AutowiredComponent; +import org.springframework.context.testfixture.context.generator.annotation.InitDestroyComponent; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.JavaFile; @@ -94,6 +96,41 @@ class ApplicationContextAotGeneratorTests { })); } + @Test + void generateApplicationContextWithInitDestroyMethods() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, + BeanDefinitionBuilder.rootBeanDefinition(CommonAnnotationBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); + context.registerBeanDefinition("initDestroyComponent", new RootBeanDefinition(InitDestroyComponent.class)); + compile(context, toFreshApplicationContext(GenericApplicationContext::new, aotContext -> { + assertThat(aotContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); + InitDestroyComponent bean = aotContext.getBean(InitDestroyComponent.class); + assertThat(bean.events).containsExactly("init"); + aotContext.close(); + assertThat(bean.events).containsExactly("init", "destroy"); + })); + } + + @Test + void generateApplicationContextWithMultipleInitDestroyMethods() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, + BeanDefinitionBuilder.rootBeanDefinition(CommonAnnotationBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); + RootBeanDefinition beanDefinition = new RootBeanDefinition(InitDestroyComponent.class); + beanDefinition.setInitMethodName("customInit"); + beanDefinition.setDestroyMethodName("customDestroy"); + context.registerBeanDefinition("initDestroyComponent", beanDefinition); + compile(context, toFreshApplicationContext(GenericApplicationContext::new, aotContext -> { + assertThat(aotContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); + InitDestroyComponent bean = aotContext.getBean(InitDestroyComponent.class); + assertThat(bean.events).containsExactly("customInit", "init"); + aotContext.close(); + assertThat(bean.events).containsExactly("customInit", "init", "customDestroy", "destroy"); + })); + } + @Test void generateApplicationContextWitNoContributors() { GeneratedTypeContext generationContext = createGenerationContext(); diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/generator/annotation/InitDestroyComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/generator/annotation/InitDestroyComponent.java new file mode 100644 index 00000000000..ac8e5b159aa --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/generator/annotation/InitDestroyComponent.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2022 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.context.testfixture.context.generator.annotation; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +public class InitDestroyComponent { + + public final List events = new ArrayList<>(); + + @PostConstruct + public void init() { + this.events.add("init"); + } + + public void customInit() { + this.events.add("customInit"); + } + + @PreDestroy + public void destroy() { + this.events.add("destroy"); + } + + public void customDestroy() { + this.events.add("customDestroy"); + } + +}