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-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.
@@ -18,6 +18,9 @@ package org.springframework.integration.event.inbound;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
@@ -341,13 +344,7 @@ public class ApplicationEventListeningMessageProducerTests {
}
private static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
private final AtomicInteger counter;
private TestApplicationListener(AtomicInteger counter) {
this.counter = counter;
}
private record TestApplicationListener(AtomicInteger counter) implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
@@ -356,4 +353,35 @@ public class ApplicationEventListeningMessageProducerTests {
}
static final class ContextEventsChannel implements PollableChannel {
private final BlockingQueue<Message<?>> internalQueue = new LinkedBlockingQueue<>();
@Override
public Message<?> receive() {
return receive(-1);
}
@Override
public Message<?> receive(long timeout) {
try {
return this.internalQueue.poll(timeout, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean send(Message<?> message, long timeout) {
try {
return this.internalQueue.offer(message, timeout, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd"
xmlns:int="http://www.springframework.org/schema/integration">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<int:channel id="channel">
<int:queue capacity="5"/>
</int:channel>
<bean id="channel"
class="org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducerTests$ContextEventsChannel"/>
<bean id="adapter" class="org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer">
<property name="outputChannel" ref="channel"/>