GH-9890: Fix ControlBusCommandRegistry for requiresDestruction()

Fixes: #9890
Issue link: https://github.com/spring-projects/spring-integration/issues/9890

The `DestructionAwareBeanPostProcessor.requiresDestruction()` defaults to `true`.
As a result, beans that are not meant to be processed during the destroy lifecycle
are being registered for the `AbstractBeanFactory#registerDisposableBeanIfNecessary`.
for example, the `SimpleThreadScope` does not support destruction callbacks.

In other words, the `DestructionAwareBeanPostProcessor.requiresDestruction()` contract
is not merely for `DestructionAwareBeanPostProcessor` logic, but also has an effect
on the whole `BeanFactory`

* Implement `requiresDestruction()` in the `ControlBusCommandRegistry`
for same filter as `registerControlBusCommands()` does right now.
* Optimize behaviour using `MergedAnnotations` for the class instead of
`AnnotationUtils.findAnnotation()` twice, which creates the mentioned `MergedAnnotations` internally.

**Auto-cherry-pick to `6.4.x`**
This commit is contained in:
Artem Bilan
2025-03-13 14:29:41 -04:00
parent 48bb0d6c45
commit 74cf59bacb

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024 the original author or authors.
* Copyright 2024-2025 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.
@@ -37,6 +37,8 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.context.Lifecycle;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -115,16 +117,17 @@ public class ControlBusCommandRegistry
* @param bean the bean for registration
*/
public void registerControlBusCommands(String beanName, Object bean) {
Class<?> beanClass = bean.getClass();
if (bean instanceof Lifecycle || bean instanceof CustomizableThreadCreator
|| AnnotationUtils.findAnnotation(beanClass, ManagedResource.class) != null
|| AnnotationUtils.findAnnotation(beanClass, IntegrationManagedResource.class) != null) {
ReflectionUtils.doWithMethods(beanClass, method -> populateExpressionForCommand(beanName, method),
if (isBeanEligible(bean)) {
ReflectionUtils.doWithMethods(bean.getClass(), method -> populateExpressionForCommand(beanName, method),
CONTROL_BUS_METHOD_FILTER);
}
}
@Override
public boolean requiresDestruction(Object bean) {
return isBeanEligible(bean);
}
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
this.controlBusCommands.remove(beanName);
@@ -221,6 +224,17 @@ public class ControlBusCommandRegistry
return buildExpressionForMethodToCall(commandMethod, methodForCommand.get());
}
private static boolean isBeanEligible(Object bean) {
MergedAnnotations mergedAnnotations =
MergedAnnotations.from(bean.getClass(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.none());
return bean instanceof Lifecycle
|| bean instanceof CustomizableThreadCreator
|| mergedAnnotations.isPresent(ManagedResource.class)
|| mergedAnnotations.isPresent(IntegrationManagedResource.class);
}
private static Expression buildExpressionForMethodToCall(CommandMethod commandMethod, Method methodForCommand) {
Assert.isTrue(CONTROL_BUS_METHOD_FILTER.matches(methodForCommand),
() -> "The method '%s' is not valid Control Bus command".formatted(methodForCommand));