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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user