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();