INT-4315: Add WebFlux module

JIRA: https://jira.spring.io/browse/INT-4315

Move Reactive components outside of HTTP module to the new WebFlux one,
including XSD, tests and documentation
Make an appropriate polishing for the `http.adoc` with cross-link
to the `webflux.adoc`

Exclude transitive `spring-webmvc` for the `spring-integration-webflux`
This commit is contained in:
Artem Bilan
2017-08-11 10:37:08 -04:00
committed by Gary Russell
parent 4fd32d2bd4
commit 84d60f4ab4
52 changed files with 1657 additions and 1003 deletions

View File

@@ -23,14 +23,10 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.integration.config.IntegrationConfigurationInitializer;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.inbound.IntegrationHandlerResultHandler;
import org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping;
import org.springframework.integration.http.inbound.ReactiveIntegrationRequestMappingHandlerMapping;
import org.springframework.integration.http.support.HttpContextUtils;
/**
@@ -48,7 +44,6 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof BeanDefinitionRegistry) {
registerRequestMappingHandlerMappingIfNecessary((BeanDefinitionRegistry) beanFactory);
registerReactiveRequestMappingHandlerMappingIfNecessary((BeanDefinitionRegistry) beanFactory);
}
else {
logger.warn("'IntegrationRequestMappingHandlerMapping' isn't registered because 'beanFactory'" +
@@ -78,30 +73,4 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
}
}
/**
* Registers a {@link ReactiveIntegrationRequestMappingHandlerMapping}
* which could also be overridden by the user by simply registering
* a {@link ReactiveIntegrationRequestMappingHandlerMapping} {@code <bean>} with 'id'
* {@link HttpContextUtils#REACTIVE_HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In addition, checks if the {@code org.springframework.web.reactive.result.method.RequestMappingInfo}
* class is present on the classpath.
* When Spring Integration HTTP is used only as an HTTP client, there is no reason to use and register
* the HTTP server components.
*/
private void registerReactiveRequestMappingHandlerMappingIfNecessary(BeanDefinitionRegistry registry) {
if (HttpContextUtils.WEB_FLUX_PRESENT &&
!registry.containsBeanDefinition(HttpContextUtils.REACTIVE_HANDLER_MAPPING_BEAN_NAME)) {
BeanDefinitionBuilder requestMappingBuilder =
BeanDefinitionBuilder.genericBeanDefinition(ReactiveIntegrationRequestMappingHandlerMapping.class);
requestMappingBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
requestMappingBuilder.addPropertyValue(IntegrationNamespaceUtils.ORDER, 0);
registry.registerBeanDefinition(HttpContextUtils.REACTIVE_HANDLER_MAPPING_BEAN_NAME,
requestMappingBuilder.getBeanDefinition());
BeanDefinitionReaderUtils.registerWithGeneratedName(
new RootBeanDefinition(IntegrationHandlerResultHandler.class), registry);
}
}
}

View File

