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