GH-3529: Add HTTP & WebFlux extractResponseBody (#3530)

* GH-3529: Add HTTP & WebFlux extractResponseBody

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

* Expose a convenient `extractResponseBody` option on the HTTP client components
to let end-user to decide if the body of `ResponseEntity` must be extracted (default)
or the whole `ResponseEntity` should be produced as a reply message payload
* Remove a deprecated since `5.3` `encode-uri` option
* Document the new feature
* Rework `webflux.adoc` chapter for the code snippet switcher

* * Fix language in Docs
* Mention a default value in JavaDocs
This commit is contained in:
Artem Bilan
2021-03-31 15:13:33 -04:00
committed by GitHub
parent 0549dbd102
commit 27e1f3a636
15 changed files with 331 additions and 347 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -90,7 +90,6 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encoding-mode");
}
return builder;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -83,6 +83,7 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel");
HttpAdapterParsingUtils.configureUriVariableExpressions(builder, parserContext, element);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "transfer-cookies");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-response-body");
return builder;
}
@@ -102,7 +103,6 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser {
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encoding-mode");
}
return builder;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 the original author or authors.
* Copyright 2017-2021 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.
@@ -25,6 +25,7 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.expression.Expression;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.integration.dsl.ComponentsRegistration;
import org.springframework.integration.dsl.MessageHandlerSpec;
import org.springframework.integration.expression.FunctionExpression;
@@ -67,19 +68,6 @@ public abstract class BaseHttpMessageHandlerSpec<S extends BaseHttpMessageHandle
return _this();
}
/**
* Specify whether the real URI should be encoded after <code>uriVariables</code>
* expanding and before send request via underlying implementation. The default value is <code>true</code>.
* @param encodeUri true if the URI should be encoded.
* @return the spec
* @deprecated since 5.3 in favor of {@link #encodingMode}
*/
@Deprecated
public S encodeUri(boolean encodeUri) {
this.target.setEncodeUri(encodeUri);
return _this();
}
/**
* Specify a {@link DefaultUriBuilderFactory.EncodingMode} for uri construction.
* @param encodingMode to use for uri construction.
@@ -314,6 +302,7 @@ public abstract class BaseHttpMessageHandlerSpec<S extends BaseHttpMessageHandle
/**
* Set to {@code true} if you wish {@code Set-Cookie} header in response to be
* transferred as {@code Cookie} header in subsequent interaction for a message.
* Defaults to false.
* @param transferCookies the transferCookies to set.
* @return the current Spec.
*/
@@ -322,6 +311,18 @@ public abstract class BaseHttpMessageHandlerSpec<S extends BaseHttpMessageHandle
return _this();
}
/**
* The flag to extract a body of the {@link ResponseEntity} for reply message payload.
* Defaults to true.
* @param extractResponseBody produce a reply message with a whole {@link ResponseEntity} or just its body.
* @return the current Spec.
* @since 5.5
*/
public S extractResponseBody(boolean extractResponseBody) {
this.target.setExtractResponseBody(extractResponseBody);
return _this();
}
@Override
public Map<Object, String> getComponentsToRegister() {
this.target.setUriVariableExpressions(this.uriVariableExpressions);

View File

@@ -101,6 +101,8 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
private boolean extractPayloadExplicitlySet = false;
private boolean extractResponseBody = true;
private Charset charset = StandardCharsets.UTF_8;
private boolean transferCookies = false;
@@ -114,22 +116,6 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
this.uriExpression = uriExpression;
}
/**
* Specify whether the real URI should be encoded after {@code uriVariables}
* expanding and before send request via {@link org.springframework.web.client.RestTemplate}.
* The default value is {@code true}.
* @param encodeUri true if the URI should be encoded.
* @see org.springframework.web.util.UriComponentsBuilder
* @deprecated since 5.3 in favor of {@link #setEncodingMode}
*/
@Deprecated
public void setEncodeUri(boolean encodeUri) {
setEncodingMode(
encodeUri
? DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES
: DefaultUriBuilderFactory.EncodingMode.NONE);
}
/**
* Set the encoding mode to use.
* By default this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES}.
@@ -202,9 +188,8 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
}
/**
* Specify the expected response type for the REST request
* otherwise the default response type is {@link ResponseEntity} and will
* be returned as a payload of the reply Message.
* Specify the expected response type for the REST request.
* Otherwise it is null and an empty {@link ResponseEntity} is returned from HTTP client.
* To take advantage of the HttpMessageConverters
* registered on this adapter, provide a different type).
* @param expectedResponseType The expected type.
@@ -259,8 +244,8 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
/**
* Set to true if you wish 'Set-Cookie' headers in responses to be
* transferred as 'Cookie' headers in subsequent interactions for
* a message.
* transferred as 'Cookie' headers in subsequent interactions for a message.
* Defaults to false.
* @param transferCookies the transferCookies to set.
*/
public void setTransferCookies(boolean transferCookies) {
@@ -278,6 +263,16 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
this.trustedSpel = trustedSpel;
}
/**
* The flag to extract a body of the {@link ResponseEntity} for reply message payload.
* Defaults to true.
* @param extractResponseBody produce a reply message with a whole {@link ResponseEntity} or just its body.
* @since 5.5
*/
public void setExtractResponseBody(boolean extractResponseBody) {
this.extractResponseBody = extractResponseBody;
}
@Override
public IntegrationPatternType getIntegrationPatternType() {
return this.expectReply ? super.getIntegrationPatternType() : IntegrationPatternType.outbound_channel_adapter;
@@ -329,7 +324,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
AbstractIntegrationMessageBuilder<?> replyBuilder;
MessageBuilderFactory messageBuilderFactory = getMessageBuilderFactory();
if (httpResponse.hasBody()) {
if (httpResponse.hasBody() && this.extractResponseBody) {
Object responseBody = httpResponse.getBody();
replyBuilder = (responseBody instanceof Message<?>)
? messageBuilderFactory.fromMessage((Message<?>) responseBody)

View File

@@ -536,6 +536,17 @@
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="extract-response-body" default="true">
<xsd:annotation>
<xsd:documentation>
Set to 'false' to return the whole 'ResponseEntity' in the reply message payload.
The default value is 'true'.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -841,16 +852,6 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="encode-uri" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
[DEPRECATED] When set to "false", the real URI won't be encoded before the request is sent.
This may be useful in some scenarios as it allows user control over the encoding, if needed,
for example by using the "url-expression". Default is "true".
Deprecated since 5.3 in favor of 'encoding-mode'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="encoding-mode" default="TEMPLATE_AND_VALUES">
<xsd:annotation>
<xsd:documentation>
@@ -892,7 +893,7 @@
<xsd:annotation>
<xsd:documentation>
The expected type to which the response body should be converted.
Default is 'org.springframework.http.ResponseEntity'.
Default is 'null' - no body expected.
This attribute cannot be provided if expected-response-type-expression has a value
</xsd:documentation>
<xsd:appinfo>

View File

@@ -36,7 +36,8 @@
charset="UTF-8"
order="77"
auto-startup="false"
transfer-cookies="true">
transfer-cookies="true"
extract-response-body="false">
<uri-variable name="foo" expression="headers.bar"/>
</outbound-gateway>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -17,16 +17,13 @@
package org.springframework.integration.http.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -48,8 +45,7 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.ResponseErrorHandler;
@@ -59,8 +55,7 @@ import org.springframework.web.client.ResponseErrorHandler;
* @author Artem Bilan
* @author Biju Kunjummen
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@SpringJUnitConfig
@DirtiesContext
public class HttpOutboundGatewayParserTests {
@@ -107,16 +102,17 @@ public class HttpOutboundGatewayParserTests {
assertThat(uriExpression.getValue()).isEqualTo("http://localhost/test1");
assertThat(TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString())
.isEqualTo(HttpMethod.POST.name());
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(Charset.forName("UTF-8"));
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.UTF_8);
assertThat(handlerAccessor.getPropertyValue("extractPayload")).isEqualTo(true);
assertThat(handlerAccessor.getPropertyValue("transferCookies")).isEqualTo(false);
}
@Test
@SuppressWarnings("unchecked")
public void fullConfig() throws Exception {
public void fullConfig() {
DirectFieldAccessor endpointAccessor = new DirectFieldAccessor(this.fullConfigEndpoint);
HttpRequestExecutingMessageHandler handler = (HttpRequestExecutingMessageHandler) endpointAccessor.getPropertyValue("handler");
HttpRequestExecutingMessageHandler handler =
(HttpRequestExecutingMessageHandler) endpointAccessor.getPropertyValue("handler");
MessageChannel requestChannel = (MessageChannel) new DirectFieldAccessor(
this.fullConfigEndpoint).getPropertyValue("inputChannel");
assertThat(requestChannel).isEqualTo(this.applicationContext.getBean("requests"));
@@ -139,7 +135,7 @@ public class HttpOutboundGatewayParserTests {
assertThat(uriExpression.getValue()).isEqualTo("http://localhost/test2");
assertThat(TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString())
.isEqualTo(HttpMethod.PUT.name());
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(Charset.forName("UTF-8"));
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.UTF_8);
assertThat(handlerAccessor.getPropertyValue("extractPayload")).isEqualTo(false);
Object requestFactoryBean = this.applicationContext.getBean("testRequestFactory");
assertThat(requestFactory).isEqualTo(requestFactoryBean);
@@ -161,6 +157,7 @@ public class HttpOutboundGatewayParserTests {
assertThat(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader2")).isTrue();
assertThat(mappedResponseHeaders[0]).isEqualTo("responseHeader");
assertThat(handlerAccessor.getPropertyValue("transferCookies")).isEqualTo(true);
assertThat(handlerAccessor.getPropertyValue("extractResponseBody")).isEqualTo(false);
}
@Test
@@ -182,7 +179,7 @@ public class HttpOutboundGatewayParserTests {
assertThat(expression.getExpressionString()).isEqualTo("'http://localhost/test1'");
assertThat(TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString())
.isEqualTo(HttpMethod.POST.name());
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(Charset.forName("UTF-8"));
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.UTF_8);
assertThat(handlerAccessor.getPropertyValue("extractPayload")).isEqualTo(true);
assertThat(handlerAccessor.getPropertyValue("transferCookies")).isEqualTo(false);
@@ -199,21 +196,17 @@ public class HttpOutboundGatewayParserTests {
public void withAdvice() {
HttpRequestExecutingMessageHandler handler = (HttpRequestExecutingMessageHandler) new DirectFieldAccessor(
this.withAdvice).getPropertyValue("handler");
handler.handleMessage(new GenericMessage<String>("foo"));
handler.handleMessage(new GenericMessage<>("foo"));
assertThat(adviceCalled).isEqualTo(1);
}
@Test
public void testInt2718FailForGatewayRequestChannelAttribute() {
try {
new ClassPathXmlApplicationContext("HttpOutboundGatewayWithinChainTests-fail-context.xml", this.getClass())
.close();
fail("Expected BeanDefinitionParsingException");
}
catch (BeansException e) {
assertThat(e instanceof BeanDefinitionParsingException).isTrue();
assertThat(e.getMessage().contains("'request-channel' attribute isn't allowed for a nested")).isTrue();
}
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() ->
new ClassPathXmlApplicationContext("HttpOutboundGatewayWithinChainTests-fail-context.xml",
getClass()))
.withMessageContaining("'request-channel' attribute isn't allowed for a nested");
}
@Test
@@ -225,13 +218,14 @@ public class HttpOutboundGatewayParserTests {
public static class StubErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
public boolean hasError(ClientHttpResponse response) {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
public void handleError(ClientHttpResponse response) {
}
}
public static class FooAdvice extends AbstractRequestHandlerAdvice {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2021 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.
@@ -363,6 +363,7 @@ public class HttpDslTests {
.handle(Http.outboundGateway("/service/internal?{params}")
.uriVariable("params", "payload")
.expectedResponseType(String.class)
.extractResponseBody(false)
.errorHandler(new HttpProxyResponseErrorHandler()),
e -> e.id("serviceInternalGateway"))
.get();

View File

@@ -581,6 +581,17 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-response-body" default="true">
<xsd:annotation>
<xsd:documentation>
Set to 'false' to return the whole 'ResponseEntity' in the reply message payload.
The default value is 'true'.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

View File

@@ -38,7 +38,8 @@
transfer-cookies="true"
reply-payload-to-flux="true"
body-extractor="bodyExtractor"
publisher-element-type-expression="headers.elementType">
publisher-element-type-expression="headers.elementType"
extract-response-body="false">
<uri-variable name="foo" expression="headers.bar"/>
</outbound-gateway>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 the original author or authors.
* Copyright 2017-2021 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,7 +18,6 @@ package org.springframework.integration.webflux.config;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@@ -79,7 +78,7 @@ public class WebFluxOutboundGatewayParserTests {
assertThat(uriExpression.getValue()).isEqualTo("http://localhost/test1");
assertThat(TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString())
.isEqualTo(HttpMethod.POST.name());
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(Charset.forName("UTF-8"));
assertThat(handlerAccessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.UTF_8);
assertThat(handlerAccessor.getPropertyValue("extractPayload")).isEqualTo(true);
assertThat(handlerAccessor.getPropertyValue("transferCookies")).isEqualTo(false);
assertThat(handlerAccessor.getPropertyValue("replyPayloadToFlux")).isEqualTo(false);
@@ -128,6 +127,8 @@ public class WebFluxOutboundGatewayParserTests {
assertThat(handlerAccessor.getPropertyValue("bodyExtractor")).isSameAs(this.bodyExtractor);
assertThat(handlerAccessor.getPropertyValue("publisherElementTypeExpression.expression"))
.isEqualTo("headers.elementType");
assertThat(handlerAccessor.getPropertyValue("extractResponseBody"))
.isEqualTo(false);
}
}

View File

@@ -25,6 +25,7 @@ import java.security.Principal;
import java.time.Duration;
import java.util.Collections;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -37,9 +38,11 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.QueueChannel;
@@ -172,11 +175,14 @@ public class WebFluxDslTests {
Message<?> receive = replyChannel.receive(10_000);
assertThat(receive).isNotNull();
assertThat(receive.getPayload()).isInstanceOf(Flux.class);
assertThat(receive).isNotNull()
.extracting(Message::getPayload)
.asInstanceOf(InstanceOfAssertFactories.type(ResponseEntity.class))
.extracting(HttpEntity<Flux<String>>::getBody)
.isInstanceOf(Flux.class);
@SuppressWarnings("unchecked")
Flux<String> response = (Flux<String>) receive.getPayload();
Flux<String> response = ((ResponseEntity<Flux<String>>) receive.getPayload()).getBody();
StepVerifier.create(response)
.expectNext("FOO", "BAR")
@@ -410,7 +416,8 @@ public class WebFluxDslTests {
.handle(WebFlux.outboundGateway("https://www.springsource.org/spring-integration")
.httpMethod(HttpMethod.GET)
.replyPayloadToFlux(true)
.expectedResponseType(String.class),
.expectedResponseType(String.class)
.extractResponseBody(false),
e -> e
.id("webFluxWithReplyPayloadToFlux")
.customizeMonoReply(

View File

@@ -8,8 +8,8 @@ See also <<./webflux.adoc#webflux,WebFlux Support>>.
You need to include this dependency into your project:
====
[source, xml, subs="normal", role="primary"]
.Maven
[source, xml, subs="normal"]
----
<dependency>
<groupId>org.springframework.integration</groupId>
@@ -17,9 +17,8 @@ You need to include this dependency into your project:
<version>{project-version}</version>
</dependency>
----
[source, groovy, subs="normal", role="secondary"]
.Gradle
[source, groovy, subs="normal"]
----
compile "org.springframework.integration:spring-integration-http:{project-version}"
----
@@ -223,6 +222,9 @@ The `expected-response-type` must be compatible with the (configured or default)
This can be an abstract class or even an interface (such as `java.io.Serializable` when you use Java serialization and `Content-Type: application/x-java-serialized-object`).
=====
Starting with version 5.5, the `HttpRequestExecutingMessageHandler` exposes an `extractResponseBody` flag (which is `true` by default) to return just the response body, or to return the whole `ResponseEntity` as the reply message payload, independently of the provided `expectedResponseType`.
If a body is not present in the `ResponseEntity`, this flag is ignored and the whole `ResponseEntity` is returned.
[[http-namespace]]
=== HTTP Namespace Support

View File

@@ -6,8 +6,8 @@ The WebFlux Spring Integration module (`spring-integration-webflux`) allows for
You need to include this dependency into your project:
====
[source, xml, subs="normal", role="primary"]
.Maven
[source, xml, subs="normal"]
----
<dependency>
<groupId>org.springframework.integration</groupId>
@@ -15,9 +15,8 @@ You need to include this dependency into your project:
<version>{project-version}</version>
</dependency>
----
[source, groovy, subs="normal", role="secondary"]
.Gradle
[source, groovy, subs="normal"]
----
compile "org.springframework.integration:spring-integration-webflux:{project-version}"
----
@@ -29,133 +28,6 @@ The WebFlux support consists of the following gateway implementations: `WebFluxI
The support is fully based on the Spring https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#spring-webflux[WebFlux] and https://projectreactor.io/[Project Reactor] foundations.
See <<./http.adoc#http,HTTP Support>> for more information, since many options are shared between reactive and regular HTTP components.
[[webflux-inbound]]
=== WebFlux Inbound Components
Starting with version 5.0, the `WebFluxInboundEndpoint` implementation of `WebHandler` is provided.
This component is similar to the MVC-based `HttpRequestHandlingEndpointSupport`, with which it shares some common options through the newly extracted `BaseHttpInboundEndpoint`.
It is used in the Spring WebFlux reactive environment (instead of MVC).
The following example shows a simple implementation of a WebFlux endpoint:
====
[source,java]
----
@Configuration
@EnableWebFlux
@EnableIntegration
public class ReactiveHttpConfiguration {
@Bean
public WebFluxInboundEndpoint simpleInboundEndpoint() {
WebFluxInboundEndpoint endpoint = new WebFluxInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/test");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("serviceChannel");
return endpoint;
}
@ServiceActivator(inputChannel = "serviceChannel")
String service() {
return "It works!";
}
}
----
====
The configuration is similar to the `HttpRequestHandlingEndpointSupport` (mentioned prior to the example), except that we use `@EnableWebFlux` to add the WebFlux infrastructure to our integration application.
Also, the `WebFluxInboundEndpoint` performs `sendAndReceive` operations to the downstream flow by using back-pressure, on-demand based capabilities, provided by the reactive HTTP server implementation.
NOTE: The reply part is non-blocking as well and is based on the internal `FutureReplyChannel`, which is flat-mapped to a reply `Mono` for on-demand resolution.
You can configure the `WebFluxInboundEndpoint` with a custom `ServerCodecConfigurer`, a `RequestedContentTypeResolver`, and even a `ReactiveAdapterRegistry`.
The latter provides a mechanism you can use to return a reply as any reactive type: Reactor `Flux`, RxJava `Observable`, `Flowable`, and others.
This way, we can implement https://en.wikipedia.org/wiki/Server-sent_events[Server Sent Events] scenarios with Spring Integration components, as the following example shows:
====
[source,java]
----
@Bean
public IntegrationFlow sseFlow() {
return IntegrationFlows
.from(WebFlux.inboundGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
.handle((p, h) -> Flux.just("foo", "bar", "baz"))
.get();
}
----
====
See <<./http.adoc#http-request-mapping,Request Mapping Support>> and <<./http.adoc#http-cors,Cross-origin Resource Sharing (CORS) Support>> for more possible configuration options.
When the request body is empty or `payloadExpression` returns `null`, the request params (`MultiValueMap<String, String>`) is used for a `payload` of the target message to process.
[[webflux-validation]]
==== Payload Validation
Starting with version 5.2, the `WebFluxInboundEndpoint` can be configured with a `Validator`.
Unlike the MVC validation in the <<./http.adoc#http-validation,HTTP Support>>, it is used to validate elements in the `Publisher` to which a request has been converted by the `HttpMessageReader`, before performing a fallback and `payloadExpression` functions.
The Framework can't assume how complex the `Publisher` object can be after building the final payload.
If there is a requirements to restrict validation visibility for exactly final payload (or its `Publisher` elements), the validation should go downstream instead of WebFlux endpoint.
See more information in the Spring WebFlux https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web-reactive.html#webflux-fn-handler-validation[documentation].
An invalid payload is rejected with an `IntegrationWebExchangeBindException` (a `WebExchangeBindException` extension), containing all the validation `Errors`.
See more in Spring Framework https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation[Reference Manual] about validation.
[[webflux-outbound]]
=== WebFlux Outbound Components
The `WebFluxRequestExecutingMessageHandler` (starting with version 5.0) implementation is similar to `HttpRequestExecutingMessageHandler`.
It uses a `WebClient` from the Spring Framework WebFlux module.
To configure it, define a bean similar to the following:
====
[source,xml]
----
<bean id="httpReactiveOutbound"
class="org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler">
<constructor-arg value="http://localhost:8080/example" />
<property name="outputChannel" ref="responseChannel" />
</bean>
----
====
You can configure a `WebClient` instance to use, as the following example shows:
====
[source,xml]
----
<beans:bean id="webClient" class="org.springframework.web.reactive.function.client.WebClient"
factory-method="create"/>
<bean id="httpReactiveOutbound"
class="org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler">
<constructor-arg value="http://localhost:8080/example" />
<constructor-arg re="webClient" />
<property name="outputChannel" ref="responseChannel" />
</bean>
----
====
The `WebClient` `exchange()` operation returns a `Mono<ClientResponse>`, which is mapped (by 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 a `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 `replyPayloadToFlux` 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.
Afterwards, you can use a <<./splitter.adoc#splitter,splitter>> to iterate over this `Flux` in a reactive manner.
In addition a `BodyExtractor<?, ClientHttpResponse>` can be injected into the `WebFluxRequestExecutingMessageHandler` instead of the `expectedResponseType` and `replyPayloadToFlux` properties.
It can be used for low-level access to the `ClientHttpResponse` and more control over body and HTTP headers conversion.
Spring Integration provides `ClientHttpResponseBodyExtractor` as a identity function to produce (downstream) the whole `ClientHttpResponse` and any other possible custom logic.
Starting with version 5.2, the `WebFluxRequestExecutingMessageHandler` supports reactive `Publisher`, `Resource`, and `MultiValueMap` types as the request message payload.
A respective `BodyInserter` is used internally to be populated into the `WebClient.RequestBodySpec`.
When the payload is a reactive `Publisher`, a configured `publisherElementType` or `publisherElementTypeExpression` can be used to determine a type for the publisher's element type.
The expression must be resolved to a `Class<?>`, `String` which is resolved to the target `Class<?>` or `ParameterizedTypeReference`.
See <<./http.adoc#http-outbound,HTTP Outbound Components>> for more possible configuration options.
[[webflux-namespace]]
=== WebFlux Namespace Support
@@ -182,61 +54,221 @@ To include it in your configuration, add the following namespace declaration in
----
====
==== Inbound
[[webflux-inbound]]
=== WebFlux Inbound Components
To configure Spring Integration WebFlux with XML, you need to use appropriate components from the `int-webflux` namespace: `inbound-channel-adapter` or `inbound-gateway`, corresponding to request and response requirements, respectively.
The following example shows how to configure both an inbound channel adapter and an inbound gateway:
Starting with version 5.0, the `WebFluxInboundEndpoint` implementation of `WebHandler` is provided.
This component is similar to the MVC-based `HttpRequestHandlingEndpointSupport`, with which it shares some common options through the newly extracted `BaseHttpInboundEndpoint`.
It is used in the Spring WebFlux reactive environment (instead of MVC).
The following example shows a simple implementation of a WebFlux endpoint:
====
[source,xml]
[source, java, role="primary"]
.Java DSL
----
<inbound-channel-adapter id="reactiveFullConfig" channel="requests"
path="test1"
auto-startup="false"
phase="101"
request-payload-type="byte[]"
error-channel="errorChannel"
payload-expression="payload"
supported-methods="PUT"
status-code-expression="'202'"
header-mapper="headerMapper"
codec-configurer="codecConfigurer"
reactive-adapter-registry="reactiveAdapterRegistry"
requested-content-type-resolver="requestedContentTypeResolver">
<request-mapping headers="foo"/>
<cross-origin origin="foo"
method="PUT"/>
<header name="foo" expression="'foo'"/>
</inbound-channel-adapter>
@Bean
public IntegrationFlow inboundChannelAdapterFlow() {
return IntegrationFlows
.from(WebFlux.inboundChannelAdapter("/reactivePost")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(ResolvableType.forClassWithGenerics(Flux.class, String.class))
.statusCodeFunction(m -> HttpStatus.ACCEPTED))
.channel(c -> c.queue("storeChannel"))
.get();
}
----
[source, kotlin, role="secondary"]
.Kotlin DSL
----
@Bean
fun inboundChannelAdapterFlow() =
integrationFlow(
WebFlux.inboundChannelAdapter("/reactivePost")
.apply {
requestMapping { m -> m.methods(HttpMethod.POST) }
requestPayloadType(ResolvableType.forClassWithGenerics(Flux::class.java, String::class.java))
statusCodeFunction { m -> HttpStatus.ACCEPTED }
})
{
channel { queue("storeChannel") }
}
----
[source, java, role="secondary"]
.Java
----
@Configuration
@EnableWebFlux
@EnableIntegration
public class ReactiveHttpConfiguration {
<inbound-gateway id="reactiveFullConfig" request-channel="requests"
path="test1"
auto-startup="false"
phase="101"
request-payload-type="byte[]"
error-channel="errorChannel"
payload-expression="payload"
supported-methods="PUT"
reply-timeout-status-code-expression="'504'"
header-mapper="headerMapper"
codec-configurer="codecConfigurer"
reactive-adapter-registry="reactiveAdapterRegistry"
requested-content-type-resolver="requestedContentTypeResolver">
<request-mapping headers="foo"/>
<cross-origin origin="foo"
method="PUT"/>
<header name="foo" expression="'foo'"/>
</inbound-gateway>
@Bean
public WebFluxInboundEndpoint simpleInboundEndpoint() {
WebFluxInboundEndpoint endpoint = new WebFluxInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/test");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("serviceChannel");
return endpoint;
}
@ServiceActivator(inputChannel = "serviceChannel")
String service() {
return "It works!";
}
}
----
[source, xml, role="secondary"]
.XML
----
<int-webflux:inbound-gateway request-channel="requests" path="/sse">
<int-webflux:request-mapping produces="text/event-stream"/>
</int-webflux:inbound-gateway>
----
====
==== Outbound
The configuration is similar to the `HttpRequestHandlingEndpointSupport` (mentioned prior to the example), except that we use `@EnableWebFlux` to add the WebFlux infrastructure to our integration application.
Also, the `WebFluxInboundEndpoint` performs `sendAndReceive` operations to the downstream flow by using back-pressure, on-demand based capabilities, provided by the reactive HTTP server implementation.
If you want to execute the HTTP request in a reactive, non-blocking way, you can use the `outbound-gateway` or `outbound-channel-adapter`.
The following example shows how to configure both an outbound gateway and an outbound channel adapter:
NOTE: The reply part is non-blocking as well and is based on the internal `FutureReplyChannel`, which is flat-mapped to a reply `Mono` for on-demand resolution.
You can configure the `WebFluxInboundEndpoint` with a custom `ServerCodecConfigurer`, a `RequestedContentTypeResolver`, and even a `ReactiveAdapterRegistry`.
The latter provides a mechanism you can use to return a reply as any reactive type: Reactor `Flux`, RxJava `Observable`, `Flowable`, and others.
This way, we can implement https://en.wikipedia.org/wiki/Server-sent_events[Server Sent Events] scenarios with Spring Integration components, as the following example shows:
====
[source,xml]
[source, java, role="primary"]
.Java DSL
----
@Bean
public IntegrationFlow sseFlow() {
return IntegrationFlows
.from(WebFlux.inboundGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
.handle((p, h) -> Flux.just("foo", "bar", "baz"))
.get();
}
----
[source, kotlin, role="secondary"]
.Kotlin DSL
----
@Bean
fun sseFlow() =
integrationFlow(
WebFlux.inboundGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
{
handle { (p, h) -> Flux.just("foo", "bar", "baz") }
}
----
[source, java, role="secondary"]
.Java
----
@Bean
public WebFluxInboundEndpoint webfluxInboundGateway() {
WebFluxInboundEndpoint endpoint = new WebFluxInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/sse");
requestMapping.setProduces(MediaType.TEXT_EVENT_STREAM_VALUE);
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("requests");
return endpoint;
}
----
[source, xml, role="secondary"]
.XML
----
<int-webflux:inbound-channel-adapter id="reactiveFullConfig" channel="requests"
path="test1"
auto-startup="false"
phase="101"
request-payload-type="byte[]"
error-channel="errorChannel"
payload-expression="payload"
supported-methods="PUT"
status-code-expression="'202'"
header-mapper="headerMapper"
codec-configurer="codecConfigurer"
reactive-adapter-registry="reactiveAdapterRegistry"
requested-content-type-resolver="requestedContentTypeResolver">
<int-webflux:request-mapping headers="foo"/>
<int-webflux:cross-origin origin="foo" method="PUT"/>
<int-webflux:header name="foo" expression="'foo'"/>
</int-webflux:inbound-channel-adapter>
----
====
See <<./http.adoc#http-request-mapping,Request Mapping Support>> and <<./http.adoc#http-cors,Cross-origin Resource Sharing (CORS) Support>> for more possible configuration options.
When the request body is empty or `payloadExpression` returns `null`, the request params (`MultiValueMap<String, String>`) is used for a `payload` of the target message to process.
[[webflux-validation]]
==== Payload Validation
Starting with version 5.2, the `WebFluxInboundEndpoint` can be configured with a `Validator`.
Unlike the MVC validation in the <<./http.adoc#http-validation,HTTP Support>>, it is used to validate elements in the `Publisher` to which a request has been converted by the `HttpMessageReader`, before performing a fallback and `payloadExpression` functions.
The Framework can't assume how complex the `Publisher` object can be after building the final payload.
If there is a requirements to restrict validation visibility for exactly final payload (or its `Publisher` elements), the validation should go downstream instead of WebFlux endpoint.
See more information in the Spring WebFlux https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web-reactive.html#webflux-fn-handler-validation[documentation].
An invalid payload is rejected with an `IntegrationWebExchangeBindException` (a `WebExchangeBindException` extension), containing all the validation `Errors`.
See more in Spring Framework https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation[Reference Manual] about validation.
[[webflux-outbound]]
=== WebFlux Outbound Components
The `WebFluxRequestExecutingMessageHandler` (starting with version 5.0) implementation is similar to `HttpRequestExecutingMessageHandler`.
It uses a `WebClient` from the Spring Framework WebFlux module.
To configure it, define a bean similar to the following:
====
[source, java, role="primary"]
.Java DSL
----
@Bean
public IntegrationFlow outboundReactive() {
return f -> f
.handle(WebFlux.<MultiValueMap<String, String>>outboundGateway(m ->
UriComponentsBuilder.fromUriString("http://localhost:8080/foo")
.queryParams(m.getPayload())
.build()
.toUri())
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class));
}
----
[source, kotlin, role="secondary"]
.Kotlin DSL
----
@Bean
fun outboundReactive() =
integrationFlow {
handle(
WebFlux.outboundGateway<MultiValueMap<String, String>>({ m ->
UriComponentsBuilder.fromUriString("http://localhost:8080/foo")
.queryParams(m.getPayload())
.build()
.toUri()
})
.httpMethod(HttpMethod.GET)
.expectedResponseType(String::class.java)
)
}
----
[source, java, role="secondary"]
.Java
----
@ServiceActivator(inputChannel = "reactiveHttpOutRequest")
@Bean
public WebFluxRequestExecutingMessageHandler reactiveOutbound(WebClient client) {
WebFluxRequestExecutingMessageHandler handler =
new WebFluxRequestExecutingMessageHandler("http://localhost:8080/foo", client);
handler.setHttpMethod(HttpMethod.POST);
handler.setExpectedResponseType(String.class);
return handler;
}
----
[source, xml, role="secondary"]
.XML
----
<int-webflux:outbound-gateway id="reactiveExample1"
request-channel="requests"
@@ -257,93 +289,30 @@ The following example shows how to configure both an outbound gateway and an out
expected-response-type="java.lang.String"
order="3"
auto-startup="false"/>
----
====
[[webflux-java-config]]
=== Configuring WebFlux Endpoints with Java
The `WebClient` `exchange()` operation returns a `Mono<ClientResponse>`, which is mapped (by 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 a `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 `replyPayloadToFlux` 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.
Afterwards, you can use a <<./splitter.adoc#splitter,splitter>> to iterate over this `Flux` in a reactive manner.
The following example shows how to configure a WebFlux inbound endpoint with Java:
In addition a `BodyExtractor<?, ClientHttpResponse>` can be injected into the `WebFluxRequestExecutingMessageHandler` instead of the `expectedResponseType` and `replyPayloadToFlux` properties.
It can be used for low-level access to the `ClientHttpResponse` and more control over body and HTTP headers conversion.
Spring Integration provides `ClientHttpResponseBodyExtractor` as a identity function to produce (downstream) the whole `ClientHttpResponse` and any other possible custom logic.
====
[source, java]
----
@Bean
public WebFluxInboundEndpoint jsonInboundEndpoint() {
WebFluxInboundEndpoint endpoint = new WebFluxInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/persons");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannel(fluxResultChannel());
return endpoint;
}
Starting with version 5.2, the `WebFluxRequestExecutingMessageHandler` supports reactive `Publisher`, `Resource`, and `MultiValueMap` types as the request message payload.
A respective `BodyInserter` is used internally to be populated into the `WebClient.RequestBodySpec`.
When the payload is a reactive `Publisher`, a configured `publisherElementType` or `publisherElementTypeExpression` can be used to determine a type for the publisher's element type.
The expression must be resolved to a `Class<?>`, `String` which is resolved to the target `Class<?>` or `ParameterizedTypeReference`.
@Bean
public MessageChannel fluxResultChannel() {
return new FluxMessageChannel();
}
Starting with version 5.5, the `WebFluxRequestExecutingMessageHandler` exposes an `extractResponseBody` flag (which is `true` by default) to return just the response body, or to return the whole `ResponseEntity` as the reply message payload, independently of the provided `expectedResponseType` or `replyPayloadToFlux`.
If a body is not present in the `ResponseEntity`, this flag is ignored and the whole `ResponseEntity` is returned.
@ServiceActivator(inputChannel = "fluxResultChannel")
Flux<Person> getPersons() {
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
}
----
====
The following example shows how to configure a WebFlux inbound gateway with the Java DSL:
====
[source, java]
----
@Bean
public IntegrationFlow inboundChannelAdapterFlow() {
return IntegrationFlows
.from(WebFlux.inboundChannelAdapter("/reactivePost")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(ResolvableType.forClassWithGenerics(Flux.class, String.class))
.statusCodeFunction(m -> HttpStatus.ACCEPTED))
.channel(c -> c.queue("storeChannel"))
.get();
}
----
====
The following example shows how to configure a WebFlux outbound gateway with Java:
====
[source, java]
----
@ServiceActivator(inputChannel = "reactiveHttpOutRequest")
@Bean
public WebFluxRequestExecutingMessageHandler reactiveOutbound(WebClient client) {
WebFluxRequestExecutingMessageHandler handler =
new WebFluxRequestExecutingMessageHandler("http://localhost:8080/foo", client);
handler.setHttpMethod(HttpMethod.POST);
handler.setExpectedResponseType(String.class);
return handler;
}
----
====
The following example shows how to configure a WebFlux outbound gateway with the Java DSL:
====
[source, java]
----
@Bean
public IntegrationFlow outboundReactive() {
return f -> f
.handle(WebFlux.<MultiValueMap<String, String>>outboundGateway(m ->
UriComponentsBuilder.fromUriString("http://localhost:8080/foo")
.queryParams(m.getPayload())
.build()
.toUri())
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class));
}
----
====
See <<./http.adoc#http-outbound,HTTP Outbound Components>> for more possible configuration options.
[[webflux-header-mapping]]
=== WebFlux Header Mappings

View File

@@ -63,7 +63,8 @@ See <<./redis.adoc#redis,Redis Support>> for more information.
==== HTTP Changes
The `HttpRequestExecutingMessageHandler` doesn't fallback to the `application/x-java-serialized-object` content type any more and lets the `RestTemplate` make the final decision for the request body conversion based on the `HttpMessageConverter` provided.
It also has now an `extractResponseBody` flag (which is `true` by default) to return just the response body, or to return the whole `ResponseEntity` as the reply message payload, independently of the provided `expectedResponseType`.
Same option is presented for the `WebFluxRequestExecutingMessageHandler`, too.
See <<./http.adoc#http,HTTP Support>> for more information.
[[x5.5-file]]