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`
This commit is contained in:
committed by
Gary Russell
parent
21bf69b7f8
commit
bd872846fb
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<? extends Flux<?>, 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<? extends Mono<?>, 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
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
|
||||
<xsd:import namespace="http://www.springframework.org/schema/tool"/>
|
||||
<xsd:import namespace="http://www.springframework.org/schema/integration"
|
||||
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-5.0.xsd" />
|
||||
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-5.0.xsd"/>
|
||||
<xsd:import namespace="http://www.springframework.org/schema/integration/http"
|
||||
schemaLocation="http://www.springframework.org/schema/integration/http/spring-integration-http-5.0.xsd"/>
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="int-http:gatewayType">
|
||||
<xsd:choice minOccurs="0" maxOccurs="3">
|
||||
<xsd:element name="uri-variable" type="int-http:uriVariableType" minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="uri-variable" type="int-http:uriVariableType" minOccurs="0"
|
||||
maxOccurs="unbounded">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specify an expression for URI variable placeholder within 'url'.
|
||||
@@ -126,7 +127,7 @@
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="transfer-cookies" type="xsd:string" default="false">
|
||||
<xsd:attribute name="transfer-cookies" default="false">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
When set to "true", if a response contains a 'Set-Cookie' header, it will be mapped to a 'Cookie' header.
|
||||
@@ -134,6 +135,9 @@
|
||||
supplied by the server. Default is "false".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:simpleType>
|
||||
<xsd:union memberTypes="xsd:boolean xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="reply-timeout" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
@@ -173,6 +177,19 @@
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="reply-to-flux" default="false">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
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".
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:simpleType>
|
||||
<xsd:union memberTypes="xsd:boolean xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
</xsd:attribute>
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
|
||||
@@ -20,22 +20,23 @@
|
||||
</si:channel>
|
||||
|
||||
<outbound-gateway id="reactiveMinimalConfig" url="http://localhost/test1" request-channel="requests"
|
||||
web-client="webClient"/>
|
||||
web-client="webClient"/>
|
||||
|
||||
<outbound-gateway id="reactiveFullConfig"
|
||||
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">
|
||||
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">
|
||||
<uri-variable name="foo" expression="headers.bar"/>
|
||||
</outbound-gateway>
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<DataBuffer> 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<String> flux = (Flux<String>) receive.getPayload();
|
||||
|
||||
StepVerifier.create(flux)
|
||||
.expectNext("foo", "bar", "baz")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -95,10 +95,13 @@ You can configure a `WebClient` instance to use:
|
||||
</bean>
|
||||
----
|
||||
|
||||
The `WebClient` `exchange()` operation returns a `Mono<ClientResponse>` which is mapped to the `AbstractIntegrationMessageBuilder` reactive support (using `Mono.map()`) as the output from the `WebFluxRequestExecutingMessageHandler`.
|
||||
The `WebClient` `exchange()` operation returns a `Mono<ClientResponse>` 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<ClientResponse>` 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 <<splitter,splitter>> afterwards can be used to iterate over this `Flux` in a reactive manner.
|
||||
|
||||
Also see <<http-outbound>> 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 <<http-header-mapping>> for more possible options and components to use for mapping headers.
|
||||
See <<http-header-mapping>> for more possible options and components to use for mapping headers.
|
||||
|
||||
Reference in New Issue
Block a user