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

@@ -0,0 +1,83 @@
/*
* 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.webflux.config;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.webflux.inbound.IntegrationHandlerResultHandler;
import org.springframework.integration.webflux.inbound.WebFluxIntegrationRequestMappingHandlerMapping;
import org.springframework.integration.webflux.support.WebFluxContextUtils;
/**
* The WebFlux Integration infrastructure {@code beanFactory} initializer.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class WebFluxIntegrationConfigurationInitializer implements IntegrationConfigurationInitializer {
private static final Log logger = LogFactory.getLog(WebFluxIntegrationConfigurationInitializer.class);
@Override
public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof BeanDefinitionRegistry) {
registerReactiveRequestMappingHandlerMappingIfNecessary((BeanDefinitionRegistry) beanFactory);
}
else {
logger.warn("'IntegrationRequestMappingHandlerMapping' isn't registered because 'beanFactory'" +
" isn't an instance of `BeanDefinitionRegistry`.");
}
}
/**
* Registers a {@link WebFluxIntegrationRequestMappingHandlerMapping}
* which could also be overridden by the user by simply registering
* a {@link WebFluxIntegrationRequestMappingHandlerMapping} {@code <bean>} with 'id'
* {@link WebFluxContextUtils#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 (WebFluxContextUtils.WEB_FLUX_PRESENT &&
!registry.containsBeanDefinition(WebFluxContextUtils.HANDLER_MAPPING_BEAN_NAME)) {
BeanDefinitionBuilder requestMappingBuilder =
BeanDefinitionBuilder.genericBeanDefinition(WebFluxIntegrationRequestMappingHandlerMapping.class);
requestMappingBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
requestMappingBuilder.addPropertyValue(IntegrationNamespaceUtils.ORDER, 0);
registry.registerBeanDefinition(WebFluxContextUtils.HANDLER_MAPPING_BEAN_NAME,
requestMappingBuilder.getBeanDefinition());
BeanDefinitionReaderUtils.registerWithGeneratedName(
new RootBeanDefinition(IntegrationHandlerResultHandler.class), registry);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-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.webflux.config;
import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler;
/**
* Namespace handler for Spring Integration's <em>webflux</em> namespace.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class WebFluxNamespaceHandler extends AbstractIntegrationNamespaceHandler {
public void init() {
registerBeanDefinitionParser("outbound-channel-adapter", new WebFluxOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-gateway", new WebFluxOutboundGatewayParser());
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2002-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.webflux.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.http.config.HttpOutboundChannelAdapterParser;
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
import org.springframework.util.StringUtils;
/**
* Parser for the 'outbound-channel-adapter' element of the webflux namespace.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class WebFluxOutboundChannelAdapterParser extends HttpOutboundChannelAdapterParser {
@Override
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(WebFluxRequestExecutingMessageHandler.class);
String webClientRef = element.getAttribute("web-client");
if (StringUtils.hasText(webClientRef)) {
builder.getBeanDefinition()
.getConstructorArgumentValues()
.addIndexedArgumentValue(1, new RuntimeBeanReference(webClientRef));
}
return builder;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2002-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.webflux.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.http.config.HttpOutboundGatewayParser;
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
import org.springframework.util.StringUtils;
/**
* Parser for the 'outbound-gateway' element of the webflux namespace.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class WebFluxOutboundGatewayParser extends HttpOutboundGatewayParser {
@Override
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(WebFluxRequestExecutingMessageHandler.class);
String webClientRef = element.getAttribute("web-client");
if (StringUtils.hasText(webClientRef)) {
builder.getBeanDefinition()
.getConstructorArgumentValues()
.addIndexedArgumentValue(1, new RuntimeBeanReference(webClientRef));
}
return builder;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides classes for configuration - parsers, namespace handlers.
*/
package org.springframework.integration.webflux.config;

View File

