From bd872846fb470ebc32d0d664dac82d1daa7599bf Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 14 Dec 2017 14:21:01 -0500 Subject: [PATCH] GH-2300: Add Flux support in WebFluxRequestExecMH Fixes: spring-projects/spring-integration#2300 To allow to consume a streaming HTTP response downstream expose `replyToFlux` option on the `WebFluxRequestExecutingMessageHandler`. This way the body of the HTTP response can be converted now to the `Flux` for subsequent output message. The option is `false` by default; can be changed to `true` in the `5.1` --- .../config/WebFluxOutboundGatewayParser.java | 2 + ...WebFluxRequestExecutingMessageHandler.java | 51 +++++++++++++++-- .../config/spring-integration-webflux-5.0.xsd | 23 +++++++- ...FluxOutboundGatewayParserTests-context.xml | 29 +++++----- .../WebFluxOutboundGatewayParserTests.java | 2 + ...uxRequestExecutingMessageHandlerTests.java | 56 ++++++++++++++++++- src/reference/asciidoc/webflux.adoc | 9 ++- 7 files changed, 144 insertions(+), 28 deletions(-) diff --git a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParser.java b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParser.java index 0da8b66a1b..ea8ec03797 100644 --- a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParser.java +++ b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParser.java @@ -21,6 +21,7 @@ import org.w3c.dom.Element; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.http.config.HttpOutboundGatewayParser; import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler; import org.springframework.util.StringUtils; @@ -46,6 +47,7 @@ public class WebFluxOutboundGatewayParser extends HttpOutboundGatewayParser { .addIndexedArgumentValue(1, new RuntimeBeanReference(webClientRef)); } + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-to-flux"); return builder; } diff --git a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandler.java b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandler.java index f181cbb3f3..29e800000c 100644 --- a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandler.java +++ b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandler.java @@ -29,6 +29,7 @@ import org.springframework.expression.common.LiteralExpression; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.ResponseEntity; import org.springframework.integration.expression.ValueExpression; import org.springframework.integration.http.outbound.AbstractHttpRequestExecutingMessageHandler; @@ -36,12 +37,14 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.util.Assert; import org.springframework.util.MimeType; +import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -59,6 +62,8 @@ public class WebFluxRequestExecutingMessageHandler extends AbstractHttpRequestEx private final WebClient webClient; + private boolean replyToFlux; + /** * Create a handler that will send requests to the provided URI. * @param uri The URI. @@ -110,6 +115,20 @@ public class WebFluxRequestExecutingMessageHandler extends AbstractHttpRequestEx this.setAsync(true); } + /** + * The boolean flag to identify if the reply payload should be as a {@link Flux} from the response body + * or as resolved value from the {@link Mono} of the response body. + * Defaults to {@code false} - simple value is pushed downstream. + * Makes sense when {@code expectedResponseType} is configured. + * @param replyToFlux represent reply payload as a {@link Flux} or as a value from the {@link Mono}. + * @since 5.0.1 + * @see #setExpectedResponseType(Class) + * @see #setExpectedResponseTypeExpression(Expression) + */ + public void setReplyToFlux(boolean replyToFlux) { + this.replyToFlux = replyToFlux; + } + @Override public String getComponentType() { return (isExpectReply() ? "webflux:outbound-gateway" : "webflux:outbound-channel-adapter"); @@ -169,13 +188,35 @@ public class WebFluxRequestExecutingMessageHandler extends AbstractHttpRequestEx ResponseEntity.status(response.statusCode()) .headers(response.headers().asHttpHeaders()); - Mono bodyMono = Mono.empty(); + Mono bodyMono; - if (expectedResponseType instanceof ParameterizedTypeReference) { - bodyMono = response.body(BodyExtractors.toMono((ParameterizedTypeReference) expectedResponseType)); + if (expectedResponseType != null) { + if (this.replyToFlux) { + BodyExtractor, ReactiveHttpInputMessage> extractor; + if (expectedResponseType instanceof ParameterizedTypeReference) { + extractor = BodyExtractors.toFlux( + (ParameterizedTypeReference) expectedResponseType); + } + else { + extractor = BodyExtractors.toFlux((Class) expectedResponseType); + } + Flux flux = response.body(extractor); + bodyMono = Mono.just(flux); + } + else { + BodyExtractor, ReactiveHttpInputMessage> extractor; + if (expectedResponseType instanceof ParameterizedTypeReference) { + extractor = BodyExtractors.toMono( + (ParameterizedTypeReference) expectedResponseType); + } + else { + extractor = BodyExtractors.toMono((Class) expectedResponseType); + } + bodyMono = response.body(extractor); + } } - else if (expectedResponseType != null) { - bodyMono = response.body(BodyExtractors.toMono((Class) expectedResponseType)); + else { + bodyMono = Mono.empty(); } return bodyMono diff --git a/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.0.xsd b/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.0.xsd index 4397635b54..b9f8660cfe 100644 --- a/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.0.xsd +++ b/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.0.xsd @@ -10,7 +10,7 @@ + schemaLocation="http://www.springframework.org/schema/integration/spring-integration-5.0.xsd"/> @@ -82,7 +82,8 @@ - + Specify an expression for URI variable placeholder within 'url'. @@ -126,7 +127,7 @@ - + + + + @@ -173,6 +177,19 @@ + + + + When set to "true", the response is converted to the Flux which is sent as a reply + message payload. + The downstream flow might have a splitter to walk through that Flux. + Default is "false". + + + + + + diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests-context.xml b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests-context.xml index 3a4b996022..62427132ad 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests-context.xml +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests-context.xml @@ -20,22 +20,23 @@ + web-client="webClient"/> + url="http://localhost/test2" + http-method="PUT" + request-channel="requests" + reply-timeout="1234" + extract-request-payload="false" + expected-response-type="java.lang.String" + mapped-request-headers="requestHeader1, requestHeader2" + mapped-response-headers="responseHeader" + reply-channel="replies" + charset="UTF-8" + order="77" + auto-startup="false" + transfer-cookies="true" + reply-to-flux="true"> diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests.java b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests.java index 6995a6abcf..3f4276fbca 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests.java +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParserTests.java @@ -82,6 +82,7 @@ public class WebFluxOutboundGatewayParserTests { assertEquals(Charset.forName("UTF-8"), handlerAccessor.getPropertyValue("charset")); assertEquals(true, handlerAccessor.getPropertyValue("extractPayload")); assertEquals(false, handlerAccessor.getPropertyValue("transferCookies")); + assertEquals(false, handlerAccessor.getPropertyValue("replyToFlux")); } @Test @@ -123,6 +124,7 @@ public class WebFluxOutboundGatewayParserTests { assertTrue(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader2")); assertEquals("responseHeader", mappedResponseHeaders[0]); assertEquals(true, handlerAccessor.getPropertyValue("transferCookies")); + assertEquals(true, handlerAccessor.getPropertyValue("replyToFlux")); } } diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java index 9c899d05e9..df7807d86b 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java @@ -28,7 +28,10 @@ import java.util.List; import org.junit.Test; import org.reactivestreams.Subscriber; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.integration.channel.FluxMessageChannel; import org.springframework.integration.channel.QueueChannel; @@ -42,6 +45,7 @@ import org.springframework.test.web.reactive.server.HttpHandlerConnector; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -54,7 +58,7 @@ import reactor.test.StepVerifier; public class WebFluxRequestExecutingMessageHandlerTests { @Test - public void testReactiveReturn() throws Throwable { + public void testReactiveReturn() { ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> { response.setStatusCode(HttpStatus.OK); @@ -84,7 +88,7 @@ public class WebFluxRequestExecutingMessageHandlerTests { } @Test - public void testReactiveErrorOneWay() throws Throwable { + public void testReactiveErrorOneWay() { ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> { response.setStatusCode(HttpStatus.UNAUTHORIZED); @@ -114,7 +118,7 @@ public class WebFluxRequestExecutingMessageHandlerTests { } @Test - public void testReactiveConnectErrorOneWay() throws Throwable { + public void testReactiveConnectErrorOneWay() { ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> { throw new RuntimeException("Intentional connection error"); @@ -182,4 +186,50 @@ public class WebFluxRequestExecutingMessageHandlerTests { assertNull(replyMessage); } + @Test + @SuppressWarnings("unchecked") + public void testFluxReply() { + ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> { + response.setStatusCode(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.TEXT_PLAIN); + + DataBufferFactory bufferFactory = response.bufferFactory(); + + Flux data = + Flux.just(bufferFactory.wrap("foo".getBytes()), + bufferFactory.wrap("bar".getBytes()), + bufferFactory.wrap("baz".getBytes())); + + return response.writeWith(data) + .then(Mono.defer(response::setComplete)); + }); + + WebClient webClient = WebClient.builder() + .clientConnector(httpConnector) + .build(); + + String destinationUri = "http://www.springsource.org/spring-integration"; + WebFluxRequestExecutingMessageHandler reactiveHandler = + new WebFluxRequestExecutingMessageHandler(destinationUri, webClient); + + QueueChannel replyChannel = new QueueChannel(); + reactiveHandler.setOutputChannel(replyChannel); + reactiveHandler.setExpectedResponseType(String.class); + reactiveHandler.setReplyToFlux(true); + + reactiveHandler.handleMessage(MessageBuilder.withPayload("hello, world").build()); + + Message receive = replyChannel.receive(10_000); + + assertNotNull(receive); + + assertThat(receive.getPayload(), instanceOf(Flux.class)); + + Flux flux = (Flux) receive.getPayload(); + + StepVerifier.create(flux) + .expectNext("foo", "bar", "baz") + .verifyComplete(); + } + } diff --git a/src/reference/asciidoc/webflux.adoc b/src/reference/asciidoc/webflux.adoc index 2ee3a055e2..d85027441c 100644 --- a/src/reference/asciidoc/webflux.adoc +++ b/src/reference/asciidoc/webflux.adoc @@ -95,10 +95,13 @@ You can configure a `WebClient` instance to use: ---- -The `WebClient` `exchange()` operation returns a `Mono` which is mapped to the `AbstractIntegrationMessageBuilder` reactive support (using `Mono.map()`) as the output from the `WebFluxRequestExecutingMessageHandler`. +The `WebClient` `exchange()` operation returns a `Mono` which is mapped (using several `Mono.map()` steps) to an `AbstractIntegrationMessageBuilder` as the output from the `WebFluxRequestExecutingMessageHandler`. Together with the `ReactiveChannel` as an `outputChannel`, the `Mono` evaluation is deferred until a downstream subscription is made. Otherwise, it is treated as an `async` mode and the `Mono` response is adapted to an `SettableListenableFuture` for an asynchronous reply from the `WebFluxRequestExecutingMessageHandler`. - +The target payload of the output message depends on the `WebFluxRequestExecutingMessageHandler` configuration. +The `setExpectedResponseType(Class)` or `setExpectedResponseTypeExpression(Expression)` identifies the target type of the response body element conversion. +If `replyToFlux` is set to `true`, the response body is converted to a `Flux` with the provided `expectedResponseType` for each element and this `Flux` is sent as the payload downstream. +A <> afterwards can be used to iterate over this `Flux` in a reactive manner. Also see <> for more possible configuration options. @@ -237,4 +240,4 @@ public IntegrationFlow outboundReactive() { === WebFlux Header Mappings Since WebFlux components are fully based on the HTTP protocol there is no difference in the HTTP headers mapping. -See <> for more possible options and components to use for mapping headers. \ No newline at end of file +See <> for more possible options and components to use for mapping headers.