From 6fbeec67a598505d3bcded70ea119fa7dcdf2bc0 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 28 Mar 2025 14:35:46 -0400 Subject: [PATCH] GH-9931: Make consumer endpoints dependant on `ChannelInitializer` Fixes: #9931 Issue link: https://github.com/spring-projects/spring-integration/issues/9931 If no message channel bean is declared explicitly, the XML parser for endpoints register such a candidate into the global `ChannelInitializer`. This way, the channel is created when this bean is initialized. However, there are cases when endpoint bean could be called before `ChannelInitializer` initialization. * Add `consumerEndpointBuilder.addDependsOn(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME);` into the `AbstractConsumerEndpointParser` to ensure that `ChannelInitializer` bean is initialized before the called endpoint bean. * Adjust `ChannelInitializer` logic to use `registerSingleton()` API instead of `registerBeanDefinition()` since the last one may cause problems with beans cache when this API is called at runtime. (cherry picked from commit c9e3de87678f7abdd589eaf0a587346408abbc0f) --- .../config/ChannelInitializer.java | 44 +++++++------------ .../xml/AbstractConsumerEndpointParser.java | 24 ++++++---- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/ChannelInitializer.java b/spring-integration-core/src/main/java/org/springframework/integration/config/ChannelInitializer.java index e38bfce642..c1fb359af0 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/ChannelInitializer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/ChannelInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -25,17 +25,18 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.util.Assert; /** - * A {@link InitializingBean} implementation that is responsible for creating + * An {@link InitializingBean} implementation that is responsible for creating * channels that are not explicitly defined but identified via the 'input-channel' * attribute of the corresponding endpoints. - * + *

* This bean plays a role of pre-instantiator since it is instantiated and - * initialized as the very first bean of all SI beans using + * initialized as the very first bean of all Spring Integration beans using * {@link org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler}. * * @author Oleg Zhurakousky @@ -48,7 +49,7 @@ public final class ChannelInitializer implements BeanFactoryAware, InitializingB private static final Log LOGGER = LogFactory.getLog(ChannelInitializer.class); - private volatile BeanFactory beanFactory; + private volatile DefaultListableBeanFactory beanFactory; private volatile boolean autoCreate = true; @@ -61,49 +62,38 @@ public final class ChannelInitializer implements BeanFactoryAware, InitializingB @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; + this.beanFactory = (DefaultListableBeanFactory) beanFactory; } @Override public void afterPropertiesSet() { Assert.notNull(this.beanFactory, "'beanFactory' must not be null"); - if (!this.autoCreate) { - return; - } - else { + if (this.autoCreate) { AutoCreateCandidatesCollector channelCandidatesCollector = this.beanFactory.getBean(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME, AutoCreateCandidatesCollector.class); // at this point channelNames are all resolved with placeholders and SpEL - Collection channelNames = channelCandidatesCollector.getChannelNames(); + Collection channelNames = channelCandidatesCollector.channelNames; if (channelNames != null) { for (String channelName : channelNames) { if (!this.beanFactory.containsBean(channelName)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Auto-creating channel '" + channelName + "' as DirectChannel"); } - IntegrationConfigUtils.autoCreateDirectChannel(channelName, - (BeanDefinitionRegistry) this.beanFactory); + DirectChannel channelToRegister = new DirectChannel(); + this.beanFactory.registerSingleton(channelName, channelToRegister); + this.beanFactory.initializeBean(channelToRegister, channelName); } } } } } - /* - * Collects candidate channel names to be auto-created by ChannelInitializer + /** + * Collects candidate channel names to be auto-created by {@link ChannelInitializer}. + * @param channelNames the auto-create candidate channel bean names. */ - public static class AutoCreateCandidatesCollector { - - private final Collection channelNames; - - AutoCreateCandidatesCollector(Collection channelNames) { - this.channelNames = channelNames; - } - - public Collection getChannelNames() { - return this.channelNames; - } + public record AutoCreateCandidatesCollector(Collection channelNames) { } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractConsumerEndpointParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractConsumerEndpointParser.java index f9642e6f22..9d194edd57 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractConsumerEndpointParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractConsumerEndpointParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -29,6 +29,7 @@ import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; @@ -148,7 +149,7 @@ public abstract class AbstractConsumerEndpointParser extends AbstractBeanDefinit String inputChannelName = element.getAttribute(inputChannelAttributeName); if (!parserContext.getRegistry().containsBeanDefinition(inputChannelName)) { - registerChannelForCreation(parserContext, inputChannelName); + registerChannelForCreation(parserContext, inputChannelName, builder); } IntegrationNamespaceUtils.checkAndConfigureFixedSubscriberChannel(element, parserContext, inputChannelName, handlerBeanName); @@ -177,12 +178,17 @@ public abstract class AbstractConsumerEndpointParser extends AbstractBeanDefinit } } - private void registerChannelForCreation(ParserContext parserContext, String inputChannelName) { - if (parserContext.getRegistry() - .containsBeanDefinition(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME)) { + private void registerChannelForCreation(ParserContext parserContext, String inputChannelName, + BeanDefinitionBuilder consumerEndpointBuilder) { - BeanDefinition channelRegistry = parserContext.getRegistry(). - getBeanDefinition(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME); + BeanDefinitionRegistry beanDefinitionRegistry = parserContext.getRegistry(); + + if (beanDefinitionRegistry.containsBeanDefinition( + IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME)) { + + BeanDefinition channelRegistry = + beanDefinitionRegistry.getBeanDefinition( + IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME); ConstructorArgumentValues caValues = channelRegistry.getConstructorArgumentValues(); ValueHolder vh = caValues.getArgumentValue(0, Collection.class); if (vh == null) { //although it should never happen if it does we can fix it @@ -194,11 +200,13 @@ public abstract class AbstractConsumerEndpointParser extends AbstractBeanDefinit (Collection) caValues.getArgumentValue(0, Collection.class) .getValue(); // NOSONAR see comment above channelCandidateNames.add(inputChannelName); // NOSONAR + + consumerEndpointBuilder.addDependsOn(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME); } else { parserContext.getReaderContext().error("Failed to locate '" + IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME + "'", - parserContext.getRegistry()); + beanDefinitionRegistry); } }