@@ -0,0 +1,245 @@
/*
* 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.webflux.dsl;
import java.net.URI;
import java.util.function.Function;
import org.springframework.expression.Expression;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.webflux.inbound.WebFluxInboundEndpoint;
import org.springframework.messaging.Message;
import org.springframework.web.reactive.function.client.WebClient;
/**
* The WebFlux components Factory.
*
* @author Artem Bilan
* @author Shiliang Li
*
* @since 5.0
*/
public final class WebFlux {
/**
* Create an {@link WebFluxMessageHandlerSpec} builder for one-way adapter based on provided {@link URI}.
* @param uri the {@link URI} to send requests.
* @return the WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundChannelAdapter(URI uri) {
return outboundChannelAdapter(uri, null);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} builder for one-way adapter based on provided {@code uri}.
* @param uri the {@code uri} to send requests.
* @return the WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundChannelAdapter(String uri) {
return outboundChannelAdapter(uri, null);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static <P> WebFluxMessageHandlerSpec outboundChannelAdapter(Function<Message<P>, ?> uriFunction) {
return outboundChannelAdapter(new FunctionExpression<>(uriFunction));
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundChannelAdapter(Expression uriExpression) {
return outboundChannelAdapter(uriExpression, null);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundChannelAdapter(URI uri, WebClient webClient) {
return new WebFluxMessageHandlerSpec(uri, webClient)
.expectReply(false);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundChannelAdapter(String uri, WebClient webClient) {
return new WebFluxMessageHandlerSpec(uri, webClient)
.expectReply(false);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static <P> WebFluxMessageHandlerSpec outboundChannelAdapter(Function<Message<P>, ?> uriFunction,
WebClient webClient) {
return outboundChannelAdapter(new FunctionExpression<>(uriFunction), webClient);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundChannelAdapter(Expression uriExpression,
WebClient webClient) {
return new WebFluxMessageHandlerSpec(uriExpression, webClient)
.expectReply(false);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} builder for request-reply gateway
* based on provided {@link URI}.
* @param uri the {@link URI} to send requests.
* @return the WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundGateway(URI uri) {
return outboundGateway(uri, null);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} builder for request-reply gateway
* based on provided {@code uri}.
* @param uri the {@code uri} to send requests.
* @return the WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundGateway(String uri) {
return outboundGateway(uri, null);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static <P> WebFluxMessageHandlerSpec outboundGateway(Function<Message<P>, ?> uriFunction) {
return outboundGateway(new FunctionExpression<>(uriFunction));
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundGateway(Expression uriExpression) {
return outboundGateway(uriExpression, null);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundGateway(URI uri, WebClient webClient) {
return new WebFluxMessageHandlerSpec(uri, webClient);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundGateway(String uri, WebClient webClient) {
return new WebFluxMessageHandlerSpec(uri, webClient);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static <P> WebFluxMessageHandlerSpec outboundGateway(Function<Message<P>, ?> uriFunction,
WebClient webClient) {
return outboundGateway(new FunctionExpression<>(uriFunction), webClient);
}
/**
* Create an {@link WebFluxMessageHandlerSpec} 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 WebFluxMessageHandlerSpec instance
*/
public static WebFluxMessageHandlerSpec outboundGateway(Expression uriExpression,
WebClient webClient) {
return new WebFluxMessageHandlerSpec(uriExpression, webClient);
}
/**
* Create an {@link WebFluxInboundEndpointSpec} 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 WebFluxInboundEndpointSpec instance
*/
public static WebFluxInboundEndpointSpec inboundChannelAdapter(String... path) {
WebFluxInboundEndpoint httpInboundChannelAdapter = new WebFluxInboundEndpoint(false);
return new WebFluxInboundEndpointSpec(httpInboundChannelAdapter, path);
}
/**
* Create an {@link WebFluxInboundEndpointSpec} 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 WebFluxInboundEndpointSpec instance
*/
public static WebFluxInboundEndpointSpec inboundGateway(String... path) {
return new WebFluxInboundEndpointSpec(new WebFluxInboundEndpoint(), path);
}
private WebFlux() {
super();
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.webflux.dsl;
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.web.reactive.accept.RequestedContentTypeResolver;
/**
* The {@link HttpInboundEndpointSupportSpec} implementation for the {@link WebFluxInboundEndpoint}.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class WebFluxInboundEndpointSpec
extends HttpInboundEndpointSupportSpec<WebFluxInboundEndpointSpec, WebFluxInboundEndpoint> {
WebFluxInboundEndpointSpec(WebFluxInboundEndpoint gateway, String... path) {
super(gateway, path);
}
public WebFluxInboundEndpointSpec codecConfigurer(ServerCodecConfigurer codecConfigurer) {
this.target.setCodecConfigurer(codecConfigurer);
return this;
}
public WebFluxInboundEndpointSpec requestedContentTypeResolver(
RequestedContentTypeResolver requestedContentTypeResolver) {
this.target.setRequestedContentTypeResolver(requestedContentTypeResolver);
return this;
}
public WebFluxInboundEndpointSpec reactiveAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
this.target.setReactiveAdapterRegistry(adapterRegistry);
return this;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.webflux.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.dsl.BaseHttpMessageHandlerSpec;
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
import org.springframework.web.reactive.function.client.WebClient;
/**
* The {@link BaseHttpMessageHandlerSpec} implementation for the {@link WebFluxRequestExecutingMessageHandler}.
*
* @author Shiliang Li
* @author Artem Bilan
*
* @since 5.0
*
* @see WebFluxRequestExecutingMessageHandler
*/
public class WebFluxMessageHandlerSpec
extends BaseHttpMessageHandlerSpec<WebFluxMessageHandlerSpec, WebFluxRequestExecutingMessageHandler> {
private final WebClient webClient;
WebFluxMessageHandlerSpec(URI uri, WebClient webClient) {
this(new ValueExpression<>(uri), webClient);
}
WebFluxMessageHandlerSpec(String uri, WebClient webClient) {
this(new LiteralExpression(uri), webClient);
}
WebFluxMessageHandlerSpec(Expression uriExpression, WebClient webClient) {
super(new WebFluxRequestExecutingMessageHandler(uriExpression, webClient));
this.webClient = webClient;
}
@Override
protected boolean isClientSet() {
return this.webClient != null;
}
@Override
protected WebFluxMessageHandlerSpec expectReply(boolean expectReply) {
return super.expectReply(expectReply);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides WebFlux Components support for Spring Integration Java DSL.
*/
package org.springframework.integration.webflux.dsl;

View File

@@ -0,0 +1,58 @@
/*
* 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.webflux.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 WebFluxInboundEndpoint} execution. Actually just return the
* {@code result.getReturnValue()} which essentially is expected {@code Mono<Void>}.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see WebFluxInboundEndpoint
*/
public class IntegrationHandlerResultHandler implements HandlerResultHandler, Ordered {
@Override
public boolean supports(HandlerResult result) {
Object handler = result.getHandler();
return handler instanceof HandlerMethod
&& WebFluxInboundEndpoint.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

@@ -0,0 +1,462 @@
/*
* 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.webflux.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.http.inbound.BaseHttpInboundEndpoint;
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 WebFluxInboundEndpoint 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 WebFluxInboundEndpoint() {
this(true);
}
public WebFluxInboundEndpoint(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:)", "$1webflux-");
}
@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

@@ -0,0 +1,175 @@
/*
* 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.webflux.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.inbound.BaseHttpInboundEndpoint;
import org.springframework.integration.http.inbound.CrossOrigin;
import org.springframework.integration.http.inbound.RequestMapping;
import org.springframework.integration.http.support.HttpContextUtils;
import org.springframework.integration.webflux.support.WebFluxContextUtils;
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.reactive.HandlerMapping} implementation that
* detects and registers {@link org.springframework.web.reactive.result.method.RequestMappingInfo}s for
* {@link org.springframework.integration.http.inbound.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 <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 WebFluxContextUtils#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 org.springframework.integration.http.inbound.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 WebFluxIntegrationRequestMappingHandlerMapping} 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 WebFluxIntegrationRequestMappingHandlerMapping 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 WebFluxInboundEndpoint.class.isAssignableFrom(beanType);
}
@Override
protected void detectHandlerMethods(Object handler) {
if (handler instanceof String) {
handler = getApplicationContext().getBean((String) handler);
}
RequestMappingInfo mapping = getMappingForEndpoint((WebFluxInboundEndpoint) 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(WebFluxInboundEndpoint 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;
}
/**
* {@link org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport}s
* may depend on auto-created {@code requestChannel}s, so MVC Handlers detection should be postponed
* as late as possible.
* @see RequestMappingHandlerMapping#afterPropertiesSet()
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!this.initialized.getAndSet(true)) {
super.afterPropertiesSet();
}
}
@Override
public void afterPropertiesSet() {
// No-op in favor of onApplicationEvent
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides classes supporting inbound endpoints.
*/
package org.springframework.integration.webflux.inbound;

View File

@@ -0,0 +1,190 @@
/*
* 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.webflux.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.integration.http.outbound.AbstractHttpRequestExecutingMessageHandler;
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 org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler
*/
public class WebFluxRequestExecutingMessageHandler extends AbstractHttpRequestExecutingMessageHandler {
private final WebClient webClient;
/**
* Create a handler that will send requests to the provided URI.
* @param uri The URI.
*/
public WebFluxRequestExecutingMessageHandler(URI uri) {
this(new ValueExpression<>(uri));
}
/**
* Create a handler that will send requests to the provided URI.
* @param uri The URI.
*/
public WebFluxRequestExecutingMessageHandler(String uri) {
this(uri, null);
}
/**
* Create a handler that will send requests to the provided URI Expression.
* @param uriExpression The URI expression.
*/
public WebFluxRequestExecutingMessageHandler(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 WebFluxRequestExecutingMessageHandler(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 WebFluxRequestExecutingMessageHandler(Expression uriExpression, WebClient webClient) {
super(uriExpression);
this.webClient = (webClient == null ? WebClient.create() : webClient);
this.setAsync(true);
}
@Override
public String getComponentType() {
return (isExpectReply() ? "webflux:outbound-gateway" : "webflux:outbound-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

@@ -0,0 +1,4 @@
/**
* Provides classes supporting outbound endpoints.
*/
package org.springframework.integration.webflux.outbound;

View File

@@ -0,0 +1,50 @@
/*
* 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.webflux.support;
import org.springframework.util.ClassUtils;
/**
* Utility class for accessing WebFlux integration components
* from the {@link org.springframework.beans.factory.BeanFactory}.
*
* @author Artem Bilan
*
* @since 5.0
*/
public final class WebFluxContextUtils {
private WebFluxContextUtils() {
super();
}
/**
* 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",
WebFluxContextUtils.class.getClassLoader());
/**
* The name for the infrastructure
* {@link org.springframework.integration.webflux.inbound.WebFluxIntegrationRequestMappingHandlerMapping} bean.
*/
public static final String HANDLER_MAPPING_BEAN_NAME = "webFluxIntegrationRequestMappingHandlerMapping";
}

View File

@@ -0,0 +1,4 @@
/**
* Provides classes to support WebFlux endpoints.
*/
package org.springframework.integration.webflux.support;

View File

@@ -0,0 +1,2 @@
org.springframework.integration.config.IntegrationConfigurationInitializer=\
org.springframework.integration.webflux.config.WebFluxIntegrationConfigurationInitializer

View File

@@ -0,0 +1 @@
http\://www.springframework.org/schema/integration/webflux=org.springframework.integration.webflux.config.WebFluxNamespaceHandler

View File

@@ -0,0 +1,2 @@
http\://www.springframework.org/schema/integration/webflux/spring-integration-webflux-5.0.xsd=org/springframework/integration/webflux/config/spring-integration-webflux-5.0.xsd
http\://www.springframework.org/schema/integration/webflux/spring-integration-webflux.xsd=org/springframework/integration/webflux/config/spring-integration-webflux-5.0.xsd

View File

@@ -0,0 +1,4 @@
# Tooling related information for the integration webflux namespace
http\://www.springframework.org/schema/integration/webflux@name=integration webflux Namespace
http\://www.springframework.org/schema/integration/webflux@prefix=int-webflux
http\://www.springframework.org/schema/integration/webflux@icon=org/springframework/integration/webflux/config/spring-integration-webflux.gif

View File

@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.springframework.org/schema/integration/webflux"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:integration="http://www.springframework.org/schema/integration"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
targetNamespace="http://www.springframework.org/schema/integration/webflux" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:import namespace="http://www.springframework.org/schema/tool"/>
<xsd:import namespace="http://www.springframework.org/schema/integration"
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-5.0.xsd" />
<xsd:import namespace="http://www.springframework.org/schema/integration/http"
schemaLocation="http://www.springframework.org/schema/integration/http/spring-integration-http-5.0.xsd"/>
<xsd:annotation>
<xsd:documentation><![CDATA[
Defines the configuration elements for Spring Integration's WebFlux adapters.
]]></xsd:documentation>
</xsd:annotation>
<xsd:element name="outbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Configures a Consumer Endpoint for the
'org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler'
with 'expectReply = false' that sends HTTP requests based on incoming messages reactive manner.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="3">
<xsd:element name="uri-variable" type="int-http: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"/>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
</xsd:choice>
<xsd:attributeGroup ref="integration:channelAdapterAttributes"/>
<xsd:attributeGroup ref="int-http: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>
Configures a Consumer Endpoint for the
'org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler'
that sends HTTP requests based on incoming messages and expects HTTP responses.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="int-http:gatewayType">
<xsd:choice minOccurs="0" maxOccurs="3">
<xsd:element name="uri-variable" type="int-http: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"/>
<xsd:element name="request-handler-advice-chain" type="integration:handlerAdviceChainType"
minOccurs="0"/>
<xsd:element ref="integration:poller" minOccurs="0"/>
</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="int-http: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:schema>