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.
@@ -26,8 +26,8 @@ import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.endpoint.ExpressionMessageProducerSupport;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -49,10 +49,10 @@ import org.springframework.util.Assert;
public class ApplicationEventListeningMessageProducer extends ExpressionMessageProducerSupport
implements GenericApplicationListener {
private volatile Set<ResolvableType> eventTypes;
private ApplicationEventMulticaster applicationEventMulticaster;
private volatile Set<ResolvableType> eventTypes;
private volatile long stoppedAt;
/**
@@ -106,12 +106,17 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (isActive() || ((event instanceof ContextStoppedEvent || event instanceof ContextClosedEvent)
&& stoppedRecently())) {
boolean contextFinished = event instanceof ContextStoppedEvent || event instanceof ContextClosedEvent;
if (isActive() || (contextFinished && stoppedRecently())) {
Object source = event.getSource();
if (source instanceof Message<?>) {
sendMessage((Message<?>) source);
if (contextFinished && getRequiredOutputChannel() instanceof AbstractMessageChannel) {
logger.warn("Messages for 'ContextStoppedEvent' or 'ContextClosedEvent' cannot be dispatched " +
"via 'AbstractMessageChannel' beans: the application context is in the finished state." +
"Consider to use custom 'MessageChannel' implementation without dispatching logic.");
}
if (event.getSource() instanceof Message<?> message) {
sendMessage(message);
}
else {
Message<?> message;
@@ -128,8 +133,8 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP
}
private Object extractObjectToSend(Object root) {
if (root instanceof PayloadApplicationEvent) {
return ((PayloadApplicationEvent<?>) root).getPayload();
if (root instanceof PayloadApplicationEvent<?> payloadApplicationEvent) {
return payloadApplicationEvent.getPayload();
}
return evaluatePayloadExpression(root);
}
@@ -167,14 +172,9 @@ public class ApplicationEventListeningMessageProducer extends ExpressionMessageP
return false;
}
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
return HIGHEST_PRECEDENCE;
}
@Override

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"/>