From 14da66f91f03b037fac0e2edfc0647eab413bc57 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 26 Apr 2024 15:28:20 -0400 Subject: [PATCH] GH-9103: Fix IntRMHandlerMapping from BPP Fixes: #9103 The `IntegrationRequestMappingHandlerMapping` implements a `DestructionAwareBeanPostProcessor` which causes an early bean initialization including interceptors loading from the application context: ``` 2024-04-26 12:11:07,434 WARN [main] [org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean '(inner bean)#39f5b723' of type [org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected into a currently created BeanPostProcessor [integrationRequestMappingHandlerMapping]? Check the corresponding BeanPostProcessor declaration and its dependencies. ``` * Extract the `DestructionAwareBeanPostProcessor` logic from the `IntegrationRequestMappingHandlerMapping` into separate `DynamicRequestMappingBeanPostProcessor` which does not cause eager beans initialization * Verify with the `IntegrationGraphControllerTests` that the mentioned `not eligible for auto-proxying` warning is not emitted anymore (cherry picked from commit 5d592713c48946e274224d67b27ff10af2b76a21) --- ...tpIntegrationConfigurationInitializer.java | 29 +++++-- ...ynamicRequestMappingBeanPostProcessor.java | 85 +++++++++++++++++++ ...tegrationRequestMappingHandlerMapping.java | 37 +------- 3 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.java index f8c14709bf..d287b1c52a 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -23,8 +23,10 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.integration.config.IntegrationConfigurationInitializer; +import org.springframework.integration.http.inbound.DynamicRequestMappingBeanPostProcessor; import org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping; /** @@ -58,16 +60,25 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi *

In addition, checks if the {@code javax.servlet.Servlet} class is present on the classpath. * When Spring Integration HTTP is used only as an HTTP client, there is no reason to use and register * the HTTP server components. + *

+ * Also registers a {@link DynamicRequestMappingBeanPostProcessor} for dynamically added HTTP inbound endpoints. */ private void registerRequestMappingHandlerMappingIfNecessary(BeanDefinitionRegistry registry) { - if (HttpContextUtils.WEB_MVC_PRESENT && - !registry.containsBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME)) { - BeanDefinitionBuilder requestMappingBuilder = - BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class) - .addPropertyValue("order", 0) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME, - requestMappingBuilder.getBeanDefinition()); + if (HttpContextUtils.WEB_MVC_PRESENT) { + if (!registry.containsBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME)) { + BeanDefinitionBuilder requestMappingBuilder = + BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class) + .addPropertyValue("order", 0) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME, + requestMappingBuilder.getBeanDefinition()); + } + + BeanDefinitionReaderUtils.registerWithGeneratedName( + BeanDefinitionBuilder.genericBeanDefinition(DynamicRequestMappingBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .getBeanDefinition(), + registry); } } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java new file mode 100644 index 0000000000..c82c38ae06 --- /dev/null +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024 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.integration.http.inbound; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; + +/** + * A {@link DestructionAwareBeanPostProcessor} to register request mapping + * created at runtime (e.g. via + * {@link org.springframework.integration.dsl.context.IntegrationFlowContext}) + * by {@link HttpRequestHandlingEndpointSupport} instances + * into the {@link IntegrationRequestMappingHandlerMapping}. + * These mappings are also removed when respective {@link HttpRequestHandlingEndpointSupport} + * bean is destroyed. + * + * @author Artem Bilan + * + * @since 6.2.5 + */ +public class DynamicRequestMappingBeanPostProcessor + implements BeanFactoryAware, DestructionAwareBeanPostProcessor, SmartInitializingSingleton { + + private BeanFactory beanFactory; + + private IntegrationRequestMappingHandlerMapping integrationRequestMappingHandlerMapping; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public void afterSingletonsInstantiated() { + this.integrationRequestMappingHandlerMapping = + this.beanFactory.getBean(IntegrationRequestMappingHandlerMapping.class); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (this.integrationRequestMappingHandlerMapping != null && isHandler(bean.getClass())) { + this.integrationRequestMappingHandlerMapping.detectHandlerMethods(bean); + } + return bean; + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + if (isHandler(bean.getClass())) { + RequestMappingInfo mapping = + this.integrationRequestMappingHandlerMapping.getMappingForEndpoint((BaseHttpInboundEndpoint) bean); + if (mapping != null) { + this.integrationRequestMappingHandlerMapping.unregisterMapping(mapping); + } + } + } + + @Override + public boolean requiresDestruction(Object bean) { + return isHandler(bean.getClass()); + } + + private boolean isHandler(Class beanType) { + return HttpRequestHandlingEndpointSupport.class.isAssignableFrom(beanType); + } + +} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java index 88eebd3858..83ee5a4b36 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2024 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. @@ -26,8 +26,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; @@ -73,11 +71,6 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl * {@link org.springframework.web.servlet.HandlerMapping} * compromise implementation between method-level annotations and component-level * (e.g. Spring Integration XML) configurations. - *

- * Starting with version 5.1, this class implements {@link DestructionAwareBeanPostProcessor} to - * register HTTP endpoints at runtime for dynamically declared beans, e.g. via - * {@link org.springframework.integration.dsl.context.IntegrationFlowContext}, and unregister - * them during the {@link BaseHttpInboundEndpoint} destruction. *

* This class extends the Spring MVC {@link RequestMappingHandlerMapping} class, inheriting * most of its logic, especially {@link #handleNoMatch(java.util.Set, String, HttpServletRequest)}, @@ -96,7 +89,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl * @see RequestMappingHandlerMapping */ public final class IntegrationRequestMappingHandlerMapping extends RequestMappingHandlerMapping - implements ApplicationListener, DestructionAwareBeanPostProcessor { + implements ApplicationListener { private static final Method HANDLE_REQUEST_METHOD = ReflectionUtils.findMethod(HttpRequestHandler.class, "handleRequest", HttpServletRequest.class, @@ -104,30 +97,6 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin private final AtomicBoolean initialized = new AtomicBoolean(); - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (this.initialized.get() && isHandler(bean.getClass())) { - detectHandlerMethods(bean); - } - - return bean; - } - - @Override - public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { - if (isHandler(bean.getClass())) { - RequestMappingInfo mapping = getMappingForEndpoint((BaseHttpInboundEndpoint) bean); - if (mapping != null) { - unregisterMapping(mapping); - } - } - } - - @Override - public boolean requiresDestruction(Object bean) { - return isHandler(bean.getClass()); - } - @Override protected boolean isHandler(Class beanType) { return HttpRequestHandlingEndpointSupport.class.isAssignableFrom(beanType); @@ -229,7 +198,7 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin * @see RequestMappingHandlerMapping#getMappingForMethod */ @Nullable - private RequestMappingInfo getMappingForEndpoint(BaseHttpInboundEndpoint endpoint) { + RequestMappingInfo getMappingForEndpoint(BaseHttpInboundEndpoint endpoint) { final RequestMapping requestMapping = endpoint.getRequestMapping(); if (ObjectUtils.isEmpty(requestMapping.getPathPatterns())) {