@@ -34,8 +34,6 @@ public class HttpNamespaceHandler extends AbstractIntegrationNamespaceHandler {
registerBeanDefinitionParser("inbound-gateway", new HttpInboundEndpointParser(true));
registerBeanDefinitionParser("outbound-channel-adapter", new HttpOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-gateway", new HttpOutboundGatewayParser());
registerBeanDefinitionParser("outbound-reactive-channel-adapter", new HttpOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-reactive-gateway", new HttpOutboundGatewayParser());
registerBeanDefinitionParser("graph-controller", new IntegrationGraphControllerParser());
}

View File

@@ -18,13 +18,13 @@ package org.springframework.integration.http.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler;
import org.springframework.util.StringUtils;
/**
@@ -42,41 +42,13 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda
@Override
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder;
boolean reactive = element.getLocalName().contains("reactive");
if (reactive) {
builder = BeanDefinitionBuilder.genericBeanDefinition(ReactiveHttpRequestExecutingMessageHandler.class);
}
else {
builder = BeanDefinitionBuilder.genericBeanDefinition(HttpRequestExecutingMessageHandler.class);
}
BeanDefinitionBuilder builder = getBuilder(element, parserContext);
builder.addPropertyValue("expectReply", false);
HttpAdapterParsingUtils.configureUrlConstructorArg(element, parserContext, builder);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
HttpAdapterParsingUtils.setHttpMethodOrExpression(element, parserContext, builder);
if (reactive) {
String webClientRef = element.getAttribute("web-client");
if (StringUtils.hasText(webClientRef)) {
builder.addConstructorArgReference(webClientRef);
}
}
else {
String restTemplateRef = element.getAttribute("rest-template");
if (StringUtils.hasText(restTemplateRef)) {
HttpAdapterParsingUtils.verifyNoRestTemplateAttributes(element, parserContext);
builder.addConstructorArgReference(restTemplateRef);
}
else {
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
}
}
String headerMapper = element.getAttribute("header-mapper");
String mappedRequestHeaders = element.getAttribute("mapped-request-headers");
if (StringUtils.hasText(headerMapper)) {
@@ -101,4 +73,24 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda
return builder.getBeanDefinition();
}
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(HttpRequestExecutingMessageHandler.class);
String restTemplateRef = element.getAttribute("rest-template");
if (StringUtils.hasText(restTemplateRef)) {
HttpAdapterParsingUtils.verifyNoRestTemplateAttributes(element, parserContext);
builder.getBeanDefinition()
.getConstructorArgumentValues()
.addIndexedArgumentValue(1, new RuntimeBeanReference(restTemplateRef));
}
else {
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
}
return builder;
}
}

View File

@@ -18,12 +18,12 @@ package org.springframework.integration.http.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractConsumerEndpointParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler;
import org.springframework.util.StringUtils;
/**
@@ -44,39 +44,12 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser {
@Override
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder;
boolean reactive = element.getLocalName().contains("reactive");
if (reactive) {
builder = BeanDefinitionBuilder.genericBeanDefinition(ReactiveHttpRequestExecutingMessageHandler.class);
}
else {
builder = BeanDefinitionBuilder.genericBeanDefinition(HttpRequestExecutingMessageHandler.class);
}
BeanDefinitionBuilder builder = getBuilder(element, parserContext);
HttpAdapterParsingUtils.configureUrlConstructorArg(element, parserContext, builder);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
HttpAdapterParsingUtils.setHttpMethodOrExpression(element, parserContext, builder);
if (reactive) {
String webClientRef = element.getAttribute("web-client");
if (StringUtils.hasText(webClientRef)) {
builder.addConstructorArgReference(webClientRef);
}
}
else {
String restTemplateRef = element.getAttribute("rest-template");
if (StringUtils.hasText(restTemplateRef)) {
HttpAdapterParsingUtils.verifyNoRestTemplateAttributes(element, parserContext);
builder.addConstructorArgReference(restTemplateRef);
}
else {
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
}
}
String headerMapper = element.getAttribute("header-mapper");
String mappedRequestHeaders = element.getAttribute("mapped-request-headers");
String mappedResponseHeaders = element.getAttribute("mapped-response-headers");
@@ -113,4 +86,24 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser {
return builder;
}
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(HttpRequestExecutingMessageHandler.class);
String restTemplateRef = element.getAttribute("rest-template");
if (StringUtils.hasText(restTemplateRef)) {
HttpAdapterParsingUtils.verifyNoRestTemplateAttributes(element, parserContext);
builder.getBeanDefinition()
.getConstructorArgumentValues()
.addIndexedArgumentValue(1, new RuntimeBeanReference(restTemplateRef));
}
else {
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
}
return builder;
}
}

View File

@@ -61,7 +61,7 @@ public abstract class BaseHttpMessageHandlerSpec<S extends BaseHttpMessageHandle
this.target.setHeaderMapper(this.headerMapper);
}
S expectReply(boolean expectReply) {
protected S expectReply(boolean expectReply) {
this.target.setExpectReply(expectReply);
return _this();
}

View File

@@ -24,12 +24,10 @@ import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.http.inbound.HttpRequestHandlingController;
import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway;
import org.springframework.integration.http.inbound.ReactiveHttpInboundEndpoint;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
/**
* The HTTP components Factory.
@@ -128,98 +126,6 @@ public final class Http {
return new HttpMessageHandlerSpec(uriExpression, restTemplate).expectReply(false);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter based on provided {@link URI}.
* @param uri the {@link URI} to send requests.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(URI uri) {
return outboundReactiveChannelAdapter(uri, null);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter based on provided {@code uri}.
* @param uri the {@code uri} to send requests.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(String uri) {
return outboundReactiveChannelAdapter(uri, null);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter based on provided {@code Function}
* to evaluate target {@code uri} against request message.
* @param uriFunction the {@code Function} to evaluate {@code uri} at runtime.
* @param <P> the expected payload type.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static <P> ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(Function<Message<P>, ?> uriFunction) {
return outboundReactiveChannelAdapter(new FunctionExpression<>(uriFunction));
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter
* based on provided SpEL {@link Expression} to evaluate target {@code uri}
* against request message.
* @param uriExpression the SpEL {@link Expression} to evaluate {@code uri} at runtime.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(Expression uriExpression) {
return outboundReactiveChannelAdapter(uriExpression, null);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter
* based on provided {@link URI} and {@link WebClient}.
* @param uri the {@link URI} to send requests.
* @param webClient {@link WebClient} to use.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(URI uri, WebClient webClient) {
return new ReactiveHttpMessageHandlerSpec(uri, webClient)
.expectReply(false);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter
* based on provided {@code uri} and {@link WebClient}.
* @param uri the {@code uri} to send requests.
* @param webClient {@link WebClient} to use.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(String uri, WebClient webClient) {
return new ReactiveHttpMessageHandlerSpec(uri, webClient)
.expectReply(false);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter
* based on provided {@code Function} to evaluate target {@code uri} against request message
* and {@link WebClient} for HTTP exchanges.
* @param uriFunction the {@code Function} to evaluate {@code uri} at runtime.
* @param webClient {@link WebClient} to use.
* @param <P> the expected payload type.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static <P> ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(Function<Message<P>, ?> uriFunction,
WebClient webClient) {
return outboundReactiveChannelAdapter(new FunctionExpression<>(uriFunction), webClient);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for one-way adapter
* based on provided SpEL {@link Expression} to evaluate target {@code uri}
* against request message and {@link WebClient} for HTTP exchanges.
* @param uriExpression the SpEL {@link Expression} to evaluate {@code uri} at runtime.
* @param webClient {@link WebClient} to use.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveChannelAdapter(Expression uriExpression,
WebClient webClient) {
return new ReactiveHttpMessageHandlerSpec(uriExpression, webClient)
.expectReply(false);
}
/**
* Create an {@link HttpMessageHandlerSpec} builder for request-reply gateway based on provided {@link URI}.
* @param uri the {@link URI} to send requests.
@@ -307,96 +213,6 @@ public final class Http {
return new HttpMessageHandlerSpec(uriExpression, restTemplate);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided {@link URI}.
* @param uri the {@link URI} to send requests.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveGateway(URI uri) {
return outboundReactiveGateway(uri, null);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided {@code uri}.
* @param uri the {@code uri} to send requests.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveGateway(String uri) {
return outboundReactiveGateway(uri, null);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided {@code Function} to evaluate target {@code uri} against request message.
* @param uriFunction the {@code Function} to evaluate {@code uri} at runtime.
* @param <P> the expected payload type.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static <P> ReactiveHttpMessageHandlerSpec outboundReactiveGateway(Function<Message<P>, ?> uriFunction) {
return outboundReactiveGateway(new FunctionExpression<>(uriFunction));
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided SpEL {@link Expression} to evaluate target {@code uri} against request message.
* @param uriExpression the SpEL {@link Expression} to evaluate {@code uri} at runtime.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveGateway(Expression uriExpression) {
return outboundReactiveGateway(uriExpression, null);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided {@link URI} and {@link WebClient}.
* @param uri the {@link URI} to send requests.
* @param webClient {@link WebClient} to use.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveGateway(URI uri, WebClient webClient) {
return new ReactiveHttpMessageHandlerSpec(uri, webClient);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided {@code uri} and {@link WebClient}.
* @param uri the {@code uri} to send requests.
* @param webClient {@link WebClient} to use.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveGateway(String uri, WebClient webClient) {
return new ReactiveHttpMessageHandlerSpec(uri, webClient);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided {@code Function} to evaluate target {@code uri} against request message
* and {@link WebClient} for HTTP exchanges.
* @param uriFunction the {@code Function} to evaluate {@code uri} at runtime.
* @param webClient {@link WebClient} to use.
* @param <P> the expected payload type.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static <P> ReactiveHttpMessageHandlerSpec outboundReactiveGateway(Function<Message<P>, ?> uriFunction,
WebClient webClient) {
return outboundReactiveGateway(new FunctionExpression<>(uriFunction), webClient);
}
/**
* Create an {@link ReactiveHttpMessageHandlerSpec} builder for request-reply gateway
* based on provided SpEL {@link Expression} to evaluate target {@code uri}
* against request message and {@link WebClient} for HTTP exchanges.
* @param uriExpression the SpEL {@link Expression} to evaluate {@code uri} at runtime.
* @param webClient {@link WebClient} to use.
* @return the ReactiveHttpMessageHandlerSpec instance
*/
public static ReactiveHttpMessageHandlerSpec outboundReactiveGateway(Expression uriExpression,
WebClient webClient) {
return new ReactiveHttpMessageHandlerSpec(uriExpression, webClient);
}
/**
* Create an {@link HttpControllerEndpointSpec} builder for one-way adapter
* based on the provided MVC {@code viewName} and {@code path} array for mapping.
@@ -468,28 +284,6 @@ public final class Http {
return new HttpRequestHandlerEndpointSpec(new HttpRequestHandlingMessagingGateway(), path);
}
/**
* Create an {@link ReactiveHttpInboundEndpointSpec} builder for one-way reactive adapter
* based on the provided {@code path} array for mapping.
* @param path the path mapping URIs (e.g. "/myPath.do").
* @return the ReactiveHttpInboundEndpointSpec instance
*/
public static ReactiveHttpInboundEndpointSpec inboundReactiveChannelAdapter(String... path) {
ReactiveHttpInboundEndpoint httpInboundChannelAdapter = new ReactiveHttpInboundEndpoint(false);
return new ReactiveHttpInboundEndpointSpec(httpInboundChannelAdapter, path);
}
/**
* Create an {@link ReactiveHttpInboundEndpointSpec} builder for request-reply reactive gateway
* based on the provided {@code path} array for mapping.
* @param path the path mapping URIs (e.g. "/myPath.do").
* @return the ReactiveHttpInboundEndpointSpec instance
*/
public static ReactiveHttpInboundEndpointSpec inboundReactiveGateway(String... path) {
return new ReactiveHttpInboundEndpointSpec(new ReactiveHttpInboundEndpoint(), path);
}
private Http() {
super();
}

View File

@@ -57,7 +57,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
private HeaderMapper<HttpHeaders> explicitHeaderMapper;
HttpInboundEndpointSupportSpec(E gateway, String... path) {
protected HttpInboundEndpointSupportSpec(E gateway, String... path) {
super(gateway);
this.requestMapping.setPathPatterns(path);
this.target.setRequestMapping(this.requestMapping);

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.dsl;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.integration.http.inbound.ReactiveHttpInboundEndpoint;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
/**
* The {@link HttpInboundEndpointSupportSpec} implementation for the {@link ReactiveHttpInboundEndpoint}.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class ReactiveHttpInboundEndpointSpec
extends HttpInboundEndpointSupportSpec<ReactiveHttpInboundEndpointSpec, ReactiveHttpInboundEndpoint> {
ReactiveHttpInboundEndpointSpec(ReactiveHttpInboundEndpoint gateway, String... path) {
super(gateway, path);
}
public ReactiveHttpInboundEndpointSpec codecConfigurer(ServerCodecConfigurer codecConfigurer) {
this.target.setCodecConfigurer(codecConfigurer);
return this;
}
public ReactiveHttpInboundEndpointSpec requestedContentTypeResolver(
RequestedContentTypeResolver requestedContentTypeResolver) {
this.target.setRequestedContentTypeResolver(requestedContentTypeResolver);
return this;
}
public ReactiveHttpInboundEndpointSpec reactiveAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
this.target.setReactiveAdapterRegistry(adapterRegistry);
return this;
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.dsl;
import java.net.URI;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler;
import org.springframework.web.reactive.function.client.WebClient;
/**
* The {@link BaseHttpMessageHandlerSpec} implementation for the {@link ReactiveHttpRequestExecutingMessageHandler}.
*
* @author Shiliang Li
* @author Artem Bilan
*
* @since 5.0
*
* @see ReactiveHttpRequestExecutingMessageHandler
*/
public class ReactiveHttpMessageHandlerSpec
extends BaseHttpMessageHandlerSpec<ReactiveHttpMessageHandlerSpec, ReactiveHttpRequestExecutingMessageHandler> {
private final WebClient webClient;
ReactiveHttpMessageHandlerSpec(URI uri, WebClient webClient) {
this(new ValueExpression<>(uri), webClient);
}
ReactiveHttpMessageHandlerSpec(String uri, WebClient webClient) {
this(new LiteralExpression(uri), webClient);
}
ReactiveHttpMessageHandlerSpec(Expression uriExpression, WebClient webClient) {
super(new ReactiveHttpRequestExecutingMessageHandler(uriExpression, webClient));
this.webClient = webClient;
}
@Override
protected boolean isClientSet() {
return this.webClient != null;
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.inbound;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* A {@link HandlerResultHandler} implementation to handle the result of the
* {@link ReactiveHttpInboundEndpoint} execution. Actually just return the
* {@code result.getReturnValue()} which essentially is expected {@code Mono<Void>}.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see ReactiveHttpInboundEndpoint
*/
public class IntegrationHandlerResultHandler implements HandlerResultHandler, Ordered {
@Override
public boolean supports(HandlerResult result) {
Object handler = result.getHandler();
return handler instanceof HandlerMethod
&& ReactiveHttpInboundEndpoint.class.isAssignableFrom(((HandlerMethod) handler).getBeanType());
}
@Override
@SuppressWarnings("unchecked")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
return (Mono<Void>) result.getReturnValue();
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
}

View File

@@ -50,7 +50,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
* elements, if there is none registered, yet. However it can be configured as a regular
* bean with appropriate configuration for {@link RequestMappingHandlerMapping}.
* It is recommended to have only one similar bean in the application context using the 'id'
* {@link org.springframework.integration.http.support.HttpContextUtils#REACTIVE_HANDLER_MAPPING_BEAN_NAME}.
* {@link org.springframework.integration.http.support.HttpContextUtils#HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In most cases, Spring MVC offers to configure Request Mapping via
* {@code org.springframework.stereotype.Controller} and

View File

@@ -1,461 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.inbound;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.integration.gateway.MessagingGatewaySupport;
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.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.server.WebHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* A {@link MessagingGatewaySupport} implementation for Spring WebFlux
* HTTP requests execution.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see org.springframework.web.reactive.result.HandlerResultHandlerSupport
* @see org.springframework.web.reactive.config.EnableWebFlux
*/
public class ReactiveHttpInboundEndpoint extends BaseHttpInboundEndpoint implements WebHandler {
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
private ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
private RequestedContentTypeResolver requestedContentTypeResolver = new HeaderContentTypeResolver();
private ReactiveAdapterRegistry adapterRegistry = new ReactiveAdapterRegistry();
public ReactiveHttpInboundEndpoint() {
this(true);
}
public ReactiveHttpInboundEndpoint(boolean expectReply) {
super(expectReply);
}
/**
* A {@link ServerCodecConfigurer} for the request readers and response writers.
* By default the {@link ServerCodecConfigurer#create()} factory is used.
* @param codecConfigurer the {@link ServerCodecConfigurer} to use.
*/
public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
Assert.notNull(codecConfigurer, "'codecConfigurer' must not be null");
this.codecConfigurer = codecConfigurer;
}
/**
* A strategy to resolve the requested media types for a {@code ServerWebExchange}.
* A {@link HeaderContentTypeResolver} is used by default.
* @param requestedContentTypeResolver the {@link RequestedContentTypeResolver} to use.
*/
public void setRequestedContentTypeResolver(RequestedContentTypeResolver requestedContentTypeResolver) {
Assert.notNull(requestedContentTypeResolver, "'requestedContentTypeResolver' must not be null");
this.requestedContentTypeResolver = requestedContentTypeResolver;
}
/**
* A registry of adapters to adapt a Reactive Streams {@link Publisher} to/from.
* @param adapterRegistry the {@link ReactiveAdapterRegistry} to use.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
Assert.notNull(adapterRegistry, "'adapterRegistry' must not be null");
this.adapterRegistry = adapterRegistry;
}
@Override
public String getComponentType() {
return super.getComponentType().replaceFirst("(http:)", "$1reactive-");
}
@Override
protected void onInit() throws Exception {
super.onInit();
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (isRunning()) {
return doHandle(exchange);
}
else {
return serviceUnavailableResponse(exchange);
}
});
}
@SuppressWarnings("unchecked")
private Mono<Void> doHandle(ServerWebExchange exchange) {
return extractRequestBody(exchange)
.doOnSubscribe(s -> this.activeCount.incrementAndGet())
.map(body -> new HttpEntity<>(body, exchange.getRequest().getHeaders()))
.map(entity -> buildMessage(entity, exchange))
.flatMap(requestMessage -> {
if (this.expectReply) {
return sendAndReceiveMessageReactive(requestMessage)
.flatMap(replyMessage -> populateResponse(exchange, replyMessage));
}
else {
send(requestMessage);
return setStatusCode(exchange);
}
})
.doOnTerminate((e, t) -> this.activeCount.decrementAndGet());
}
@SuppressWarnings("unchecked")
private <T> Mono<T> extractRequestBody(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (isReadable(request)) {
MediaType contentType;
if (request.getHeaders().getContentType() == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
else {
contentType = request.getHeaders().getContentType();
}
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
return (Mono<T>) exchange.getFormData();
}
else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
return (Mono<T>) exchange.getMultipartData();
}
else {
ResolvableType bodyType = getRequestPayloadType();
if (bodyType == null) {
bodyType =
"text".equals(contentType.getType())
? ResolvableType.forClass(String.class)
: ResolvableType.forClass(byte[].class);
}
Class<?> resolvedType = bodyType.resolve();
ReactiveAdapter adapter = (resolvedType != null ? this.adapterRegistry.getAdapter(resolvedType) : null);
ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType);
HttpMessageReader<?> httpMessageReader = this.codecConfigurer
.getReaders()
.stream()
.filter(reader -> reader.canRead(elementType, contentType))
.findFirst()
.orElseThrow(() -> new UnsupportedMediaTypeStatusException(
"Could not convert request: no suitable HttpMessageReader found for expected type ["
+ elementType + "] and content type [" + contentType + "]"));
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = httpMessageReader.read(bodyType, elementType, request, response, readHints);
return (Mono<T>) Mono.just(adapter.fromPublisher(flux));
}
else {
Mono<?> mono = httpMessageReader.readMono(bodyType, elementType, request, response, readHints);
if (adapter != null) {
return (Mono<T>) Mono.just(adapter.fromPublisher(mono));
}
else {
return (Mono<T>) mono;
}
}
}
}
else {
return (Mono<T>) Mono.just(exchange.getRequest().getQueryParams());
}
}
@SuppressWarnings("unchecked")
private Message<?> buildMessage(HttpEntity<?> httpEntity, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders requestHeaders = request.getHeaders();
Map<String, Object> exchangeAttributes = exchange.getAttributes();
StandardEvaluationContext evaluationContext = createEvaluationContext();
evaluationContext.setVariable("requestAttributes", exchangeAttributes);
MultiValueMap<String, String> requestParams = request.getQueryParams();
evaluationContext.setVariable("requestParams", requestParams);
evaluationContext.setVariable("requestHeaders", requestHeaders);
if (!CollectionUtils.isEmpty(request.getCookies())) {
evaluationContext.setVariable("cookies", request.getCookies());
}
Map<String, String> pathVariables =
(Map<String, String>) exchangeAttributes.get(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(pathVariables)) {
evaluationContext.setVariable("pathVariables", pathVariables);
}
Map<String, MultiValueMap<String, String>> matrixVariables =
(Map<String, MultiValueMap<String, String>>) exchangeAttributes.get(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(matrixVariables)) {
evaluationContext.setVariable("matrixVariables", matrixVariables);
}
evaluationContext.setRootObject(httpEntity);
Object payload;
if (getPayloadExpression() != null) {
payload = getPayloadExpression().getValue(evaluationContext);
if (payload == null) {
throw new IllegalStateException("The payload expression '" + getPayloadExpression().getExpressionString()
+ "' returned null.");
}
}
else {
payload = httpEntity.getBody();
}
Map<String, Object> headers = getHeaderMapper().toHeaders(request.getHeaders());
if (!CollectionUtils.isEmpty(getHeaderExpressions())) {
for (Map.Entry<String, Expression> entry : getHeaderExpressions().entrySet()) {
String headerName = entry.getKey();
Expression headerExpression = entry.getValue();
Object headerValue = headerExpression.getValue(evaluationContext);
if (headerValue != null) {
headers.put(headerName, headerValue);
}
}
}
AbstractIntegrationMessageBuilder<?> messageBuilder;
if (payload instanceof Message<?>) {
messageBuilder =
getMessageBuilderFactory()
.fromMessage((Message<?>) payload)
.copyHeadersIfAbsent(headers);
}
else {
messageBuilder =
getMessageBuilderFactory()
.withPayload(payload)
.copyHeaders(headers);
}
return messageBuilder
.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, request.getURI().toString())
.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, request.getMethod().toString())
.setHeader(org.springframework.integration.http.HttpHeaders.USER_PRINCIPAL, exchange.getPrincipal().block())
.build();
}
private Mono<Void> populateResponse(ServerWebExchange exchange, Message<?> replyMessage) {
ServerHttpResponse response = exchange.getResponse();
getHeaderMapper().fromHeaders(replyMessage.getHeaders(), response.getHeaders());
Object responseContent = replyMessage;
if (getExtractReplyPayload()) {
responseContent = replyMessage.getPayload();
}
if (responseContent instanceof HttpStatus) {
response.setStatusCode((HttpStatus) responseContent);
return response.setComplete();
}
else {
HttpStatus httpStatus = resolveHttpStatusFromHeaders(replyMessage.getHeaders());
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
return writeResponseBody(exchange, responseContent);
}
}
@SuppressWarnings("unchecked")
private Mono<Void> writeResponseBody(ServerWebExchange exchange, Object body) {
ResolvableType bodyType = ResolvableType.forInstance(body);
ReactiveAdapter adapter = this.adapterRegistry.getAdapter(bodyType.resolve(), body);
Publisher<?> publisher;
ResolvableType elementType;
if (adapter != null) {
publisher = adapter.toPublisher(body);
ResolvableType genericType = bodyType.getGeneric(0);
elementType = getElementType(adapter, genericType);
}
else {
publisher = Mono.justOrEmpty(body);
elementType = bodyType;
}
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
return Mono.from((Publisher<Void>) publisher);
}
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(bodyType);
MediaType bestMediaType = selectMediaType(exchange, () -> producibleMediaTypes);
if (bestMediaType != null) {
for (HttpMessageWriter<?> writer : this.codecConfigurer.getWriters()) {
if (writer.canWrite(bodyType, bestMediaType)) {
return ((HttpMessageWriter<Object>) writer).write(publisher, elementType,
bestMediaType, exchange.getResponse(), Collections.emptyMap());
}
}
}
else {
if (producibleMediaTypes.isEmpty()) {
return Mono.error(new IllegalStateException("No HttpMessageWriters for response type: " + bodyType));
}
}
return Mono.error(new NotAcceptableStatusException(producibleMediaTypes));
}
private ResolvableType getElementType(ReactiveAdapter adapter, ResolvableType genericType) {
if (adapter.isNoValue()) {
return ResolvableType.forClass(Void.class);
}
else if (genericType != ResolvableType.NONE) {
return genericType;
}
else {
return ResolvableType.forClass(Object.class);
}
}
private List<MediaType> getProducibleMediaTypes(ResolvableType elementType) {
return this.codecConfigurer.getWriters()
.stream()
.filter(converter -> converter.canWrite(elementType, null))
.flatMap(converter -> converter.getWritableMediaTypes().stream())
.collect(Collectors.toList());
}
private MediaType selectMediaType(ServerWebExchange exchange, Supplier<List<MediaType>> producibleTypesSupplier) {
List<MediaType> acceptableTypes = getAcceptableTypes(exchange);
List<MediaType> producibleTypes = getProducibleTypes(exchange, producibleTypesSupplier);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
for (MediaType acceptable : acceptableTypes) {
for (MediaType producible : producibleTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible));
}
}
}
List<MediaType> result = new ArrayList<>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(result);
for (MediaType mediaType : result) {
if (mediaType.isConcrete()) {
return mediaType;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
return null;
}
private List<MediaType> getAcceptableTypes(ServerWebExchange exchange) {
List<MediaType> mediaTypes = this.requestedContentTypeResolver.resolveMediaTypes(exchange);
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}
@SuppressWarnings("unchecked")
private List<MediaType> getProducibleTypes(ServerWebExchange exchange,
Supplier<List<MediaType>> producibleTypesSupplier) {
Set<MediaType> mediaTypes = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
return (mediaTypes != null ? new ArrayList<>(mediaTypes) : producibleTypesSupplier.get());
}
private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) {
producible = producible.copyQualityValue(acceptable);
Comparator<MediaType> comparator = MediaType.SPECIFICITY_COMPARATOR;
return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
}
private Mono<Void> setStatusCode(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
if (getStatusCodeExpression() != null) {
HttpStatus httpStatus = evaluateHttpStatus();
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
}
return response.setComplete();
}
private Mono<Void> serviceUnavailableResponse(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
logger.debug("Endpoint is stopped; returning status " + HttpStatus.SERVICE_UNAVAILABLE);
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
return response.writeWith(
Mono.just(response.bufferFactory()
.wrap("Endpoint is stopped".getBytes())));
}
}

View File

@@ -1,170 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.inbound;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.integration.http.support.HttpContextUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.result.condition.NameValueExpression;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
/**
* The {@link org.springframework.web.servlet.HandlerMapping} implementation that
* detects and registers {@link org.springframework.web.servlet.mvc.method.RequestMappingInfo}s for
* {@link HttpRequestHandlingEndpointSupport} from a Spring Integration HTTP configuration
* of {@code <inbound-channel-adapter/>} and {@code <inbound-gateway/>} elements.
* <p>
* This class is automatically configured as a bean in the application context during the
* parsing phase of the {@code <reactive-inbound-gateway/>}
* elements, if there is none registered, yet. However it can be configured as a regular
* bean with appropriate configuration for
* {@link org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping}.
* It is recommended to have only one similar bean in the application context using the 'id'
* {@link org.springframework.integration.http.support.HttpContextUtils#REACTIVE_HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In most cases, Spring MVC offers to configure Request Mapping via
* {@code org.springframework.stereotype.Controller} and
* {@link org.springframework.web.bind.annotation.RequestMapping}.
* That's why Spring MVC's Handler Mapping infrastructure relies on
* {@link org.springframework.web.method.HandlerMethod}, as different methods at the same
* {@code org.springframework.stereotype.Controller} user-class may have their own
* {@link org.springframework.web.bind.annotation.RequestMapping}.
* On the other side, all Spring Integration HTTP Inbound Endpoints are configured on
* the basis of the same {@link HttpRequestHandlingEndpointSupport} class and there is no
* single {@link org.springframework.web.reactive.result.method.RequestMappingInfo} configuration without
* {@link org.springframework.web.method.HandlerMethod} in Spring MVC.
* Accordingly {@link ReactiveIntegrationRequestMappingHandlerMapping} is a
* {@link org.springframework.web.reactive.HandlerMapping}
* compromise implementation between method-level annotations and component-level
* (e.g. Spring Integration XML) configurations.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see RequestMapping
* @see RequestMappingHandlerMapping
*/
public class ReactiveIntegrationRequestMappingHandlerMapping extends RequestMappingHandlerMapping
implements ApplicationListener<ContextRefreshedEvent> {
private static final Method HANDLER_METHOD = ReflectionUtils.findMethod(WebHandler.class,
"handle", ServerWebExchange.class);
private final AtomicBoolean initialized = new AtomicBoolean();
@Override
protected boolean isHandler(Class<?> beanType) {
return ReactiveHttpInboundEndpoint.class.isAssignableFrom(beanType);
}
@Override
protected void detectHandlerMethods(Object handler) {
if (handler instanceof String) {
handler = getApplicationContext().getBean((String) handler);
}
RequestMappingInfo mapping = getMappingForEndpoint((ReactiveHttpInboundEndpoint) handler);
if (mapping != null) {
registerMapping(mapping, handler, HANDLER_METHOD);
}
}
/**
* Create a {@link RequestMappingInfo} from
* a Spring Integration Reactive HTTP Inbound Endpoint {@link RequestMapping}.
* @see RequestMappingHandlerMapping#getMappingForMethod
*/
private RequestMappingInfo getMappingForEndpoint(ReactiveHttpInboundEndpoint endpoint) {
org.springframework.web.bind.annotation.RequestMapping requestMappingAnnotation =
HttpContextUtils.convertRequestMappingToAnnotation(endpoint.getRequestMapping());
if (requestMappingAnnotation != null) {
return createRequestMappingInfo(requestMappingAnnotation, getCustomTypeCondition(endpoint.getClass()));
}
else {
return null;
}
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
CrossOrigin crossOrigin = ((BaseHttpInboundEndpoint) handler).getCrossOrigin();
if (crossOrigin != null) {
CorsConfiguration config = new CorsConfiguration();
for (String origin : crossOrigin.getOrigin()) {
config.addAllowedOrigin(origin);
}
for (RequestMethod requestMethod : crossOrigin.getMethod()) {
config.addAllowedMethod(requestMethod.name());
}
for (String header : crossOrigin.getAllowedHeaders()) {
config.addAllowedHeader(header);
}
for (String header : crossOrigin.getExposedHeaders()) {
config.addExposedHeader(header);
}
if (crossOrigin.getAllowCredentials() != null) {
config.setAllowCredentials(crossOrigin.getAllowCredentials());
}
if (crossOrigin.getMaxAge() != -1) {
config.setMaxAge(crossOrigin.getMaxAge());
}
if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
config.addAllowedMethod(allowedMethod.name());
}
}
if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
for (NameValueExpression<String> headerExpression : mappingInfo.getHeadersCondition().getExpressions()) {
if (!headerExpression.isNegated()) {
config.addAllowedHeader(headerExpression.getName());
}
}
}
return config.applyPermitDefaultValues();
}
return null;
}
@Override
public void afterPropertiesSet() {
// No-op in favor of onApplicationEvent
}
/**
* {@link HttpRequestHandlingEndpointSupport}s may depend on auto-created
* {@code requestChannel}s, so MVC Handlers detection should be postponed
* as late as possible.
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet()
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!this.initialized.getAndSet(true)) {
super.afterPropertiesSet();
}
}
}

View File

@@ -1,189 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.outbound;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ResponseEntity;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
/**
* A {@link MessageHandler} implementation that executes HTTP requests by delegating
* to a Reactive {@link WebClient} instance.
*
* @author Shiliang Li
* @author Artem Bilan
*
* @since 5.0
*
* @see HttpRequestExecutingMessageHandler
*/
public class ReactiveHttpRequestExecutingMessageHandler extends AbstractHttpRequestExecutingMessageHandler {
private final WebClient webClient;
/**
* Create a handler that will send requests to the provided URI.
* @param uri The URI.
*/
public ReactiveHttpRequestExecutingMessageHandler(URI uri) {
this(new ValueExpression<>(uri));
}
/**
* Create a handler that will send requests to the provided URI.
* @param uri The URI.
*/
public ReactiveHttpRequestExecutingMessageHandler(String uri) {
this(uri, null);
}
/**
* Create a handler that will send requests to the provided URI Expression.
* @param uriExpression The URI expression.
*/
public ReactiveHttpRequestExecutingMessageHandler(Expression uriExpression) {
this(uriExpression, null);
}
/**
* Create a handler that will send requests to the provided URI using a provided WebClient.
* @param uri The URI.
* @param webClient The WebClient to use.
*/
public ReactiveHttpRequestExecutingMessageHandler(String uri, WebClient webClient) {
this(new LiteralExpression(uri), webClient);
/*
* We'd prefer to do this assertion first, but the compiler doesn't allow it. However,
* it's safe because the literal expression simply wraps the String variable, even
* when null.
*/
Assert.hasText(uri, "URI is required");
}
/**
* Create a handler that will send requests to the provided URI using a provided WebClient.
* @param uriExpression A SpEL Expression that can be resolved against the message object and
* {@link BeanFactory}.
* @param webClient The WebClient to use.
*/
public ReactiveHttpRequestExecutingMessageHandler(Expression uriExpression, WebClient webClient) {
super(uriExpression);
this.webClient = (webClient == null ? WebClient.create() : webClient);
this.setAsync(true);
}
@Override
public String getComponentType() {
return (isExpectReply() ? "http:outbound-reactive-gateway" : "http:outbound-reactive-channel-adapter");
}
@Override
protected Object exchange(Supplier<URI> uriSupplier, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage) {
WebClient.RequestBodySpec requestSpec =
this.webClient.method(httpMethod)
.uri(b -> uriSupplier.get())
.headers(headers -> headers.putAll(httpRequest.getHeaders()));
if (httpRequest.hasBody()) {
requestSpec.body(BodyInserters.fromObject(httpRequest.getBody()));
}
Mono<ClientResponse> responseMono = requestSpec.exchange()
.doOnNext(response -> {
HttpStatus httpStatus = response.statusCode();
if (httpStatus.is4xxClientError() || httpStatus.is5xxServerError()) {
throw new WebClientResponseException(
String.format("ClientResponse has erroneous status code: %d %s",
response.statusCode().value(),
response.statusCode().getReasonPhrase()),
httpStatus.value(),
httpStatus.getReasonPhrase(),
response.headers()
.asHttpHeaders(),
response.body(BodyExtractors.toDataBuffers())
.reduce(DataBuffer::write)
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.block(),
response.headers()
.contentType()
.map(MimeType::getCharset)
.orElse(StandardCharsets.ISO_8859_1));
}
});
if (isExpectReply()) {
BodyExtractor<? extends Mono<?>, ReactiveHttpInputMessage> bodyExtractor;
if (expectedResponseType instanceof ParameterizedTypeReference<?>) {
bodyExtractor = BodyExtractors.toMono((ParameterizedTypeReference<?>) expectedResponseType);
}
else if (expectedResponseType != null) {
bodyExtractor = BodyExtractors.toMono((Class<?>) expectedResponseType);
}
else {
bodyExtractor = null;
}
return responseMono
.map(response ->
new ResponseEntity<>(bodyExtractor != null
? response.body(bodyExtractor).block()
: null,
response.headers().asHttpHeaders(),
response.statusCode()))
.map(this::getReply);
}
else {
responseMono.subscribe(v -> { }, ex -> sendErrorMessage(requestMessage, ex));
return null;
}
}
}

View File

@@ -48,27 +48,12 @@ public final class HttpContextUtils {
ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet",
HttpContextUtils.class.getClassLoader());
/**
* The {@code boolean} flag to indicate if the
* {@code org.springframework.web.reactive.result.method.RequestMappingInfo}
* is present in the CLASSPATH to allow to register the Integration server reactive components.
*/
public static final boolean WEB_FLUX_PRESENT =
ClassUtils.isPresent("org.springframework.web.reactive.result.method.RequestMappingInfo",
HttpContextUtils.class.getClassLoader());
/**
* The name for the infrastructure
* {@link org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping} bean.
*/
public static final String HANDLER_MAPPING_BEAN_NAME = "integrationRequestMappingHandlerMapping";
/**
* The name for the infrastructure
* {@link org.springframework.integration.http.inbound.ReactiveIntegrationRequestMappingHandlerMapping} bean.
*/
public static final String REACTIVE_HANDLER_MAPPING_BEAN_NAME = "reactiveIntegrationRequestMappingHandlerMapping";
/**
* Represents the environment property for the {@code IntegrationGraphController} request mapping path.
*/

View File

@@ -401,55 +401,6 @@
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-reactive-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Configures a Consumer Endpoint for the
'org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler'
with 'expectReply = false' that sends HTTP requests based on incoming messages.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="3">
<xsd:element name="uri-variable" type="uriVariableType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Specify an expression for URI variable placeholder within 'url'.
This element is mutually exclusive with 'uri-variables-expression' attribute.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="request-handler-advice-chain" type="integration:handlerAdviceChainType" minOccurs="0" maxOccurs="1" />
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
</xsd:choice>
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
<xsd:attributeGroup ref="httpOutboundCommonAttributes"/>
<xsd:attribute name="web-client" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.web.reactive.function.client.WebClient" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
A reference to an org.springframework.web.reactive.function.client.WebClient bean
which is used to send to send the HTTP Requests reactive manner.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-payload" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Specify whether the outbound message's payload should be extracted
when preparing the request body. Otherwise the Message instance itself
will be serialized.
The default value is 'true'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-gateway">
<xsd:annotation>
<xsd:documentation>
@@ -544,114 +495,6 @@
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-reactive-gateway">
<xsd:annotation>
<xsd:documentation>
Configures a Consumer Endpoint for the
'org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler'
that sends HTTP requests based on incoming messages and expects HTTP responses.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="gatewayType">
<xsd:choice minOccurs="0" maxOccurs="3">
<xsd:element name="uri-variable" type="uriVariableType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Specify an expression for URI variable placeholder within 'url'.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="transactional" type="integration:transactionalType" minOccurs="0"
maxOccurs="1" />
<xsd:element name="request-handler-advice-chain" type="integration:handlerAdviceChainType"
minOccurs="0" maxOccurs="1" />
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
</xsd:choice>
<xsd:attribute name="request-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.messaging.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
The receiving Message Channel of this endpoint.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mapped-response-headers" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Comma-separated list of names of HttpHeaders to be mapped from the HTTP response into the MessageHeaders.
This can only be provided if the 'header-mapper' reference is not being set directly. The values in
this list can also be simple patterns to be matched against the header names (e.g. "foo*" or "*foo").
The String "HTTP_RESPONSE_HEADERS" will match against any of the standard HTTP Response headers.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-request-payload" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Specifies whether the outbound message's payload should be extracted
when preparing the request body. Otherwise the Message instance itself
will be serialized.
The default value is 'true'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="transfer-cookies" type="xsd:string" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
When set to "true", if a response contains a 'Set-Cookie' header, it will be mapped to a 'Cookie' header. This enables simple
cookie handling where subsequent HTTP interactions in the same message flow can use a cookie
supplied by the server. Default is "false".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-timeout" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Allows you to specify how long this gateway will wait for
the reply message to be sent successfully to the reply channel
before throwing an exception. This attribute only applies when the
channel might block, for example when using a bounded queue channel that
is currently full.
Also, keep in mind that when sending to a DirectChannel, the
invocation will occur in the sender's thread. Therefore,
the failing of the send operation may be caused by other
components further downstream.
The "reply-timeout" attribute maps to the "sendTimeout" property of the
underlying 'MessagingTemplate' instance (org.springframework.integration.core.MessagingTemplate).
The attribute will default, if not specified, to '-1', meaning that
by default, the Gateway will wait indefinitely. The value is
specified in milliseconds.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="httpOutboundCommonAttributes"/>
<xsd:attribute name="web-client" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.web.reactive.function.client.WebClient" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
A reference to an org.springframework.web.reactive.function.client.WebClient bean
which is used to send to send the HTTP Requests reactive manner.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="graph-controller">
<xsd:annotation>
<xsd:documentation>

View File

@@ -17,16 +17,8 @@
<outbound-channel-adapter id="restTemplateConfig" url="http://localhost/test1" channel="requests"
rest-template="customRestTemplate"/>
<outbound-reactive-channel-adapter id="reactiveMinimalConfig" url="http://localhost/test1" channel="requests"/>
<outbound-reactive-channel-adapter id="reactiveWebClientConfig" url="http://localhost/test1" channel="requests"
web-client="webClient"/>
<beans:bean id="customRestTemplate" class="org.springframework.web.client.RestTemplate"/>
<beans:bean id="webClient" class="org.springframework.web.reactive.function.client.WebClient"
factory-method="create"/>
<outbound-channel-adapter id="fullConfig"
url="http://localhost/test2/{foo}"
http-method="GET"

View File

@@ -58,7 +58,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Mark Fisher
@@ -85,21 +84,10 @@ public class HttpOutboundChannelAdapterParserTests {
@Qualifier("restTemplateConfig")
private AbstractEndpoint restTemplateConfig;
@Autowired
@Qualifier("reactiveMinimalConfig")
private AbstractEndpoint reactiveMinimalConfig;
@Autowired
@Qualifier("reactiveWebClientConfig")
private AbstractEndpoint reactiveWebClientConfig;
@Autowired
@Qualifier("customRestTemplate")
private RestTemplate customRestTemplate;
@Autowired
private WebClient webClient;
@Autowired
@Qualifier("withUrlAndTemplate")
private AbstractEndpoint withUrlAndTemplate;
@@ -187,25 +175,6 @@ public class HttpOutboundChannelAdapterParserTests {
assertTrue(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader2"));
}
@Test
public void reactiveMinimalConfig() {
DirectFieldAccessor endpointAccessor = new DirectFieldAccessor(this.reactiveMinimalConfig);
WebClient webClient =
TestUtils.getPropertyValue(this.reactiveMinimalConfig, "handler.webClient", WebClient.class);
assertNotSame(this.webClient, webClient);
Object handler = endpointAccessor.getPropertyValue("handler");
DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(handler);
assertEquals(false, handlerAccessor.getPropertyValue("expectReply"));
assertEquals(this.applicationContext.getBean("requests"), endpointAccessor.getPropertyValue("inputChannel"));
assertNull(handlerAccessor.getPropertyValue("outputChannel"));
Expression uriExpression = (Expression) handlerAccessor.getPropertyValue("uriExpression");
assertEquals("http://localhost/test1", uriExpression.getValue());
assertEquals(HttpMethod.POST.name(),
TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString());
assertEquals(Charset.forName("UTF-8"), handlerAccessor.getPropertyValue("charset"));
assertEquals(true, handlerAccessor.getPropertyValue("extractPayload"));
}
@Test
public void restTemplateConfig() {
RestTemplate restTemplate =
@@ -213,11 +182,6 @@ public class HttpOutboundChannelAdapterParserTests {
assertEquals(customRestTemplate, restTemplate);
}
@Test
public void reactiveWebClientConfig() {
assertSame(this.webClient, TestUtils.getPropertyValue(this.reactiveWebClientConfig, "handler.webClient"));
}
@Test(expected = BeanDefinitionParsingException.class)
public void failWithRestTemplateAndRestAttributes() {
new ClassPathXmlApplicationContext("HttpOutboundChannelAdapterParserTests-fail-context.xml", this.getClass())

View File

@@ -14,9 +14,6 @@
<si:channel id="requests"/>
<beans:bean id="webClient" class="org.springframework.web.reactive.function.client.WebClient"
factory-method="create"/>
<outbound-gateway id="minimalConfig" url="http://localhost/test1" request-channel="requests"/>
<si:channel id="replies">
@@ -43,26 +40,6 @@
<uri-variable name="foo" expression="headers.bar"/>
</outbound-gateway>
<outbound-reactive-gateway id="reactiveMinimalConfig" url="http://localhost/test1" request-channel="requests"
web-client="webClient"/>
<outbound-reactive-gateway id="reactiveFullConfig"
url="http://localhost/test2"
http-method="PUT"
request-channel="requests"
reply-timeout="1234"
extract-request-payload="false"
expected-response-type="java.lang.String"
mapped-request-headers="requestHeader1, requestHeader2"
mapped-response-headers="responseHeader"
reply-channel="replies"
charset="UTF-8"
order="77"
auto-startup="false"
transfer-cookies="true">
<uri-variable name="foo" expression="headers.bar"/>
</outbound-reactive-gateway>
<util:map id="uriVariables">
<beans:entry key="foo1" value="bar1"/>
<beans:entry key="foo2" value="bar2"/>

View File

@@ -19,7 +19,6 @@ package org.springframework.integration.http.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -58,7 +57,6 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Mark Fisher
@@ -79,14 +77,6 @@ public class HttpOutboundGatewayParserTests {
@Qualifier("fullConfig")
private AbstractEndpoint fullConfigEndpoint;
@Autowired
@Qualifier("reactiveMinimalConfig")
private AbstractEndpoint reactiveMinimalConfigEndpoint;
@Autowired
@Qualifier("reactiveFullConfig")
private AbstractEndpoint reactiveFullConfigEndpoint;
@Autowired
@Qualifier("withUrlExpression")
private AbstractEndpoint withUrlExpressionEndpoint;
@@ -99,9 +89,6 @@ public class HttpOutboundGatewayParserTests {
@Qualifier("withPoller1")
private AbstractEndpoint withPoller1;
@Autowired
private WebClient webClient;
@Autowired
private ApplicationContext applicationContext;
@@ -178,66 +165,6 @@ public class HttpOutboundGatewayParserTests {
assertEquals(true, handlerAccessor.getPropertyValue("transferCookies"));
}
@Test
public void reactiveMinimalConfig() {
Object handler = new DirectFieldAccessor(this.reactiveMinimalConfigEndpoint).getPropertyValue("handler");
Object requestChannel = new DirectFieldAccessor(this.reactiveMinimalConfigEndpoint)
.getPropertyValue("inputChannel");
assertEquals(this.applicationContext.getBean("requests"), requestChannel);
DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(handler);
Object replyChannel = handlerAccessor.getPropertyValue("outputChannel");
assertNull(replyChannel);
assertSame(this.webClient, handlerAccessor.getPropertyValue("webClient"));
Expression uriExpression = (Expression) handlerAccessor.getPropertyValue("uriExpression");
assertEquals("http://localhost/test1", uriExpression.getValue());
assertEquals(HttpMethod.POST.name(),
TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString());
assertEquals(Charset.forName("UTF-8"), handlerAccessor.getPropertyValue("charset"));
assertEquals(true, handlerAccessor.getPropertyValue("extractPayload"));
assertEquals(false, handlerAccessor.getPropertyValue("transferCookies"));
}
@Test
@SuppressWarnings("unchecked")
public void reactiveFullConfig() {
DirectFieldAccessor endpointAccessor = new DirectFieldAccessor(this.reactiveFullConfigEndpoint);
Object handler = endpointAccessor.getPropertyValue("handler");
MessageChannel requestChannel = (MessageChannel) new DirectFieldAccessor(
this.reactiveFullConfigEndpoint).getPropertyValue("inputChannel");
assertEquals(this.applicationContext.getBean("requests"), requestChannel);
DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(handler);
assertEquals(77, handlerAccessor.getPropertyValue("order"));
assertEquals(Boolean.FALSE, endpointAccessor.getPropertyValue("autoStartup"));
Object replyChannel = handlerAccessor.getPropertyValue("outputChannel");
assertNotNull(replyChannel);
assertEquals(this.applicationContext.getBean("replies"), replyChannel);
assertEquals(String.class.getName(),
TestUtils.getPropertyValue(handler, "expectedResponseTypeExpression", Expression.class).getValue());
Expression uriExpression = (Expression) handlerAccessor.getPropertyValue("uriExpression");
assertEquals("http://localhost/test2", uriExpression.getValue());
assertEquals(HttpMethod.PUT.name(),
TestUtils.getPropertyValue(handler, "httpMethodExpression", Expression.class).getExpressionString());
assertEquals(Charset.forName("UTF-8"), handlerAccessor.getPropertyValue("charset"));
assertEquals(false, handlerAccessor.getPropertyValue("extractPayload"));
Object sendTimeout = new DirectFieldAccessor(
handlerAccessor.getPropertyValue("messagingTemplate")).getPropertyValue("sendTimeout");
assertEquals(new Long("1234"), sendTimeout);
Map<String, Expression> uriVariableExpressions =
(Map<String, Expression>) handlerAccessor.getPropertyValue("uriVariableExpressions");
assertEquals(1, uriVariableExpressions.size());
assertEquals("headers.bar", uriVariableExpressions.get("foo").getExpressionString());
DirectFieldAccessor mapperAccessor = new DirectFieldAccessor(handlerAccessor.getPropertyValue("headerMapper"));
String[] mappedRequestHeaders = (String[]) mapperAccessor.getPropertyValue("outboundHeaderNames");
String[] mappedResponseHeaders = (String[]) mapperAccessor.getPropertyValue("inboundHeaderNames");
assertEquals(2, mappedRequestHeaders.length);
assertEquals(1, mappedResponseHeaders.length);
assertTrue(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader1"));
assertTrue(ObjectUtils.containsElement(mappedRequestHeaders, "requestHeader2"));
assertEquals("responseHeader", mappedResponseHeaders[0]);
assertEquals(true, handlerAccessor.getPropertyValue("transferCookies"));
}
@Test
public void withUrlExpression() {
HttpRequestExecutingMessageHandler handler = (HttpRequestExecutingMessageHandler) new DirectFieldAccessor(

View File

@@ -16,9 +16,6 @@
package org.springframework.integration.http.dsl;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -30,28 +27,19 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.integration.security.channel.SecuredChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
@@ -63,20 +51,11 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
@@ -95,23 +74,14 @@ public class HttpDslTests {
@Autowired
private HttpRequestExecutingMessageHandler serviceInternalGatewayHandler;
@Autowired
private ReactiveHttpRequestExecutingMessageHandler serviceInternalReactiveGatewayHandler;
private MockMvc mockMvc;
private WebTestClient webTestClient;
@Before
public void setup() {
this.mockMvc =
MockMvcBuilders.webAppContextSetup(this.wac)
.apply(springSecurity())
.build();
this.webTestClient =
WebTestClient.bindToApplicationContext(this.wac)
.build();
}
@@ -130,71 +100,7 @@ public class HttpDslTests {
.string("FOO"));
}
@Test
public void testHttpReactiveProxyFlow() throws Exception {
ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
return response.writeWith(Mono.just(response.bufferFactory().wrap("FOO".getBytes())))
.then(Mono.defer(response::setComplete));
});
WebClient webClient = WebClient.builder()
.clientConnector(httpConnector)
.build();
new DirectFieldAccessor(this.serviceInternalReactiveGatewayHandler)
.setPropertyValue("webClient", webClient);
this.mockMvc.perform(
get("/service2")
.with(httpBasic("guest", "guest"))
.param("name", "foo"))
.andExpect(
content()
.string("FOO"));
}
@Autowired
private PollableChannel storeChannel;
@Test
@SuppressWarnings("unchecked")
public void testHttpReactivePost() {
this.webTestClient.post().uri("/reactivePost")
.body(Flux.just("foo", "bar", "baz"), String.class)
.exchange()
.expectStatus().isAccepted();
Message<?> store = this.storeChannel.receive(10_000);
assertNotNull(store);
assertThat(store.getPayload(), instanceOf(Flux.class));
StepVerifier
.create((Publisher<String>) store.getPayload())
.expectNext("foo", "bar", "baz")
.verifyComplete();
}
@Test
public void testSse() {
Flux<String> responseBody =
this.webTestClient.get().uri("/sse")
.exchange()
.returnResult(String.class)
.getResponseBody();
StepVerifier
.create(responseBody)
.expectNext("foo", "bar", "baz")
.verifyComplete();
}
@Configuration
@EnableWebFlux
@EnableWebSecurity
@EnableIntegration
public static class ContextConfiguration extends WebSecurityConfigurerAdapter {
@@ -247,42 +153,6 @@ public class HttpDslTests {
.get();
}
@Bean
public IntegrationFlow httpReactiveProxyFlow() {
return IntegrationFlows
.from(Http.inboundGateway("/service2")
.requestMapping(r -> r.params("name")))
.handle(Http.<MultiValueMap<String, String>>outboundReactiveGateway(m ->
UriComponentsBuilder.fromUriString("http://www.springsource.org/spring-integration")
.queryParams(m.getPayload())
.build()
.toUri())
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class),
e -> e.id("serviceInternalReactiveGateway"))
.get();
}
@Bean
public IntegrationFlow httpReactiveInboundChannelAdapterFlow() {
return IntegrationFlows
.from(Http.inboundReactiveChannelAdapter("/reactivePost")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(ResolvableType.forClassWithGenerics(Flux.class, String.class))
.statusCodeFunction(m -> HttpStatus.ACCEPTED))
.channel(c -> c.queue("storeChannel"))
.get();
}
@Bean
public IntegrationFlow sseFlow() {
return IntegrationFlows
.from(Http.inboundReactiveGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
.handle((p, h) -> Flux.just("foo", "bar", "baz"))
.get();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));

View File

@@ -1,170 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.inbound;
import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Flux;
/**
* @author Artem Bilan
*
* @since 5.0
*/
@RunWith(SpringRunner.class)
@DirtiesContext
public class ReactiveHttpInboundEndpointTests {
@Autowired
private WebTestClient webTestClient;
@Autowired
private ReactiveHttpInboundEndpoint simpleInboundEndpoint;
@Test
public void testSimpleGet() {
this.webTestClient.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("It works!");
this.simpleInboundEndpoint.stop();
this.webTestClient.get().uri("/test")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody(String.class).isEqualTo("Endpoint is stopped");
}
@Test
public void testJsonResult() {
this.webTestClient.get().uri("/persons")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
.jsonPath("$[2].name").isEqualTo("John");
}
@Configuration
@EnableWebFlux
@EnableIntegration
public static class ContextConfiguration {
@Bean
public WebTestClient webTestClient(ApplicationContext applicationContext) {
return WebTestClient.bindToApplicationContext(applicationContext).build();
}
@Bean
public ReactiveHttpInboundEndpoint simpleInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/test");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("serviceChannel");
return endpoint;
}
@ServiceActivator(inputChannel = "serviceChannel")
String service() {
return "It works!";
}
@Bean
public ReactiveHttpInboundEndpoint jsonInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/persons");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannel(fluxResultChannel());
return endpoint;
}
@Bean
public MessageChannel fluxResultChannel() {
return new FluxMessageChannel();
}
@ServiceActivator(inputChannel = "fluxResultChannel")
Flux<Person> getPersons() {
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
}
}
static class Person {
private final String name;
@JsonCreator
Person(@JsonProperty("name") String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return Objects.equals(this.name, person.name);
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public String toString() {
return "Person[name='" + this.name + "']";
}
}
}

View File

@@ -1,142 +0,0 @@
/*
* Copyright 2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http.outbound;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.springframework.integration.test.matcher.HeaderMatcher.hasHeader;
import java.util.List;
import org.junit.Test;
import org.reactivestreams.Subscriber;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.http.HttpHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Shiliang Li
* @author Artem Bilan
*
* @since 5.0
*/
public class ReactiveHttpRequestExecutingMessageHandlerTests {
@Test
public void testReactiveReturn() throws Throwable {
ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> {
response.setStatusCode(HttpStatus.OK);
return Mono.empty()
.then(Mono.defer(response::setComplete));
});
WebClient webClient = WebClient.builder()
.clientConnector(httpConnector)
.build();
String destinationUri = "http://www.springsource.org/spring-integration";
ReactiveHttpRequestExecutingMessageHandler reactiveHandler =
new ReactiveHttpRequestExecutingMessageHandler(destinationUri, webClient);
FluxMessageChannel ackChannel = new FluxMessageChannel();
reactiveHandler.setOutputChannel(ackChannel);
reactiveHandler.handleMessage(MessageBuilder.withPayload("hello, world").build());
reactiveHandler.handleMessage(MessageBuilder.withPayload("hello, world").build());
StepVerifier.create(ackChannel, 2)
.assertNext(m -> assertThat(m, hasHeader(HttpHeaders.STATUS_CODE, HttpStatus.OK)))
.assertNext(m -> assertThat(m, hasHeader(HttpHeaders.STATUS_CODE, HttpStatus.OK)))
.then(() ->
((Subscriber<?>) TestUtils.getPropertyValue(ackChannel, "subscribers", List.class).get(0))
.onComplete())
.verifyComplete();
}
@Test
public void testReactiveErrorOneWay() throws Throwable {
ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return Mono.empty()
.then(Mono.defer(response::setComplete));
});
WebClient webClient = WebClient.builder()
.clientConnector(httpConnector)
.build();
String destinationUri = "http://www.springsource.org/spring-integration";
ReactiveHttpRequestExecutingMessageHandler reactiveHandler =
new ReactiveHttpRequestExecutingMessageHandler(destinationUri, webClient);
reactiveHandler.setExpectReply(false);
QueueChannel errorChannel = new QueueChannel();
reactiveHandler.handleMessage(MessageBuilder.withPayload("hello, world")
.setErrorChannel(errorChannel)
.build());
Message<?> errorMessage = errorChannel.receive(10000);
assertNotNull(errorMessage);
assertThat(errorMessage, instanceOf(ErrorMessage.class));
Throwable throwable = (Throwable) errorMessage.getPayload();
assertThat(throwable.getMessage(), containsString("401 Unauthorized"));
}
@Test
public void testReactiveConnectErrorOneWay() throws Throwable {
ClientHttpConnector httpConnector = new HttpHandlerConnector((request, response) -> {
throw new RuntimeException("Intentional connection error");
});
WebClient webClient = WebClient.builder()
.clientConnector(httpConnector)
.build();
String destinationUri = "http://www.springsource.org/spring-integration";
ReactiveHttpRequestExecutingMessageHandler reactiveHandler =
new ReactiveHttpRequestExecutingMessageHandler(destinationUri, webClient);
reactiveHandler.setExpectReply(false);
QueueChannel errorChannel = new QueueChannel();
reactiveHandler.handleMessage(MessageBuilder.withPayload("hello, world")
.setErrorChannel(errorChannel)
.build());
Message<?> errorMessage = errorChannel.receive(10000);
assertNotNull(errorMessage);
assertThat(errorMessage, instanceOf(ErrorMessage.class));
Throwable throwable = (Throwable) errorMessage.getPayload();
assertThat(throwable.getMessage(), containsString("Intentional connection error"));
}
}