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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user