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 9d99a3344592..c4efb48dcd00 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,7 +139,25 @@ private void addInitDestroyMethods(Builder code, AbstractBeanDefinition beanDefi } private void addInitDestroyHint(Class beanUserClass, String methodName) { - Method method = ReflectionUtils.findMethod(beanUserClass, methodName); + Class methodDeclaringClass = beanUserClass; + + // Parse fully-qualified method name if necessary. + int indexOfDot = methodName.lastIndexOf('.'); + if (indexOfDot > 0) { + String className = methodName.substring(0, indexOfDot); + methodName = methodName.substring(indexOfDot + 1); + if (!beanUserClass.getName().equals(className)) { + try { + methodDeclaringClass = ClassUtils.forName(className, beanUserClass.getClassLoader()); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to load Class [" + className + + "] from ClassLoader [" + beanUserClass.getClassLoader() + "]", ex); + } + } + } + + Method method = ReflectionUtils.findMethod(methodDeclaringClass, 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 25b4d94a2a05..bf4ee5bce8f3 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 @@ -1842,18 +1842,22 @@ 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(); + MethodDescriptor descriptor = MethodDescriptor.create(beanName, beanClass, initMethodName); + String methodName = descriptor.methodName(); + Method initMethod = (mbd.isNonPublicAccessAllowed() ? - BeanUtils.findMethod(bean.getClass(), initMethodName) : - ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); + BeanUtils.findMethod(descriptor.declaringClass(), 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. @@ -1862,9 +1866,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 d33302180738..22d92730b31e 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 @@ -266,12 +266,15 @@ else if (this.destroyMethodNames != null) { private Method determineDestroyMethod(String destroyMethodName) { try { Class beanClass = this.bean.getClass(); - Method destroyMethod = findDestroyMethod(beanClass, destroyMethodName); + MethodDescriptor descriptor = MethodDescriptor.create(this.beanName, beanClass, destroyMethodName); + String methodName = descriptor.methodName(); + + Method destroyMethod = findDestroyMethod(descriptor.declaringClass(), methodName); if (destroyMethod != null) { return destroyMethod; } for (Class beanInterface : beanClass.getInterfaces()) { - destroyMethod = findDestroyMethod(beanInterface, destroyMethodName); + destroyMethod = findDestroyMethod(beanInterface, methodName); if (destroyMethod != null) { return destroyMethod; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java new file mode 100644 index 000000000000..c895734f0ec3 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2023 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.support; + +import org.springframework.util.ClassUtils; + +/** + * Descriptor for a {@link java.lang.reflect.Method Method} which holds a + * reference to the method's {@linkplain #declaringClass declaring class}, + * {@linkplain #methodName name}, and {@linkplain #parameterTypes parameter types}. + * + * @param declaringClass the method's declaring class + * @param methodName the name of the method + * @param parameterTypes the types of parameters accepted by the method + * @author Sam Brannen + * @since 6.0.11 + */ +record MethodDescriptor(Class declaringClass, String methodName, Class... parameterTypes) { + + /** + * Create a {@link MethodDescriptor} for the supplied bean class and method name. + *

The supplied {@code methodName} may be a {@linkplain Method#getName() + * simple method name} or a + * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) + * qualified method name}. + *

If the method name is fully qualified, this utility will parse the + * method name and its declaring class from the qualified method name and then + * attempt to load the method's declaring class using the {@link ClassLoader} + * of the supplied {@code beanClass}. Otherwise, the returned descriptor will + * reference the supplied {@code beanClass} and {@code methodName}. + * @param beanName the bean name in the factory (for debugging purposes) + * @param beanClass the bean class + * @param methodName the name of the method + * @return a new {@code MethodDescriptor}; never {@code null} + */ + static MethodDescriptor create(String beanName, Class beanClass, String methodName) { + try { + Class declaringClass = beanClass; + String methodNameToUse = methodName; + + // Parse fully-qualified method name if necessary. + int indexOfDot = methodName.lastIndexOf('.'); + if (indexOfDot > 0) { + String className = methodName.substring(0, indexOfDot); + methodNameToUse = methodName.substring(indexOfDot + 1); + if (!beanClass.getName().equals(className)) { + declaringClass = ClassUtils.forName(className, beanClass.getClassLoader()); + } + } + return new MethodDescriptor(declaringClass, methodNameToUse); + } + catch (Exception | LinkageError ex) { + throw new BeanDefinitionValidationException( + "Could not create MethodDescriptor for method '%s' on bean with name '%s': %s" + .formatted(methodName, 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-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java index 64cab3e0ea6a..3edeb086c2b3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java @@ -376,6 +376,9 @@ void multipleItems() { @Nested class InitDestroyMethodTests { + private final String privateInitMethod = InitDestroyBean.class.getName() + ".privateInit"; + private final String privateDestroyMethod = InitDestroyBean.class.getName() + ".privateDestroy"; + @BeforeEach void setTargetType() { beanDefinition.setTargetType(InitDestroyBean.class); @@ -393,11 +396,18 @@ void singleInitMethod() { assertHasMethodInvokeHints(InitDestroyBean.class, "init"); } + @Test + void privateInitMethod() { + beanDefinition.setInitMethodName(privateInitMethod); + compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly(privateInitMethod)); + assertHasMethodInvokeHints(InitDestroyBean.class, "privateInit"); + } + @Test void multipleInitMethods() { - beanDefinition.setInitMethodNames("init", "init2"); - compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly("init", "init2")); - assertHasMethodInvokeHints(InitDestroyBean.class, "init", "init2"); + beanDefinition.setInitMethodNames("init", privateInitMethod); + compile((beanDef, compiled) -> assertThat(beanDef.getInitMethodNames()).containsExactly("init", privateInitMethod)); + assertHasMethodInvokeHints(InitDestroyBean.class, "init", "privateInit"); } @Test @@ -412,11 +422,18 @@ void singleDestroyMethod() { assertHasMethodInvokeHints(InitDestroyBean.class, "destroy"); } + @Test + void privateDestroyMethod() { + beanDefinition.setDestroyMethodName(privateDestroyMethod); + compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly(privateDestroyMethod)); + assertHasMethodInvokeHints(InitDestroyBean.class, "privateDestroy"); + } + @Test void multipleDestroyMethods() { - beanDefinition.setDestroyMethodNames("destroy", "destroy2"); - compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly("destroy", "destroy2")); - assertHasMethodInvokeHints(InitDestroyBean.class, "destroy", "destroy2"); + beanDefinition.setDestroyMethodNames("destroy", privateDestroyMethod); + compile((beanDef, compiled) -> assertThat(beanDef.getDestroyMethodNames()).containsExactly("destroy", privateDestroyMethod)); + assertHasMethodInvokeHints(InitDestroyBean.class, "destroy", "privateDestroy"); } } @@ -461,13 +478,15 @@ static class InitDestroyBean { void init() { } - void init2() { + @SuppressWarnings("unused") + private void privateInit() { } void destroy() { } - void destroy2() { + @SuppressWarnings("unused") + private void privateDestroy() { } } 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 9197aa152ebc..42ea7d19f3de 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; @@ -156,6 +164,79 @@ void jakartaAnnotationsCustomPackagePrivateInitDestroyMethodsWithTheSameMethodNa ); } + /** + * @see org.springframework.context.aot.ApplicationContextAotGeneratorTests#processAheadOfTimeWhenHasMultipleInitDestroyMethods + */ + @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; @@ -188,6 +269,31 @@ 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 { diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index eb155764afc6..81767e3cfee9 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -261,9 +261,9 @@ void processAheadOfTimeWhenHasMultipleInitDestroyMethods() { 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"); }); }