GH-3280: NullChannel as reply for void gateways (#3292)

* GH-3280: NullChannel as reply for void gateways

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

To properly support a one-way gateway (`void` return type), it is
better to ignore any possible replies from downstream instead of
unexpected `no output-channel or replyChannel header available`
error

* Populate a `nullChannel` into a `replyChannel` header
for `void` gateway methods when `replyChannel` is not set explicitly
* Remove redundant `GatewayProxyFactoryBean.PARSER` in favor of
similar `EXPRESSION_PARSER` in the super class
* Test and document the feature

* * Fix language in the `whats-new.adoc`

Co-authored-by: Gary Russell <grussell@vmware.com>

* * Fix language in gateway.adoc

Co-authored-by: Gary Russell <grussell@vmware.com>

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2020-05-27 16:10:05 -04:00
committed by GitHub
parent e98169146b
commit b8e2dfe4e1
4 changed files with 59 additions and 12 deletions

View File

@@ -54,11 +54,11 @@ import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.IntegrationPatternType;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.ValueExpression;
@@ -103,8 +103,6 @@ import reactor.core.publisher.Mono;
public class GatewayProxyFactoryBean extends AbstractEndpoint
implements TrackableComponent, FactoryBean<Object>, MethodInterceptor, BeanClassLoaderAware {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private final Object initializationMonitor = new Object();
private final Map<Method, MethodInvocationGateway> gatewayMap = new HashMap<>();
@@ -718,7 +716,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
* no longer work as expected; they will need to use, say, -1 instead.
*/
if (payloadExpression == null && StringUtils.hasText(gatewayAnnotation.payloadExpression())) {
payloadExpression = PARSER.parseExpression(gatewayAnnotation.payloadExpression());
payloadExpression = EXPRESSION_PARSER.parseExpression(gatewayAnnotation.payloadExpression());
}
}
else if (methodMetadata != null && methodMetadata.getPayloadExpression() != null) {
@@ -880,11 +878,24 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
Map<String, Object> headers = null;
// We don't want to eagerly resolve the error channel here
Object errorChannelForVoidReturn = this.errorChannel == null ? this.errorChannelName : this.errorChannel;
if (errorChannelForVoidReturn != null && method.getReturnType().equals(void.class)) {
boolean isVoidReturnType = method.getReturnType().equals(void.class);
if (errorChannelForVoidReturn != null && isVoidReturnType) {
headers = new HashMap<>();
headers.put(MessageHeaders.ERROR_CHANNEL, errorChannelForVoidReturn);
}
if (isVoidReturnType &&
(!headerExpressions.containsKey(MessageHeaders.REPLY_CHANNEL) ||
(this.globalMethodMetadata != null &&
!this.globalMethodMetadata.getHeaderExpressions()
.containsKey(MessageHeaders.REPLY_CHANNEL)))) {
if (headers == null) {
headers = new HashMap<>();
}
headers.put(MessageHeaders.REPLY_CHANNEL, IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME);
}
if (getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory) {
Set<String> headerNames = new HashSet<>(headerExpressions.keySet());
@@ -987,16 +998,12 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
@Override // guarded by super#lifecycleLock
protected void doStart() {
for (MethodInvocationGateway gateway : this.gatewayMap.values()) {
gateway.start();
}
this.gatewayMap.values().forEach(MethodInvocationGateway::start);
}
@Override // guarded by super#lifecycleLock
protected void doStop() {
for (MethodInvocationGateway gateway : this.gatewayMap.values()) {
gateway.stop();
}
this.gatewayMap.values().forEach(MethodInvocationGateway::stop);
}
@SuppressWarnings("unchecked")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -19,6 +19,7 @@ package org.springframework.integration.gateway;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -48,7 +49,9 @@ import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
@@ -129,6 +132,32 @@ public class GatewayProxyFactoryBeanTests {
assertThat(message.getPayload()).isEqualTo("test");
}
@Test
public void testOneWayIgnoreReply() {
DirectChannel requestChannel = new DirectChannel();
BeanFactory beanFactory = mock(BeanFactory.class);
QueueChannel nullChannel = new QueueChannel();
willReturn(nullChannel)
.given(beanFactory)
.getBean(IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME, MessageChannel.class);
BridgeHandler handler = new BridgeHandler();
handler.setBeanFactory(beanFactory);
handler.afterPropertiesSet();
requestChannel.subscribe(handler);
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean(TestService.class);
proxyFactory.setDefaultRequestChannel(requestChannel);
proxyFactory.setBeanName("testGateway");
proxyFactory.setBeanFactory(beanFactory);
proxyFactory.afterPropertiesSet();
TestService service = (TestService) proxyFactory.getObject();
service.oneWay("test");
Message<?> message = nullChannel.receive(1000);
assertThat(message)
.isNotNull()
.extracting(Message::getPayload)
.isEqualTo("test");
}
@Test
public void testSolicitResponse() {
QueueChannel replyChannel = new QueueChannel();

View File

@@ -82,6 +82,9 @@ The gateway creates a bridge from it to the temporary, anonymous reply channel t
You might also want to explicitly provide a reply channel for monitoring or auditing through an interceptor (for example, <<./channel.adoc#channel-wiretap,wiretap>>).
To configure a channel interceptor, you need a named channel.
NOTE: Starting with version 5.4, when gateway method return type is `void`, the framework populates a `replyChannel` header as a `nullChannel` bean reference if such a header is not provided explicitly.
This allows any possible reply from the downstream flow to be discarded, meeting the one-way gateway contract.
[[gateway-configuration-annotations]]
==== Gateway Configuration with Annotations and XML

View File

@@ -12,3 +12,11 @@ If you are interested in the changes and features that were introduced in earlie
If you are interested in more details, see the Issue Tracker tickets that were resolved as part of the 5.4 development process.
[[x5.4-new-components]]
=== New Components
[[x5.4-general]]
=== General Changes
The one-way messaging gateway (the `void` method return type) now sets a `nullChannel` explicitly into the `replyChannel` header to ignore any possible downstream replies.
See <<./gateway.adoc#gateway-default-reply-channel,Setting the Default Reply Channel>> for more information.