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 df1e11632ee0..f0aeaffd950a 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 @@ -182,7 +182,7 @@ private LifecycleMetadata findLifecycleMetadata(RootBeanDefinition beanDefinitio private static String[] safeMerge(@Nullable String[] existingNames, Collection detectedMethods) { Stream detectedNames = detectedMethods.stream().map(LifecycleMethod::getIdentifier); Stream mergedNames = (existingNames != null ? - Stream.concat(Stream.of(existingNames), detectedNames) : detectedNames); + Stream.concat(detectedNames, Stream.of(existingNames)) : detectedNames); return mergedNames.distinct().toArray(String[]::new); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java index 72dbf9ebd580..2f1fcbcef9d3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -72,6 +72,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Sam Brannen * @since 6.0 */ class BeanDefinitionPropertiesCodeGenerator { @@ -138,6 +139,7 @@ private void addInitDestroyMethods(Builder code, } private void addInitDestroyHint(Class beanUserClass, String methodName) { + // TODO Handle fully-qualified method names Method method = ReflectionUtils.findMethod(beanUserClass, methodName); if (method != null) { this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 0aa1f3fb4a9b..d8abe69b93bc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1840,18 +1840,32 @@ protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBea protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefinition mbd, String initMethodName) throws Throwable { + Class beanClass = bean.getClass(); + Class methodDeclaringClass = beanClass; + String methodName = initMethodName; + + // Parse fully-qualified method name if necessary. + int indexOfDot = initMethodName.lastIndexOf('.'); + if (indexOfDot > 0) { + String className = initMethodName.substring(0, indexOfDot); + methodName = initMethodName.substring(indexOfDot + 1); + if (!beanClass.getName().equals((className))) { + methodDeclaringClass = ClassUtils.forName(className, beanClass.getClassLoader()); + } + } + Method initMethod = (mbd.isNonPublicAccessAllowed() ? - BeanUtils.findMethod(bean.getClass(), initMethodName) : - ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); + BeanUtils.findMethod(methodDeclaringClass, methodName) : + ClassUtils.getMethodIfAvailable(beanClass, methodName)); if (initMethod == null) { if (mbd.isEnforceInitMethod()) { throw new BeanDefinitionValidationException("Could not find an init method named '" + - initMethodName + "' on bean with name '" + beanName + "'"); + methodName + "' on bean with name '" + beanName + "'"); } else { if (logger.isTraceEnabled()) { - logger.trace("No default init method named '" + initMethodName + + logger.trace("No default init method named '" + methodName + "' found on bean with name '" + beanName + "'"); } // Ignore non-existent default lifecycle methods. @@ -1860,9 +1874,9 @@ protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefi } if (logger.isTraceEnabled()) { - logger.trace("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'"); + logger.trace("Invoking init method '" + methodName + "' on bean with name '" + beanName + "'"); } - Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, bean.getClass()); + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, beanClass); try { ReflectionUtils.makeAccessible(methodToInvoke); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index 0d2bc2adeb22..4d503a07255f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -255,19 +255,32 @@ else if (this.destroyMethodNames != null) { private Method determineDestroyMethod(String name) { try { Class beanClass = this.bean.getClass(); - Method destroyMethod = findDestroyMethod(beanClass, name); + Class methodDeclaringClass = beanClass; + String methodName = name; + + // Parse fully-qualified method name if necessary. + int indexOfDot = name.lastIndexOf('.'); + if (indexOfDot > 0) { + String className = name.substring(0, indexOfDot); + methodName = name.substring(indexOfDot + 1); + if (!beanClass.getName().equals((className))) { + methodDeclaringClass = ClassUtils.forName(className, beanClass.getClassLoader()); + } + } + + Method destroyMethod = findDestroyMethod(methodDeclaringClass, methodName); if (destroyMethod != null) { return destroyMethod; } for (Class beanInterface : beanClass.getInterfaces()) { - destroyMethod = findDestroyMethod(beanInterface, name); + destroyMethod = findDestroyMethod(beanInterface, methodName); if (destroyMethod != null) { return destroyMethod; } } return null; } - catch (IllegalArgumentException ex) { + catch (ClassNotFoundException | IllegalArgumentException ex) { throw new BeanDefinitionValidationException("Could not find unique destroy method on bean with name '" + this.beanName + ": " + ex.getMessage()); } 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 index 7be4027371f2..be1fbaaea641 100644 --- 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 @@ -70,8 +70,8 @@ void processAheadOfTimeWhenHasInitDestroyAnnotationsAndCustomDefinedMethodNamesA beanDefinition.setDestroyMethodNames("customDestroyMethod"); processAheadOfTime(beanDefinition); RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); - assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly("customInitMethod", "initMethod"); - assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("customDestroyMethod", "destroyMethod"); + assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly("initMethod", "customInitMethod"); + assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("destroyMethod", "customDestroyMethod"); } @Test @@ -129,16 +129,16 @@ void processAheadOfTimeWithMultipleLevelsOfPublicAndPrivateInitAndDestroyMethods RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition(); assertSoftly(softly -> { softly.assertThat(mergedBeanDefinition.getInitMethodNames()).containsExactly( - "afterPropertiesSet", - "customInit", CustomAnnotatedPrivateInitDestroyBean.class.getName() + ".privateInit", // fully-qualified private method - CustomAnnotatedPrivateSameNameInitDestroyBean.class.getName() + ".privateInit" // fully-qualified private method + CustomAnnotatedPrivateSameNameInitDestroyBean.class.getName() + ".privateInit", // fully-qualified private method + "afterPropertiesSet", + "customInit" ); softly.assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly( - "destroy", - "customDestroy", CustomAnnotatedPrivateSameNameInitDestroyBean.class.getName() + ".privateDestroy", // fully-qualified private method - CustomAnnotatedPrivateInitDestroyBean.class.getName() + ".privateDestroy" // fully-qualified private method + CustomAnnotatedPrivateInitDestroyBean.class.getName() + ".privateDestroy", // fully-qualified private method + "destroy", + "customDestroy" ); }); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java index 8ac8784b6a7d..99d725dffcba 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java @@ -18,18 +18,26 @@ 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; @@ -143,6 +151,7 @@ void jakartaAnnotationsCustomPackagePrivateInitDestroyMethodsWithTheSameMethodNa assertThat(bean.initMethods).as("init-methods").containsExactly( "PackagePrivateInitDestroyBean.postConstruct", "SubPackagePrivateInitDestroyBean.postConstruct", + "InitializingBean.afterPropertiesSet", "initMethod" ); @@ -150,10 +159,81 @@ void jakartaAnnotationsCustomPackagePrivateInitDestroyMethodsWithTheSameMethodNa assertThat(bean.destroyMethods).as("destroy-methods").containsExactly( "SubPackagePrivateInitDestroyBean.preDestroy", "PackagePrivateInitDestroyBean.preDestroy", + "DisposableBean.destroy", "destroyMethod" ); } + @Test + @CompileWithForkedClassLoader + void jakartaAnnotationsWithCustomSameMethodNamesWithAotProcessingAndAotRuntime() { + Class 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 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; @@ -186,17 +266,42 @@ private static DefaultListableBeanFactory createBeanFactoryAndRegisterBean(Class return beanFactory; } + private static GenericApplicationContext createApplicationContext( + ApplicationContextInitializer initializer) { + + GenericApplicationContext context = new GenericApplicationContext(); + initializer.initialize(context); + context.refresh(); + return context; + } + + @SuppressWarnings("unchecked") + private static void testCompiledResult(GenericApplicationContext applicationContext, + BiConsumer, 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 { @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.initMethods.add("InitializingBean.afterPropertiesSet"); } @Override - public void destroy() throws Exception { + public void destroy() { this.destroyMethods.add("DisposableBean.destroy"); } } @@ -206,11 +311,11 @@ static class CustomInitDestroyBean { final List initMethods = new ArrayList<>(); final List destroyMethods = new ArrayList<>(); - public void customInit() throws Exception { + public void customInit() { this.initMethods.add("customInit"); } - public void customDestroy() throws Exception { + public void customDestroy() { this.destroyMethods.add("customDestroy"); } } @@ -218,12 +323,12 @@ public void customDestroy() throws Exception { static class CustomAnnotatedPrivateInitDestroyBean extends CustomInitializingDisposableBean { @PostConstruct - private void customInit1() throws Exception { + private void customInit1() { this.initMethods.add("@PostConstruct.privateCustomInit1"); } @PreDestroy - private void customDestroy1() throws Exception { + private void customDestroy1() { this.destroyMethods.add("@PreDestroy.privateCustomDestroy1"); } } @@ -232,13 +337,13 @@ static class CustomAnnotatedPrivateSameNameInitDestroyBean extends CustomAnnotat @PostConstruct @SuppressWarnings("unused") - private void customInit1() throws Exception { + private void customInit1() { this.initMethods.add("@PostConstruct.sameNameCustomInit1"); } @PreDestroy @SuppressWarnings("unused") - private void customDestroy1() throws Exception { + private void customDestroy1() { this.destroyMethods.add("@PreDestroy.sameNameCustomDestroy1"); } } @@ -247,12 +352,12 @@ static class CustomInitializingDisposableBean extends CustomInitDestroyBean implements InitializingBean, DisposableBean { @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.initMethods.add("afterPropertiesSet"); } @Override - public void destroy() throws Exception { + public void destroy() { this.destroyMethods.add("destroy"); } } @@ -260,12 +365,12 @@ public void destroy() throws Exception { static class CustomAnnotatedInitDestroyBean extends CustomInitializingDisposableBean { @PostConstruct - public void postConstruct() throws Exception { + public void postConstruct() { this.initMethods.add("postConstruct"); } @PreDestroy - public void preDestroy() throws Exception { + public void preDestroy() { this.destroyMethods.add("preDestroy"); } } @@ -274,13 +379,13 @@ static class CustomAnnotatedInitDestroyWithShadowedMethodsBean extends CustomIni @PostConstruct @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.initMethods.add("@PostConstruct.afterPropertiesSet"); } @PreDestroy @Override - public void destroy() throws Exception { + public void destroy() { this.destroyMethods.add("@PreDestroy.destroy"); } } @@ -292,18 +397,29 @@ static class AllInOneBean implements InitializingBean, DisposableBean { @PostConstruct @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.initMethods.add("afterPropertiesSet"); } @PreDestroy @Override - public void destroy() throws Exception { + public void destroy() { this.destroyMethods.add("destroy"); } } - static class SubPackagePrivateInitDestroyBean extends PackagePrivateInitDestroyBean { + static class SubPackagePrivateInitDestroyBean extends PackagePrivateInitDestroyBean + implements InitializingBean, DisposableBean { + + @Override + public void afterPropertiesSet() { + this.initMethods.add("InitializingBean.afterPropertiesSet"); + } + + @Override + public void destroy() { + this.destroyMethods.add("DisposableBean.destroy"); + } @PostConstruct void postConstruct() {