Add Validation to HTTP Inbound (#2978)

* Add Validation to HTTP Inbound

* Pull a validation functionality from `WebFluxInboundEndpoint` to its
super class `BaseHttpInboundEndpoint` making a validation available for
the `HttpRequestHandlingEndpointSupport` as well
* Do the same for the `validator()` option in DSL for the
`HttpInboundEndpointSupportSpec`
* Add `validator` XML attribute for both HTTP and WebFlux inbound
endpoint XSDs
* Test parsers for a new `validator` option
* Document validation in the `http.adoc`
* Apply some polishing in the `http.adoc`, as well as in the
`HttpRequestHandlingEndpointSupport` JavaDocs
* Clarify in `webflux.adoc` that validation is applied for the
`Publisher` items before the payload is finally built for the message to
send.
* Add WebFlux into the table of endpoints in the `endpoint-summary.adoc`

* * Remove unused imports
* Fix `SimpleMessageListenerContainerSpec` for deprecated `txSize` option
This commit is contained in:
Artem Bilan
2019-06-27 09:47:45 -04:00
committed by Gary Russell
parent c5ec2d94c6
commit c18c2e2141
19 changed files with 293 additions and 130 deletions

View File

@@ -22,6 +22,8 @@ import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
* Spec for a {@link SimpleMessageListenerContainer}.
*
* @author Gary Russell
* @author Artem Bilan
*
* @since 5.0
*
*/
@@ -108,10 +110,22 @@ public class SimpleMessageListenerContainerSpec extends
/**
* @param txSize the txSize.
* @return the spec.
* @see SimpleMessageListenerContainer#setTxSize(int)
* @see SimpleMessageListenerContainer#setBatchSize(int)
* @deprecated since 5.2 in favor of {@link #batchSize(int)}
*/
public SimpleMessageListenerContainerSpec txSize(int txSize) {
this.listenerContainer.setTxSize(txSize);
return batchSize(txSize);
}
/**
* The batch size to use.
* @param batchSize the batchSize.
* @return the spec.
* @see SimpleMessageListenerContainer#setBatchSize(int)
* @since 5.2
*/
public SimpleMessageListenerContainerSpec batchSize(int batchSize) {
this.listenerContainer.setBatchSize(batchSize);
return this;
}

View File

@@ -107,12 +107,13 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
List<Element> headerElements = DomUtils.getChildElementsByTagName(element, "header");
if (!CollectionUtils.isEmpty(headerElements)) {
ManagedMap<String, Object> headerElementsMap = new ManagedMap<String, Object>();
ManagedMap<String, Object> headerElementsMap = new ManagedMap<>();
for (Element headerElement : headerElements) {
String name = headerElement.getAttribute(NAME_ATTRIBUTE);
BeanDefinition headerExpressionDef =
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined(IntegrationNamespaceUtils.EXPRESSION_ATTRIBUTE,
headerElement);
IntegrationNamespaceUtils
.createExpressionDefIfAttributeDefined(IntegrationNamespaceUtils.EXPRESSION_ATTRIBUTE,
headerElement);
if (headerExpressionDef != null) {
headerElementsMap.put(name, headerExpressionDef);
}
@@ -133,8 +134,8 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
}
BeanDefinition expressionDef =
IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("view-name", "view-expression",
parserContext, element, false);
IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("view-name",
"view-expression", parserContext, element, false);
if (expressionDef != null) {
builder.addPropertyValue("viewExpression", expressionDef);
}
@@ -154,13 +155,16 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
if (StringUtils.hasText(headerMapper)) {
if (hasMappedRequestHeaders || hasMappedResponseHeaders) {
parserContext.getReaderContext().error("Neither 'mapped-request-headers' or 'mapped-response-headers' " +
"attributes are allowed when a 'header-mapper' has been specified.", parserContext.extractSource(element));
parserContext.getReaderContext()
.error("Neither 'mapped-request-headers' or 'mapped-response-headers' " +
"attributes are allowed when a 'header-mapper' has been specified.",
parserContext.extractSource(element));
}
builder.addPropertyReference("headerMapper", headerMapper);
}
else {
BeanDefinitionBuilder headerMapperBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultHttpHeaderMapper.class);
BeanDefinitionBuilder headerMapperBuilder =
BeanDefinitionBuilder.genericBeanDefinition(DefaultHttpHeaderMapper.class);
headerMapperBuilder.setFactoryMethod("inboundMapper");
if (hasMappedRequestHeaders) {
@@ -181,7 +185,7 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
if (crossOriginElement != null) {
BeanDefinitionBuilder crossOriginBuilder =
BeanDefinitionBuilder.genericBeanDefinition(CrossOrigin.class);
String[] attributes = {"origin", "allowed-headers", "exposed-headers", "max-age", "method"};
String[] attributes = { "origin", "allowed-headers", "exposed-headers", "max-age", "method" };
for (String crossOriginAttribute : attributes) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(crossOriginBuilder, crossOriginElement,
crossOriginAttribute);
@@ -191,7 +195,8 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
builder.addPropertyValue("crossOrigin", crossOriginBuilder.getBeanDefinition());
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-payload-type", "requestPayloadTypeClass");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element,
"request-payload-type", "requestPayloadTypeClass");
BeanDefinition statusCodeExpressionDef =
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("status-code-expression", element);
@@ -205,6 +210,7 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "validator");
}
private String getInputChannelAttributeName() {
@@ -212,7 +218,8 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
}
private BeanDefinition createRequestMapping(Element element) {
BeanDefinitionBuilder requestMappingDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(RequestMapping.class);
BeanDefinitionBuilder requestMappingDefBuilder =
BeanDefinitionBuilder.genericBeanDefinition(RequestMapping.class);
String methods = element.getAttribute("supported-methods");
if (StringUtils.hasText(methods)) {
@@ -224,7 +231,7 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
Element requestMappingElement = DomUtils.getChildElementByTagName(element, "request-mapping");
if (requestMappingElement != null) {
for (String requestMappingAttribute : new String[]{"params", "headers", "consumes", "produces"}) {
for (String requestMappingAttribute : new String[] { "params", "headers", "consumes", "produces" }) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(requestMappingDefBuilder, requestMappingElement,
requestMappingAttribute);
}

View File

@@ -37,6 +37,7 @@ import org.springframework.integration.http.inbound.RequestMapping;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.util.Assert;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.RequestMethod;
/**
@@ -45,7 +46,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
*
* @since 5.0
*/
public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpointSupportSpec<S, E>, E extends BaseHttpInboundEndpoint>
public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpointSupportSpec<S, E>,
E extends BaseHttpInboundEndpoint>
extends MessagingGatewaySpec<S, E>
implements ComponentsRegistration {
@@ -93,7 +95,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* Specify a SpEL expression to evaluate in order to generate the Message payload.
* @param payloadExpression The payload expression.
* @return the spec
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
* @see BaseHttpInboundEndpoint#setPayloadExpression(Expression)
*/
public S payloadExpression(String payloadExpression) {
return payloadExpression(PARSER.parseExpression(payloadExpression));
@@ -103,7 +105,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* Specify a SpEL expression to evaluate in order to generate the Message payload.
* @param payloadExpression The payload expression.
* @return the spec
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
* @see BaseHttpInboundEndpoint#setPayloadExpression(Expression)
*/
public S payloadExpression(Expression payloadExpression) {
this.target.setPayloadExpression(payloadExpression);
@@ -115,7 +117,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* @param payloadFunction The payload {@link Function}.
* @param <P> the expected HTTP request body type.
* @return the spec
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
* @see BaseHttpInboundEndpoint#setPayloadExpression(Expression)
*/
public <P> S payloadFunction(Function<HttpEntity<P>, ?> payloadFunction) {
return payloadExpression(new FunctionExpression<>(payloadFunction));
@@ -125,7 +127,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* Specify a Map of SpEL expressions to evaluate in order to generate the Message headers.
* @param expressions The {@link Map} of SpEL expressions for headers.
* @return the spec
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
* @see BaseHttpInboundEndpoint#setHeaderExpressions(Map)
*/
public S headerExpressions(Map<String, Expression> expressions) {
Assert.notNull(expressions, "'headerExpressions' must not be null");
@@ -139,7 +141,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* @param header the header name to populate.
* @param expression the SpEL expression for the header.
* @return the spec
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
* @see BaseHttpInboundEndpoint#setHeaderExpressions(Map)
*/
public S headerExpression(String header, String expression) {
return headerExpression(header, PARSER.parseExpression(expression));
@@ -150,7 +152,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* @param header the header name to populate.
* @param expression the SpEL expression for the header.
* @return the spec
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
* @see BaseHttpInboundEndpoint#setHeaderExpressions(Map)
*/
public S headerExpression(String header, Expression expression) {
this.headerExpressions.put(header, expression);
@@ -163,7 +165,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* @param headerFunction the function to evaluate the header value against {@link HttpEntity}.
* @param <P> the expected HTTP body type.
* @return the current Spec.
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
* @see BaseHttpInboundEndpoint#setHeaderExpressions(Map)
*/
public <P> S headerFunction(String header, Function<HttpEntity<P>, ?> headerFunction) {
return headerExpression(header, new FunctionExpression<>(headerFunction));
@@ -251,7 +253,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @return the current Spec.
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
* @see BaseHttpInboundEndpoint#setStatusCodeExpression(Expression)
*/
public S statusCodeExpression(String statusCodeExpression) {
this.target.setStatusCodeExpressionString(statusCodeExpression);
@@ -263,7 +265,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @return the current Spec.
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
* @see BaseHttpInboundEndpoint#setStatusCodeExpression(Expression)
*/
public S statusCodeExpression(Expression statusCodeExpression) {
this.target.setStatusCodeExpression(statusCodeExpression);
@@ -275,12 +277,23 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeFunction The status code {@link Function}.
* @return the current Spec.
* @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
* @see BaseHttpInboundEndpoint#setStatusCodeExpression(Expression)
*/
public S statusCodeFunction(Function<RequestEntity<?>, ?> statusCodeFunction) {
return statusCodeExpression(new FunctionExpression<>(statusCodeFunction));
}
/**
* Specify a {@link Validator} to validate a converted payload from request.
* @param validator the {@link Validator} to use.
* @return the spec
* @since 5.2
*/
public S validator(Validator validator) {
this.target.setValidator(validator);
return _this();
}
@Override
public Map<Object, String> getComponentsToRegister() {
HeaderMapper<HttpHeaders> headerMapperToRegister =

View File

@@ -33,11 +33,15 @@ import org.springframework.integration.context.OrderlyShutdownCapable;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.http.support.IntegrationWebExchangeBindException;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
/**
* The {@link MessagingGatewaySupport} extension for HTTP Inbound endpoints
@@ -65,6 +69,8 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
private final boolean expectReply;
private Validator validator;
private ResolvableType requestPayloadType = null;
private HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
@@ -253,6 +259,19 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
return this.statusCodeExpression;
}
/**
* Specify a {@link Validator} to validate a converted payload from request.
* @param validator the {@link Validator} to use.
* @since 5.2
*/
public void setValidator(Validator validator) {
this.validator = validator;
}
protected Validator getValidator() {
return this.validator;
}
@Override
protected void onInit() {
super.onInit();
@@ -334,4 +353,12 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
return !(CollectionUtils.containsInstance(NON_READABLE_BODY_HTTP_METHODS, httpMethod));
}
protected void validate(Object value) {
BeanPropertyBindingResult errors = new BeanPropertyBindingResult(value, "requestPayload");
ValidationUtils.invokeValidator(this.validator, value, errors);
if (errors.hasErrors()) {
throw new IntegrationWebExchangeBindException(getComponentName(), value, errors);
}
}
}

View File

@@ -319,6 +319,10 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
AbstractIntegrationMessageBuilder<?> messageBuilder;
if (getValidator() != null) {
validate(payload);
}
if (payload instanceof Message<?>) {
messageBuilder =
getMessageBuilderFactory()

View File

@@ -362,6 +362,18 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="validator" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.validation.Validator" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
A 'Validator' bean reference to validate a payload converted from the HTTP request.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:attributeGroup>
<xsd:element name="outbound-channel-adapter">

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
xmlns="http://www.springframework.org/schema/integration/http"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:si="http://www.springframework.org/schema/integration"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
xmlns="http://www.springframework.org/schema/integration/http"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:si="http://www.springframework.org/schema/integration"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
https://www.springframework.org/schema/integration/spring-integration.xsd
@@ -20,20 +20,25 @@
<si:queue capacity="1"/>
</si:channel>
<beans:bean id="validator" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.springframework.validation.Validator"/>
</beans:bean>
<inbound-channel-adapter id="defaultAdapter" channel="requests" error-channel="errorChannel"
auto-startup="false"
phase="1001"
status-code-expression="'101'"/>
auto-startup="false"
phase="1001"
status-code-expression="'101'"
validator="validator"/>
<inbound-channel-adapter id="postOnlyAdapter" path="/postOnly" channel="requests" supported-methods="POST"/>
<inbound-channel-adapter id="adapterWithCustomConverterWithDefaults" message-converters="customConverters"
channel="requests" supported-methods="DELETE" merge-with-default-converters="true"/>
channel="requests" supported-methods="DELETE" merge-with-default-converters="true"/>
<inbound-channel-adapter id="adapterWithCustomConverterNoDefaults" message-converters="customConverters"
channel="requests" supported-methods="HEAD" />
channel="requests" supported-methods="HEAD"/>
<inbound-channel-adapter id="adapterNoCustomConverterNoDefaults" channel="requests" supported-methods="POST" />
<inbound-channel-adapter id="adapterNoCustomConverterNoDefaults" channel="requests" supported-methods="POST"/>
<util:list id="customConverters">
<beans:bean class="org.springframework.integration.http.converter.SerializingHttpMessageConverter"/>
@@ -42,7 +47,7 @@
<inbound-channel-adapter id="putOrDeleteAdapter" channel="requests" supported-methods="PUT, delete"/>
<inbound-channel-adapter id="inboundController" channel="requests" view-name="foo" error-code="oops"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED">
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED">
<request-mapping headers="BAR"/>
</inbound-channel-adapter>

View File

@@ -17,6 +17,9 @@
package org.springframework.integration.http.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willReturn;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
@@ -49,10 +52,10 @@ import org.springframework.messaging.PollableChannel;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.HandlerMapping;
@@ -65,8 +68,7 @@ import org.springframework.web.servlet.HandlerMapping;
* @author Artem Bilan
* @author Biju Kunjummen
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@RunWith(SpringRunner.class)
@DirtiesContext
public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTests {
@@ -112,9 +114,13 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
@Autowired
private MessageChannel autoChannel;
@Autowired @Qualifier("autoChannel.adapter")
@Autowired
@Qualifier("autoChannel.adapter")
private HttpRequestHandlingMessagingGateway autoChannelAdapter;
@Autowired
private Validator validator;
@Test
@SuppressWarnings("unchecked")
public void getRequestOk() throws Exception {
@@ -128,6 +134,7 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
this.defaultAdapter.start();
response = new MockHttpServletResponse();
willReturn(true).given(this.validator).supports(any());
this.defaultAdapter.handleRequest(request, response);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_SWITCHING_PROTOCOLS);
Message<?> message = requests.receive(0);
@@ -140,12 +147,13 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
assertThat(map.get("foo").size()).isEqualTo(1);
assertThat(map.getFirst("foo")).isEqualTo("bar");
assertThat(TestUtils.getPropertyValue(this.defaultAdapter, "errorChannel")).isNotNull();
assertThat(TestUtils.getPropertyValue(this.defaultAdapter, "validator")).isSameAs(this.validator);
}
@Test
public void getRequestWithHeaders() throws Exception {
public void getRequestWithHeaders() {
DefaultHttpHeaderMapper headerMapper =
(DefaultHttpHeaderMapper) TestUtils.getPropertyValue(withMappedHeaders, "headerMapper");
TestUtils.getPropertyValue(withMappedHeaders, "headerMapper", DefaultHttpHeaderMapper.class);
HttpHeaders headers = new HttpHeaders();
headers.set("foo", "foo");
@@ -187,19 +195,18 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
}
@Test
public void getRequestNotAllowed() throws Exception {
public void getRequestNotAllowed() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.setParameter("foo", "bar");
request.setRequestURI("/postOnly");
try {
this.integrationRequestMappingHandlerMapping.getHandler(request);
}
catch (HttpRequestMethodNotSupportedException e) {
assertThat(e.getMethod()).isEqualTo("GET");
assertThat(e.getSupportedMethods()).isEqualTo(new String[] { "POST" });
}
assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class)
.isThrownBy(() -> this.integrationRequestMappingHandlerMapping.getHandler(request))
.satisfies((ex) -> {
assertThat(ex.getMethod()).isEqualTo("GET");
assertThat(ex.getSupportedMethods()).containsExactly("POST");
});
}
@Test
@@ -222,7 +229,8 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
assertThat(message.getPayload()).isEqualTo("test");
}
@Test @DirtiesContext
@Test
@DirtiesContext
public void postRequestWithSerializedObjectContentOk() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
@@ -244,15 +252,15 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
@Test
public void putOrDeleteMethodsSupported() throws Exception {
public void putOrDeleteMethodsSupported() {
HttpMethod[] supportedMethods =
TestUtils.getPropertyValue(putOrDeleteAdapter, "requestMapping.methods", HttpMethod[].class);
assertThat(supportedMethods.length).isEqualTo(2);
assertThat(supportedMethods).isEqualTo(new HttpMethod[] { HttpMethod.PUT, HttpMethod.DELETE });
assertThat(supportedMethods).containsExactly(HttpMethod.PUT, HttpMethod.DELETE);
}
@Test
public void testController() throws Exception {
public void testController() {
String errorCode = TestUtils.getPropertyValue(inboundController, "errorCode", String.class);
assertThat(errorCode).isEqualTo("oops");
Expression viewExpression = TestUtils.getPropertyValue(inboundController, "viewExpression", Expression.class);
@@ -269,8 +277,9 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
}
@Test
public void testInt2717ControllerWithViewExpression() throws Exception {
Expression viewExpression = TestUtils.getPropertyValue(inboundControllerViewExp, "viewExpression", Expression.class);
public void testInt2717ControllerWithViewExpression() {
Expression viewExpression = TestUtils
.getPropertyValue(inboundControllerViewExp, "viewExpression", Expression.class);
assertThat(viewExpression.getExpressionString()).isEqualTo("'foo'");
}
@@ -282,7 +291,8 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
@Test
public void testInboundAdapterWithMessageConverterDefaults() {
@SuppressWarnings("unchecked")
List<HttpMessageConverter<?>> messageConverters = TestUtils.getPropertyValue(adapterWithCustomConverterWithDefaults, "messageConverters", List.class);
List<HttpMessageConverter<?>> messageConverters = TestUtils
.getPropertyValue(adapterWithCustomConverterWithDefaults, "messageConverters", List.class);
assertThat(messageConverters.size() > 1)
.as("There should be more than 1 message converter. The customized one and the defaults.").isTrue();
@@ -293,17 +303,19 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
@Test
public void testInboundAdapterWithNoMessageConverterDefaults() {
@SuppressWarnings("unchecked")
List<HttpMessageConverter<?>> messageConverters = TestUtils.getPropertyValue(adapterWithCustomConverterNoDefaults, "messageConverters", List.class);
List<HttpMessageConverter<?>> messageConverters = TestUtils
.getPropertyValue(adapterWithCustomConverterNoDefaults, "messageConverters", List.class);
//First converter should be the customized one
assertThat(messageConverters.get(0)).isInstanceOf(SerializingHttpMessageConverter.class);
assertThat(messageConverters.size() == 1).as("There should be only the customized messageconverter registered.")
.isTrue();
assertThat(messageConverters).as("There should be only the customized MessageConverter registered.")
.hasSize(1);
}
@Test
public void testInboundAdapterWithNoMessageConverterNoDefaults() {
@SuppressWarnings("unchecked")
List<HttpMessageConverter<?>> messageConverters = TestUtils.getPropertyValue(adapterNoCustomConverterNoDefaults, "messageConverters", List.class);
List<HttpMessageConverter<?>> messageConverters = TestUtils
.getPropertyValue(adapterNoCustomConverterNoDefaults, "messageConverters", List.class);
assertThat(messageConverters.size() > 1).as("There should be more than 1 message converter. The defaults.")
.isTrue();
}
@@ -316,6 +328,7 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
TestObject(String text) {
this.text = text;
}
}
}

View File

@@ -21,6 +21,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -38,6 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -71,6 +73,9 @@ import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.WebApplicationContext;
@@ -199,6 +204,37 @@ public class HttpDslTests {
}));
}
@Autowired
private Validator validator;
@Test
public void testValidation() throws Exception {
IntegrationFlow flow =
IntegrationFlows.from(
Http.inboundChannelAdapter("/validation")
.requestMapping((mapping) -> mapping
.methods(HttpMethod.POST)
.consumes(MediaType.APPLICATION_JSON_VALUE))
.requestPayloadType(TestModel.class)
.validator(this.validator))
.bridge()
.get();
IntegrationFlowContext.IntegrationFlowRegistration flowRegistration =
this.integrationFlowContext.registration(flow).register();
this.mockMvc.perform(
post("/validation")
.with(httpBasic("user", "user"))
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"\"}"))
.andExpect(status().isBadRequest())
.andExpect(status().reason("Validation failure"));
flowRegistration.destroy();
}
@Configuration
@EnableWebSecurity
@EnableIntegration
@@ -309,6 +345,11 @@ public class HttpDslTests {
return channelSecurityInterceptor;
}
@Bean
public Validator customValidator() {
return new TestModelValidator();
}
}
public static class HttpProxyResponseErrorHandler extends DefaultResponseErrorHandler {
@@ -322,4 +363,35 @@ public class HttpDslTests {
}
public static class TestModel {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
private static class TestModelValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return TestModel.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
TestModel testModel = (TestModel) target;
if (!StringUtils.hasText(testModel.getName())) {
errors.rejectValue("name", "Must not be empty");
}
}
}
}

View File

@@ -20,7 +20,6 @@ import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.integration.http.dsl.HttpInboundEndpointSupportSpec;
import org.springframework.integration.webflux.inbound.WebFluxInboundEndpoint;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
/**
@@ -54,15 +53,4 @@ public class WebFluxInboundEndpointSpec
return this;
}
/**
* Specify a {@link Validator} to validate a converted payload from request.
* @param validator the {@link Validator} to use.
* @return the spec
* @since 5.2
*/
public WebFluxInboundEndpointSpec validator(Validator validator) {
this.target.setValidator(validator);
return this;
}
}

View File

@@ -48,15 +48,11 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.integration.expression.ExpressionEvalMap;
import org.springframework.integration.http.inbound.BaseHttpInboundEndpoint;
import org.springframework.integration.http.support.IntegrationWebExchangeBindException;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@@ -94,8 +90,6 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W
private ReactiveAdapterRegistry adapterRegistry = new ReactiveAdapterRegistry();
private Validator validator;
public WebFluxInboundEndpoint() {
this(true);
}
@@ -133,15 +127,6 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W
this.adapterRegistry = adapterRegistry;
}
/**
* Specify a {@link Validator} to validate a converted payload from request.
* @param validator the {@link Validator} to use.
* @since 5.2
*/
public void setValidator(Validator validator) {
this.validator = validator;
}
@Override
public String getComponentType() {
return super.getComponentType().replaceFirst("http", "webflux");
@@ -244,14 +229,14 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = httpMessageReader.read(bodyType, elementType, request, response, readHints);
if (this.validator != null) {
if (getValidator() != null) {
flux = flux.doOnNext(this::validate);
}
return Mono.just(adapter.fromPublisher(flux));
}
else {
Mono<?> mono = httpMessageReader.readMono(bodyType, elementType, request, response, readHints);
if (this.validator != null) {
if (getValidator() != null) {
mono = mono.doOnNext(this::validate);
}
if (adapter != null) {
@@ -263,14 +248,6 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W
}
}
private void validate(Object value) {
BeanPropertyBindingResult errors = new BeanPropertyBindingResult(value, "requestPayload");
ValidationUtils.invokeValidator(this.validator, value, errors);
if (errors.hasErrors()) {
throw new IntegrationWebExchangeBindException(getComponentName(), value, errors);
}
}
private Mono<Tuple2<Message<Object>, RequestEntity<?>>> buildMessage(RequestEntity<?> httpEntity,
ServerWebExchange exchange) {

View File

@@ -327,6 +327,18 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="validator" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.validation.Validator" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
A 'Validator' bean reference to validate a payload converted from the HTTP request.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:attributeGroup>
<xsd:element name="outbound-channel-adapter">

View File

@@ -10,7 +10,11 @@
<si:channel id="requests"/>
<inbound-channel-adapter id="reactiveMinimalConfig" channel="requests"/>
<beans:bean id="validator" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.springframework.validation.Validator"/>
</beans:bean>
<inbound-channel-adapter id="reactiveMinimalConfig" channel="requests" validator="validator"/>
<inbound-channel-adapter id="reactiveFullConfig" channel="requests"
path="test1"

View File

@@ -37,6 +37,7 @@ import org.springframework.integration.webflux.inbound.WebFluxInboundEndpoint;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
/**
@@ -71,6 +72,9 @@ public class WebFluxInboundChannelAdapterParserTests {
@Autowired
private ReactiveAdapterRegistry reactiveAdapterRegistry;
@Autowired
private Validator validator;
@Test
public void reactiveMinimalConfig() {
DirectFieldAccessor endpointAccessor = new DirectFieldAccessor(this.reactiveMinimalConfig);
@@ -88,6 +92,7 @@ public class WebFluxInboundChannelAdapterParserTests {
assertThat(endpointAccessor.getPropertyValue("requestedContentTypeResolver"))
.isNotSameAs(this.requestedContentTypeResolver);
assertThat(endpointAccessor.getPropertyValue("adapterRegistry")).isNotSameAs(this.reactiveAdapterRegistry);
assertThat(endpointAccessor.getPropertyValue("validator")).isSameAs(this.validator);
}
@Test

View File

@@ -330,7 +330,7 @@ public class WebFluxDslTests {
@Override
public Validator getValidator() {
return webFluxValidator();
return new TestModelValidator();
}
@Bean
@@ -455,11 +455,6 @@ public class WebFluxDslTests {
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));
}
@Bean
public Validator webFluxValidator() {
return new TestModelValidator();
}
}
public static class TestModel {

View File

@@ -90,6 +90,12 @@ The following table summarizes the various endpoints with quick links to the app
| <<./http.adoc#http-inbound,Http Inbound Components>>
| <<./http.adoc#http-outbound,HTTP Outbound Components>>
| *WebFlux*
| <<./webflux.adoc#webflux-namespace,WebFlux Namespace Support>>
| <<./webflux.adoc#webflux-namespace,WebFlux Namespace Support>>
| <<./webflux.adoc#webflux-inbound,WebFlux Inbound Components>>
| <<./webflux.adoc#webflux-outbound,WebFlux Outbound Components>>
| *JDBC*
| <<./jdbc.adoc#jdbc-inbound-channel-adapter,Inbound Channel Adapter>> and <<./jdbc.adoc#stored-procedure-inbound-channel-adapter,Stored Procedure Inbound Channel Adapter>>
| <<./jdbc.adoc#jdbc-outbound-channel-adapter,Outbound Channel Adapter>> and <<./jdbc.adoc#stored-procedure-outbound-channel-adapter,Stored Procedure Outbound Channel Adapter>>

View File

@@ -32,8 +32,7 @@ The `javax.servlet:javax.servlet-api` dependency must be provided on the target
To receive messages over HTTP, you need to use an HTTP inbound channel adapter or an HTTP inbound gateway.
To support the HTTP inbound adapters, they need to be deployed within a servlet container such as https://tomcat.apache.org/[Apache Tomcat] or https://www.eclipse.org/jetty/[Jetty].
The easiest way to do this is to use Spring's
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/support/HttpRequestHandlerServlet.html[`HttpRequestHandlerServlet`], by providing the following servlet definition in the `web.xml` file:
The easiest way to do this is to use Spring's https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/support/HttpRequestHandlerServlet.html[`HttpRequestHandlerServlet`], by providing the following servlet definition in the `web.xml` file:
====
[source,xml]
@@ -46,9 +45,7 @@ https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/c
====
Notice that the servlet name matches the bean name.
For more information on using the `HttpRequestHandlerServlet`, see
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remoting and web services using Spring],
which is part of the Spring Framework Reference documentation.
For more information on using the `HttpRequestHandlerServlet`, see https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remoting and web services using Spring], which is part of the Spring Framework Reference documentation.
If you are running within a Spring MVC application, then the aforementioned explicit servlet definition is not necessary.
In that case, the bean name for your gateway can be matched against the URL path as you would for a Spring MVC Controller bean.
@@ -79,8 +76,7 @@ An additional flag (`mergeWithDefaultConverters`) can be set along with the list
By default, this flag is set to `false`, meaning that the custom converters replace the default list.
The message conversion process uses the (optional) `requestPayloadType` property and the incoming `Content-Type` header.
Starting with version 4.3, if a request has no content type header, `application/octet-stream` is assumed, as
recommended by `RFC 2616`.
Starting with version 4.3, if a request has no content type header, `application/octet-stream` is assumed, as recommended by `RFC 2616`.
Previously, the body of such messages was ignored.
Spring Integration 2.0 implemented multipart file support.
@@ -146,10 +142,17 @@ The preceding example also shows how to customize the HTTP methods accepted by t
The reply message is available in the model map.
By default, the key for that map entry is 'reply', but you can override this default by setting the 'replyKey' property on the endpoint's configuration.
[[http-validation]]
==== Payload Validation
Starting with version 5.2, the HTTP inbound endpoints can be supplied with a `Validator` to check a payload before sending into the channel.
This payload is already a result of conversion and extraction after `payloadExpression` to narrow a validation scope in regards to the valuable data.
The validation failure handling is fully the same what we have in Spring MVC https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-exceptionhandlers[Error Handling].
[[http-outbound]]
=== HTTP Outbound Components
This section describes Spring Integration's HTTP outbound components
This section describes Spring Integration's HTTP outbound components.
==== Using `HttpRequestExecutingMessageHandler`
@@ -205,8 +208,7 @@ If `transfer-cookies` is `false`, any `Set-Cookie` header received remains as `S
HTTP is a request-response protocol.
However, the response may not have a body, only headers.
In this case, the `HttpRequestExecutingMessageHandler` produces a reply `Message` with the payload being an `org.springframework.http.ResponseEntity`, regardless of any provided `expected-response-type`.
According to the https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[HTTP RFC Status Code Definitions], there are many statuses that mandate that a response must not contain a message-body (for example,
`204 No Content`).
According to the https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[HTTP RFC Status Code Definitions], there are many statuses that mandate that a response must not contain a message-body (for example, `204 No Content`).
There are also cases where calls to the same URL might or might not return a response body.
For example, the first request to an HTTP resource returns content, but the second does not (returning a `304 Not Modified`).
In all cases, however, the `http_statusCode` message header is populated.
@@ -350,8 +352,7 @@ For more information regarding handler mappings, see https://docs.spring.io/spri
[[http-cors]]
==== Cross-origin Resource Sharing (CORS) Support
Starting with version 4.2, you can configure the `<http:inbound-channel-adapter>` and `<http:inbound-gateway>` with
a `<cross-origin>` element.
Starting with version 4.2, you can configure the `<http:inbound-channel-adapter>` and `<http:inbound-gateway>` with a `<cross-origin>` element.
It represents the same options as Spring MVC's `@CrossOrigin` for `@Controller` annotations and allows the configuration of cross-origin resource sharing (CORS) for Spring Integration HTTP endpoints:
* `origin`: List of allowed origins.
@@ -376,8 +377,7 @@ This property controls the value of the `Access-Control-Max-Age` header in the p
A value of `-1` means undefined.
The default value is 1800 seconds (30 minutes).
The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class,
instances of which can be injected into the `HttpRequestHandlingEndpointSupport` beans.
The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class, instances of which can be injected into the `HttpRequestHandlingEndpointSupport` beans.
[[http-response-statuscode]]
==== Response Status Code
@@ -403,8 +403,7 @@ The following example shows how to set the status code to `ACCEPTED`:
====
The `<http:inbound-gateway>` resolves the 'status code' from the `http_statusCode` header of the reply `Message`.
Starting with version 4.2, the default response status code when no reply is received within the `reply-timeout`
is `500 Internal Server Error`.
Starting with version 4.2, the default response status code when no reply is received within the `reply-timeout` is `500 Internal Server Error`.
There are two ways to modify this behavior:
* Add a `reply-timeout-status-code-expression`.
@@ -427,8 +426,7 @@ The payload of the `ErrorMessage` is a `MessageTimeoutException`.
It must be transformed to something that can be converted by the gateway, such as a `String`.
A good candidate is the exception's message property, which is the value used when you use the `expression` technique.
If the error flow times out after a main flow timeout, `500 Internal Server Error` is returned, or, if the
`reply-timeout-status-code-expression` is present, it is evaluated.
If the error flow times out after a main flow timeout, `500 Internal Server Error` is returned, or, if the `reply-timeout-status-code-expression` is present, it is evaluated.
NOTE: Previously, the default status code for a timeout was `200 OK`.
To restore that behavior, set `reply-timeout-status-code-expression="200"`.
@@ -655,7 +653,7 @@ The `uriVariablesExpression` property provides a very powerful mechanism for eva
We anticipate that people mostly use simple expressions, such as the preceding example.
However, you can also configure something such as `"@uriVariablesBean.populate(#root)"` with an expression in the returned map being `variables.put("thing1", EXPRESSION_PARSER.parseExpression(message.getHeaders().get("thing2", String.class)));`, where the expression is dynamically provided in the message header named `thing2`.
Since the header may come from an untrusted source, the HTTP outbound endpoints use `SimpleEvaluationContext` when evaluating these expressions.
`SimpleEvaluationContext` uses only a subset of SpEL features.
The `SimpleEvaluationContext` uses only a subset of SpEL features.
If you trust your message sources and wish to use the restricted SpEL constructs, set the `trustedSpel` property of the outbound endpoint to `true`.
====
@@ -830,8 +828,7 @@ image::images/http-outbound-gateway.png[align="center"]
//TODO These images are too small, and the text within them is much too small.
You may want to configure the HTTP related timeout behavior, when making active HTTP requests by using the HTTP outbound gateway or the HTTP outbound channel adapter.
In those instances, these two components use Spring's
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html[`RestTemplate`] support to execute HTTP requests.
In those instances, these two components use Spring's https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html[`RestTemplate`] support to execute HTTP requests.
To configure timeouts for the HTTP outbound gateway and the HTTP outbound channel adapter, you can either reference a `RestTemplate` bean directly (by using the `rest-template` attribute) or you can provide a reference to a https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/client/ClientHttpRequestFactory.html[`ClientHttpRequestFactory`] bean (by using the `request-factory` attribute).
Spring provides the following implementations of the `ClientHttpRequestFactory` interface:

View File

@@ -91,8 +91,14 @@ See <<./http.adoc#http-request-mapping,Request Mapping Support>> and <<./http.ad
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`.
It is used to validate elements in the `Publisher` to which a request has been converted by the `HttpMessageReader`.
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.

View File

@@ -95,6 +95,12 @@ See <<./ip.adoc#tcp-connection-factory, TCP Connection Factories>> for more info
The `AbstractMailReceiver` has now an `autoCloseFolder` option (`true` by default), to disable an automatic folder close after a fetch, but populate `IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE` header instead for downstream interaction.
See <<./mail.adoc#mail-inbound,Mail-receiving Channel Adapter>> for more information.
[[x5.2-http]]
==== HTTP Changes
The HTTP inbound endpoint now support a request payload validation.
See <<./http.adoc#http,HTTP Support>> for more information.
[[x5.2-webflux]]
==== WebFlux Changes