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