GH-9854: Special error for "too early message production"

Fixes: https://github.com/spring-projects/spring-integration/issues/9854

The well-known `Dispatcher has no subscribers` is not very informative
when a message is produced from early application context initialization phase

* Add internal `ApplicationRunningController` bean to handle early `start()` event
* Check for this bean status from the `AbstractMessageChannel.send()`
* Throw specific `MessageDispatchingException` to indicate that the message was produced from a wrong place
* Adjust `ApplicationEventListeningMessageProducer` logic for `ContextStoppedEvent` & `ContextClosedEvent`
to indicate that `AbstractMessageChannel` bean might not dispatch a message because the application context is not running
This commit is contained in:
Artem Bilan
2025-02-20 17:45:20 -05:00
parent 84a3be1022
commit 2296e4798e
10 changed files with 213 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
@@ -34,10 +34,14 @@ import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.core.OrderComparator;
import org.springframework.core.log.LogAccessor;
import org.springframework.integration.IntegrationPattern;
import org.springframework.integration.IntegrationPatternType;
import org.springframework.integration.MessageDispatchingException;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.history.MessageHistory;
@@ -110,6 +114,10 @@ public abstract class AbstractMessageChannel extends IntegrationObjectSupport
private volatile String fullChannelName;
private volatile boolean applicationRunning;
private volatile Lifecycle applicationRunningController;
@Override
public String getComponentType() {
return "channel";
@@ -319,6 +327,7 @@ public abstract class AbstractMessageChannel extends IntegrationObjectSupport
public boolean send(Message<?> messageArg, long timeout) {
Assert.notNull(messageArg, "message must not be null");
Assert.notNull(messageArg.getPayload(), "message payload must not be null");
assertApplicationRunning(messageArg);
Message<?> message = messageArg;
if (this.shouldTrack) {
message = MessageHistory.write(message, this, getMessageBuilderFactory());
@@ -335,6 +344,39 @@ public abstract class AbstractMessageChannel extends IntegrationObjectSupport
}
}
private void assertApplicationRunning(Message<?> message) {
if (!this.applicationRunning) {
ApplicationContext applicationContext = getApplicationContext();
this.applicationRunning =
applicationContext == null ||
!applicationContext.containsBean(
IntegrationContextUtils.APPLICATION_RUNNING_CONTROLLER_BEAN_NAME);
if (!this.applicationRunning) {
if (((ConfigurableApplicationContext) applicationContext).isActive()) {
this.applicationRunningController =
applicationContext.getBean(IntegrationContextUtils.APPLICATION_RUNNING_CONTROLLER_BEAN_NAME,
Lifecycle.class);
this.applicationRunning = this.applicationRunningController.isRunning();
}
}
}
if (this.applicationRunning && this.applicationRunningController != null) {
this.applicationRunning = this.applicationRunningController.isRunning();
}
if (!this.applicationRunning) {
throw new MessageDispatchingException(message,
"""
The application context is not ready to dispatch messages. \
It has to be refreshed or started first. \
Also, messages must not be emitted from initialization phase, \
like 'afterPropertiesSet()', '@PostConstruct' or bean definition methods. \
Consider to use 'SmartLifecycle.start()' instead.""");
}
}
private boolean sendWithObservation(Message<?> message, long timeout) {
MutableMessage<?> messageToSend = MutableMessage.of(message);
Observation observation = IntegrationObservation.PRODUCER.observation(
@@ -343,15 +385,15 @@ public abstract class AbstractMessageChannel extends IntegrationObjectSupport
() -> new MessageSenderContext(messageToSend, getComponentName()),
this.observationRegistry);
Boolean observe = observation.observe(() -> {
Message<?> messageToSendInternal = messageToSend;
if (message instanceof ErrorMessage errorMessage) {
messageToSendInternal =
new ErrorMessage(errorMessage.getPayload(),
messageToSend.getHeaders(),
errorMessage.getOriginalMessage());
}
return sendInternal(messageToSendInternal, timeout);
});
Message<?> messageToSendInternal = messageToSend;
if (message instanceof ErrorMessage errorMessage) {
messageToSendInternal =
new ErrorMessage(errorMessage.getPayload(),
messageToSend.getHeaders(),
errorMessage.getOriginalMessage());
}
return sendInternal(messageToSendInternal, timeout);
});
return Boolean.TRUE.equals(observe);
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 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.
* 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.config;
import org.springframework.context.SmartLifecycle;
/**
* An infrastructure bean to hold the status of the application context when
* it is ready for interaction: refreshed or started.
* <p>
* Well-known {@link org.springframework.context.ConfigurableApplicationContext#isRunning()}
* (or {@link org.springframework.context.event.ContextRefreshedEvent})
* is good for target applications, when all the beans are already started,
* but most of Spring Integration channel adapters initiate their logic
* from the {@link SmartLifecycle#start()} implementation, so it would be false report
* that application is not running during start.
* <p>
* This implementation uses {@value Integer#MIN_VALUE} for its phase to be started as early as possible.
*
* @author Artem Bilan
*
* @since 6.5
*/
class ApplicationRunningController implements SmartLifecycle {
private volatile boolean running;
@Override
public void start() {
this.running = true;
}
@Override
public void stop() {
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
return Integer.MIN_VALUE;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -129,6 +129,7 @@ public class DefaultConfiguringBeanFactoryPostProcessor implements BeanDefinitio
registerListMessageHandlerMethodFactory();
registerIntegrationConfigurationReport();
registerControlBusCommandRegistry();
registerApplicationRunningController();
}
@Override
@@ -453,6 +454,17 @@ public class DefaultConfiguringBeanFactoryPostProcessor implements BeanDefinitio
}
}
private void registerApplicationRunningController() {
if (!this.beanFactory.containsBean(IntegrationContextUtils.APPLICATION_RUNNING_CONTROLLER_BEAN_NAME)) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(ApplicationRunningController.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
this.registry.registerBeanDefinition(IntegrationContextUtils.APPLICATION_RUNNING_CONTROLLER_BEAN_NAME,
builder.getBeanDefinition());
}
}
private static BeanDefinitionBuilder createMessageHandlerMethodFactoryBeanDefinition(boolean listCapable) {
return BeanDefinitionBuilder.genericBeanDefinition(IntegrationMessageHandlerMethodFactory.class)
.addConstructorArgValue(listCapable)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -102,6 +102,8 @@ public abstract class IntegrationContextUtils {
public static final String CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME = "controlBusCommandRegistry";
public static final String APPLICATION_RUNNING_CONTROLLER_BEAN_NAME = "applicationRunningController";
/**
* The default timeout for blocking operations like send and receive messages.
* @since 6.1

View File

@@ -334,7 +334,7 @@ public abstract class MessageProducerSupport extends AbstractEndpoint
return ErrorMessageUtils.getAttributeAccessor(message, null);
}
private MessageChannel getRequiredOutputChannel() {
protected MessageChannel getRequiredOutputChannel() {
MessageChannel messageChannel = getOutputChannel();
Assert.state(messageChannel != null, "The 'outputChannel' or `outputChannelName` must be configured");
return messageChannel;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -24,8 +24,10 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.MessageDispatchingException;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.context.IntegrationContextUtils;
@@ -43,8 +45,10 @@ import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
/**
@@ -67,14 +71,21 @@ public class ApplicationContextMessageBusTests {
}
@Test
public void endpointRegistrationWithInputChannelReference() {
public void endpointRegistrationWithInputChannelReference() throws ClassNotFoundException {
this.context.registerBean(IntegrationContextUtils.APPLICATION_RUNNING_CONTROLLER_BEAN_NAME,
BeanUtils.instantiateClass(
ClassUtils.forName(
IntegrationContextUtils.BASE_PACKAGE + ".config.ApplicationRunningController", null)));
QueueChannel sourceChannel = new QueueChannel();
sourceChannel.setApplicationContext(this.context);
QueueChannel targetChannel = new QueueChannel();
this.context.registerChannel("sourceChannel", sourceChannel);
this.context.registerChannel("targetChannel", targetChannel);
Message<String> message = MessageBuilder.withPayload("test")
.setReplyChannelName("targetChannel").build();
sourceChannel.send(message);
assertThatExceptionOfType(MessageDispatchingException.class)
.isThrownBy(() -> sourceChannel.send(message))
.withMessageStartingWith("The application context is not ready to dispatch messages.");
AbstractReplyProducingMessageHandler handler = new AbstractReplyProducingMessageHandler() {
@Override
@@ -88,8 +99,15 @@ public class ApplicationContextMessageBusTests {
endpoint.setBeanFactory(mock(BeanFactory.class));
this.context.registerEndpoint("testEndpoint", endpoint);
this.context.refresh();
sourceChannel.send(message);
Message<?> result = targetChannel.receive(10000);
assertThat(result.getPayload()).isEqualTo("test");
this.context.stop();
assertThatExceptionOfType(MessageDispatchingException.class)
.isThrownBy(() -> sourceChannel.send(message))
.withMessageStartingWith("The application context is not ready to dispatch messages.");
}
@Test