Support RouterFunction in @EnableWebFlux

This commit removes the RouterFunctions.toHandlerMapping method, in
favor of native support for RouterFunctions in @EnableWebFlux
configuration classes.

In order to accomplish this, the HandlerStrategies components has been
repurposed to only be used for the "bare-bones" HttpHandler, while the
(newly introduced) RouterFunctionMapping uses the strategies as exposed
through WebFluxConfigurationSupport.

Furthermore, this commit also introduces support for testing
RouterFunctions without resorting to an application context.

Issue: SPR-15536
This commit is contained in:
Arjen Poutsma
2017-05-11 14:57:01 +02:00
parent 3d290165fb
commit 01e3561db9
21 changed files with 647 additions and 247 deletions

View File

@@ -0,0 +1,58 @@
/*
* 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.test.web.reactive.server;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Spec for setting up server-less testing against a RouterFunction.
*
* @author Arjen Poutsma
* @since 5.0
*/
class DefaultRouterFunctionSpec extends AbstractMockServerSpec<WebTestClient.RouterFunctionSpec>
implements WebTestClient.RouterFunctionSpec {
private final RouterFunction<?> routerFunction;
private HandlerStrategies handlerStrategies = HandlerStrategies.withDefaults();
DefaultRouterFunctionSpec(RouterFunction<?> routerFunction) {
this.routerFunction = routerFunction;
}
@Override
public WebTestClient.RouterFunctionSpec handlerStrategies(HandlerStrategies handlerStrategies) {
if (handlerStrategies != null) {
this.handlerStrategies = handlerStrategies;
}
return this;
}
@Override
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
WebHandler webHandler = RouterFunctions.toWebHandler(this.routerFunction, this.handlerStrategies);
return WebHttpHandlerBuilder.webHandler(webHandler);
}
}

View File

@@ -1,62 +0,0 @@
/*
* 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.test.web.reactive.server;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.HandlerAdapter;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter;
import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Spec for setting up server-less testing against a RouterFunction.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class RouterFunctionSpec extends AbstractMockServerSpec<RouterFunctionSpec> {
private final RouterFunction<?> routerFunction;
RouterFunctionSpec(RouterFunction<?> routerFunction) {
this.routerFunction = routerFunction;
}
@Override
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
return WebHttpHandlerBuilder.applicationContext(initApplicationContext());
}
private ApplicationContext initApplicationContext() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean("webHandler", DispatcherHandler.class, () -> new DispatcherHandler());
context.registerBean(HandlerMapping.class, () -> RouterFunctions.toHandlerMapping(this.routerFunction));
context.registerBean(HandlerAdapter.class, () -> new HandlerFunctionAdapter());
context.registerBean(HandlerResultHandler.class, () -> new ServerResponseResultHandler());
context.refresh();
return context;
}
}

View File

@@ -48,6 +48,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
import org.springframework.web.server.ServerWebExchange;
@@ -169,8 +170,8 @@ public interface WebTestClient {
* @param routerFunction the RouterFunction to test
* @return the {@link WebTestClient} builder
*/
static MockServerSpec<?> bindToRouterFunction(RouterFunction<?> routerFunction) {
return new RouterFunctionSpec(routerFunction);
static RouterFunctionSpec bindToRouterFunction(RouterFunction<?> routerFunction) {
return new DefaultRouterFunctionSpec(routerFunction);
}
/**
@@ -285,6 +286,18 @@ public interface WebTestClient {
}
/**
* Specification for customizing router function configuration.
*/
interface RouterFunctionSpec extends MockServerSpec<RouterFunctionSpec> {
/**
* Configure handler strategies.
*/
RouterFunctionSpec handlerStrategies(HandlerStrategies handlerStrategies);
}
/**
* Steps for customizing the {@link WebClient} used to test with
* internally delegating to a {@link WebClient.Builder}.

View File

@@ -47,6 +47,9 @@ import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter;
import org.springframework.web.reactive.function.server.support.RouterFunctionMapping;
import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler;
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
import org.springframework.web.reactive.result.SimpleHandlerAdapter;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
@@ -81,6 +84,8 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
private PathMatchConfigurer pathMatchConfigurer;
private ViewResolverRegistry viewResolverRegistry;
private ApplicationContext applicationContext;
@@ -203,6 +208,23 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
public void configurePathMatching(PathMatchConfigurer configurer) {
}
@Bean
public RouterFunctionMapping routerFunctionMapping() {
RouterFunctionMapping mapping = createRouterFunctionMapping();
mapping.setOrder(-1); // go before RequestMappingHandlerMapping
mapping.setMessageCodecConfigurer(serverCodecConfigurer());
mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}
/**
* Override to plug a sub-class of {@link RouterFunctionMapping}.
*/
protected RouterFunctionMapping createRouterFunctionMapping() {
return new RouterFunctionMapping();
}
/**
* Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped
* resource handlers. To configure resource handling, override
@@ -362,6 +384,11 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
return null;
}
@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
return new HandlerFunctionAdapter();
}
@Bean
public SimpleHandlerAdapter simpleHandlerAdapter() {
return new SimpleHandlerAdapter();
@@ -381,15 +408,38 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
@Bean
public ViewResolutionResultHandler viewResolutionResultHandler() {
ViewResolverRegistry registry = new ViewResolverRegistry(getApplicationContext());
configureViewResolvers(registry);
ViewResolverRegistry registry = getViewResolverRegistry();
List<ViewResolver> resolvers = registry.getViewResolvers();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(
resolvers, webFluxContentTypeResolver(), webFluxAdapterRegistry());
handler.setDefaultViews(registry.getDefaultViews());
handler.setOrder(registry.getOrder());
return handler;
}
@Bean
public ServerResponseResultHandler serverResponseResultHandler() {
ViewResolverRegistry registry = getViewResolverRegistry();
List<ViewResolver> resolvers = registry.getViewResolvers();
ServerResponseResultHandler handler = new ServerResponseResultHandler();
handler.setMessageCodecConfigurer(serverCodecConfigurer());
handler.setViewResolvers(resolvers);
handler.setOrder(registry.getOrder() + 1);
return handler;
}
/**
* Callback for building the {@link ViewResolverRegistry}. This method is final,
* use {@link #configureViewResolvers} to customize view resolvers.
*/
protected final ViewResolverRegistry getViewResolverRegistry() {
if (this.viewResolverRegistry == null) {
this.viewResolverRegistry = new ViewResolverRegistry(getApplicationContext());
configureViewResolvers(this.viewResolverRegistry);
}
return this.viewResolverRegistry;
}
/**

View File

@@ -203,13 +203,13 @@ class DefaultEntityResponseBuilder<T> implements EntityResponse.Builder<T> {
}
@Override
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
ServerHttpResponse response = exchange.getResponse();
writeStatusAndHeaders(response);
return inserter().insert(response, new BodyInserter.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters();
return context.messageWriters();
}
@Override

View File

@@ -19,10 +19,7 @@ package org.springframework.web.reactive.function.server;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -44,18 +41,10 @@ import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
*/
class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
static final Function<ServerRequest, Optional<Locale>> DEFAULT_LOCALE_RESOLVER =
request -> request.headers().acceptLanguage().stream()
.map(Locale.LanguageRange::getRange)
.map(Locale::forLanguageTag).findFirst();
private final ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
private final List<ViewResolver> viewResolvers = new ArrayList<>();
private Function<ServerRequest, Optional<Locale>> localeResolver;
private final List<WebFilter> webFilters = new ArrayList<>();
private final List<WebExceptionHandler> exceptionHandlers = new ArrayList<>();
@@ -67,7 +56,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
public void defaultConfiguration() {
this.codecConfigurer.registerDefaults(true);
localeResolver(DEFAULT_LOCALE_RESOLVER);
exceptionHandler(new ResponseStatusExceptionHandler());
}
@@ -94,13 +82,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
return this;
}
@Override
public HandlerStrategies.Builder localeResolver(Function<ServerRequest, Optional<Locale>> localeResolver) {
Assert.notNull(localeResolver, "'localeResolver' must not be null");
this.localeResolver = localeResolver;
return this;
}
@Override
public HandlerStrategies.Builder webFilter(WebFilter filter) {
Assert.notNull(filter, "'filter' must not be null");
@@ -118,8 +99,8 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
@Override
public HandlerStrategies build() {
return new DefaultHandlerStrategies(this.codecConfigurer.getReaders(),
this.codecConfigurer.getWriters(), this.viewResolvers, this.localeResolver,
this.webFilters, this.exceptionHandlers);
this.codecConfigurer.getWriters(), this.viewResolvers, this.webFilters,
this.exceptionHandlers);
}
@@ -131,8 +112,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
private final List<ViewResolver> viewResolvers;
private final Function<ServerRequest, Optional<Locale>> localeResolver;
private final List<WebFilter> webFilters;
private final List<WebExceptionHandler> exceptionHandlers;
@@ -141,14 +120,12 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
List<HttpMessageReader<?>> messageReaders,
List<HttpMessageWriter<?>> messageWriters,
List<ViewResolver> viewResolvers,
Function<ServerRequest, Optional<Locale>> localeResolver,
List<WebFilter> webFilters,
List<WebExceptionHandler> exceptionHandlers) {
this.messageReaders = unmodifiableCopy(messageReaders);
this.messageWriters = unmodifiableCopy(messageWriters);
this.viewResolvers = unmodifiableCopy(viewResolvers);
this.localeResolver = localeResolver;
this.webFilters = unmodifiableCopy(webFilters);
this.exceptionHandlers = unmodifiableCopy(exceptionHandlers);
}
@@ -172,11 +149,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
return this.viewResolvers::stream;
}
@Override
public Supplier<Function<ServerRequest, Optional<Locale>>> localeResolver() {
return () -> this.localeResolver;
}
@Override
public Supplier<Stream<WebFilter>> webFilters() {
return this.webFilters::stream;

View File

@@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;
@@ -158,12 +159,12 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder {
}
@Override
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
ServerHttpResponse response = exchange.getResponse();
writeStatusAndHeaders(response);
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
Locale locale = resolveLocale(exchange, strategies);
Stream<ViewResolver> viewResolverStream = strategies.viewResolvers().get();
Locale locale = resolveLocale(exchange);
Stream<ViewResolver> viewResolverStream = context.viewResolvers().get();
return Flux.fromStream(viewResolverStream)
.concatMap(viewResolver -> viewResolver.resolveViewName(name(), locale))
@@ -173,15 +174,9 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder {
.flatMap(view -> view.render(model(), contentType, exchange));
}
private Locale resolveLocale(ServerWebExchange exchange, HandlerStrategies strategies) {
ServerRequest request =
exchange.<ServerRequest>getAttribute(RouterFunctions.REQUEST_ATTRIBUTE)
.orElseThrow(() -> new IllegalStateException(
"Could not find ServerRequest in exchange attributes"));
return strategies.localeResolver().get()
.apply(request)
.orElse(Locale.getDefault());
private Locale resolveLocale(ServerWebExchange exchange) {
List<Locale> locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
return locales.isEmpty() ? Locale.getDefault() : locales.get(0);
}
}

View File

@@ -169,7 +169,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
@Override
public Mono<ServerResponse> build(
BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction) {
BiFunction<ServerWebExchange, ServerResponse.Context, Mono<Void>> writeFunction) {
Assert.notNull(writeFunction, "'writeFunction' must not be null");
return Mono.just(new WriterFunctionServerResponse(this.statusCode, this.headers, writeFunction));
@@ -277,19 +277,19 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
private static final class WriterFunctionServerResponse extends AbstractServerResponse {
private final BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction;
private final BiFunction<ServerWebExchange, Context, Mono<Void>> writeFunction;
public WriterFunctionServerResponse(HttpStatus statusCode, HttpHeaders headers,
BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction) {
BiFunction<ServerWebExchange, Context, Mono<Void>> writeFunction) {
super(statusCode, headers);
this.writeFunction = writeFunction;
}
@Override
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
writeStatusAndHeaders(exchange.getResponse());
return this.writeFunction.apply(exchange, strategies);
return this.writeFunction.apply(exchange, context);
}
}
@@ -309,13 +309,13 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
}
@Override
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
public Mono<Void> writeTo(ServerWebExchange exchange, Context context) {
ServerHttpResponse response = exchange.getResponse();
writeStatusAndHeaders(response);
return this.inserter.insert(response, new BodyInserter.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters();
return context.messageWriters();
}
@Override

View File

@@ -16,10 +16,7 @@
package org.springframework.web.reactive.function.server;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -40,7 +37,6 @@ import org.springframework.web.server.WebFilter;
* @author Juergen Hoeller
* @since 5.0
* @see RouterFunctions#toHttpHandler(RouterFunction, HandlerStrategies)
* @see RouterFunctions#toHandlerMapping(RouterFunction, HandlerStrategies)
*/
public interface HandlerStrategies {
@@ -67,12 +63,6 @@ public interface HandlerStrategies {
*/
Supplier<Stream<ViewResolver>> viewResolvers();
/**
* Supply a function that resolves the locale of a given {@link ServerRequest}.
* @return the locale resolver
*/
Supplier<Function<ServerRequest, Optional<Locale>>> localeResolver();
/**
* Supply a {@linkplain Stream stream} of {@link WebFilter}s to be used for filtering the
* request and response.
@@ -147,13 +137,6 @@ public interface HandlerStrategies {
*/
Builder viewResolver(ViewResolver viewResolver);
/**
* Set the given function as {@link Locale} resolver for this builder.
* @param localeResolver the locale resolver to set
* @return this builder
*/
Builder localeResolver(Function<ServerRequest, Optional<Locale>> localeResolver);
/**
* Add the given web filter to this builder.
* @param filter the filter to add

View File

@@ -19,17 +19,17 @@ package org.springframework.web.reactive.function.server;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter;
import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@@ -92,18 +92,7 @@ public abstract class RouterFunctions {
Assert.notNull(predicate, "'predicate' must not be null");
Assert.notNull(handlerFunction, "'handlerFunction' must not be null");
return request -> {
if (predicate.test(request)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Predicate \"%s\" matches against \"%s\"",
predicate, request));
}
return Mono.just(handlerFunction);
}
else {
return Mono.empty();
}
};
return new DefaultRouterFunction<>(predicate, handlerFunction);
}
/**
@@ -135,17 +124,7 @@ public abstract class RouterFunctions {
Assert.notNull(predicate, "'predicate' must not be null");
Assert.notNull(routerFunction, "'routerFunction' must not be null");
return request -> predicate.nest(request)
.map(nestedRequest -> {
if (logger.isDebugEnabled()) {
logger.debug(
String.format("Nested predicate \"%s\" matches against \"%s\"",
predicate, request));
}
return routerFunction.route(nestedRequest);
}
)
.orElseGet(Mono::empty);
return new DefaultNestedRouterFunction<>(predicate, routerFunction);
}
/**
@@ -219,19 +198,32 @@ public abstract class RouterFunctions {
Assert.notNull(routerFunction, "RouterFunction must not be null");
Assert.notNull(strategies, "HandlerStrategies must not be null");
WebHandler webHandler = exchange -> {
WebHandler webHandler = toWebHandler(routerFunction, strategies);
WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.webHandler(webHandler);
strategies.webFilters().get().forEach(handlerBuilder::filter);
strategies.exceptionHandlers().get().forEach(handlerBuilder::exceptionHandler);
return handlerBuilder.build();
}
/**
* Convert the given {@linkplain RouterFunction router function} into a {@link WebHandler},
* using the given strategies.
* @param routerFunction the router function to convert
* @param strategies the strategies to use
* @return a web handler that handles web request using the given router function
*/
public static WebHandler toWebHandler(RouterFunction<?> routerFunction, HandlerStrategies strategies) {
Assert.notNull(routerFunction, "RouterFunction must not be null");
Assert.notNull(strategies, "HandlerStrategies must not be null");
return exchange -> {
ServerRequest request = new DefaultServerRequest(exchange, strategies.messageReaders());
addAttributes(exchange, request);
return routerFunction.route(request)
.defaultIfEmpty(notFound())
.flatMap(handlerFunction -> wrapException(() -> handlerFunction.handle(request)))
.flatMap(response -> wrapException(() -> response.writeTo(exchange, strategies)));
.flatMap(response -> wrapException(() -> response.writeTo(exchange, toContext(strategies))));
};
WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.webHandler(webHandler);
strategies.webFilters().get().forEach(handlerBuilder::filter);
strategies.exceptionHandlers().get().forEach(handlerBuilder::exceptionHandler);
return handlerBuilder.build();
}
private static <T> Mono<T> wrapException(Supplier<Mono<T>> supplier) {
@@ -243,39 +235,17 @@ public abstract class RouterFunctions {
}
}
/**
* Convert the given {@code RouterFunction} into a {@code HandlerMapping}.
* This conversion uses {@linkplain HandlerStrategies#builder() default strategies}.
* <p>The returned {@code HandlerMapping} can be run in a
* {@link org.springframework.web.reactive.DispatcherHandler}.
* @param routerFunction the router function to convert
* @return an handler mapping that maps HTTP request to a handler using the given router function
* @see HandlerFunctionAdapter
* @see ServerResponseResultHandler
*/
public static HandlerMapping toHandlerMapping(RouterFunction<?> routerFunction) {
return toHandlerMapping(routerFunction, HandlerStrategies.withDefaults());
}
private static ServerResponse.Context toContext(HandlerStrategies strategies) {
return new ServerResponse.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters();
}
/**
* Convert the given {@linkplain RouterFunction router function} into a {@link HandlerMapping},
* using the given strategies.
* <p>The returned {@code HandlerMapping} can be run in a
* {@link org.springframework.web.reactive.DispatcherHandler}.
* @param routerFunction the router function to convert
* @param strategies the strategies to use
* @return an handler mapping that maps HTTP request to a handler using the given router function
* @see HandlerFunctionAdapter
* @see ServerResponseResultHandler
*/
public static HandlerMapping toHandlerMapping(RouterFunction<?> routerFunction, HandlerStrategies strategies) {
Assert.notNull(routerFunction, "RouterFunction must not be null");
Assert.notNull(strategies, "HandlerStrategies must not be null");
return exchange -> {
ServerRequest request = new DefaultServerRequest(exchange, strategies.messageReaders());
addAttributes(exchange, request);
return routerFunction.route(request).map(handlerFunction -> (Object)handlerFunction);
@Override
public Supplier<Stream<ViewResolver>> viewResolvers() {
return strategies.viewResolvers();
}
};
}
@@ -294,4 +264,72 @@ public abstract class RouterFunctions {
return (HandlerFunction<T>) handlerFunction;
}
private static class DefaultRouterFunction<T extends ServerResponse>
implements RouterFunction<T> {
private final RequestPredicate predicate;
private final HandlerFunction<T> handlerFunction;
public DefaultRouterFunction(RequestPredicate predicate,
HandlerFunction<T> handlerFunction) {
this.predicate = predicate;
this.handlerFunction = handlerFunction;
}
@Override
public Mono<HandlerFunction<T>> route(ServerRequest request) {
if (this.predicate.test(request)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Predicate \"%s\" matches against \"%s\"",
this.predicate, request));
}
return Mono.just(this.handlerFunction);
}
else {
return Mono.empty();
}
}
@Override
public String toString() {
return String.format("%s -> %s", this.predicate, this.handlerFunction);
}
}
private static class DefaultNestedRouterFunction<T extends ServerResponse>
implements RouterFunction<T> {
private final RequestPredicate predicate;
private final RouterFunction<T> routerFunction;
public DefaultNestedRouterFunction(RequestPredicate predicate,
RouterFunction<T> routerFunction) {
this.predicate = predicate;
this.routerFunction = routerFunction;
}
@Override
public Mono<HandlerFunction<T>> route(ServerRequest serverRequest) {
return this.predicate.nest(serverRequest)
.map(nestedRequest -> {
if (logger.isDebugEnabled()) {
logger.debug(
String.format(
"Nested predicate \"%s\" matches against \"%s\"",
this.predicate, serverRequest));
}
return this.routerFunction.route(nestedRequest);
}
)
.orElseGet(Mono::empty);
}
@Override
public String toString() {
return String.format("%s -> %s", this.predicate, this.routerFunction);
}
}
}

View File

@@ -33,9 +33,11 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.json.Jackson2CodecSupport;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
/**
@@ -175,6 +177,19 @@ public interface ServerRequest {
Mono<? extends Principal> principal();
/**
* Create a new {@code ServerRequest} based on the given {@code ServerWebExchange} and
* message readers.
* @param exchange the exchange
* @param messageReaders the message readers
* @return the created {@code ServerRequest}
*/
static ServerRequest create(ServerWebExchange exchange,
List<HttpMessageReader<?>> messageReaders) {
return new DefaultServerRequest(exchange, messageReaders::stream);
}
/**
* Represents the headers of the HTTP request.
* @see ServerRequest#headers()

View File

@@ -22,6 +22,8 @@ import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@@ -31,11 +33,13 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.json.Jackson2CodecSupport;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
/**
@@ -64,10 +68,10 @@ public interface ServerResponse {
/**
* Write this response to the given web exchange.
* @param exchange the web exchange to write to
* @param strategies the strategies to use when writing
* @param context the context to use when writing
* @return {@code Mono<Void>} to indicate when writing is complete
*/
Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies);
Mono<Void> writeTo(ServerWebExchange exchange, Context context);
// Static builder methods
@@ -298,7 +302,7 @@ public interface ServerResponse {
* @param writeFunction the function used to write to the {@link ServerWebExchange}
* @return the built response
*/
Mono<ServerResponse> build(BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction);
Mono<ServerResponse> build(BiFunction<ServerWebExchange, Context, Mono<Void>> writeFunction);
}
@@ -385,4 +389,25 @@ public interface ServerResponse {
Mono<ServerResponse> render(String name, Map<String, ?> model);
}
/**
* Defines the context used during the {@link #writeTo(ServerWebExchange, Context)}.
*/
interface Context {
/**
* Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s
* to be used for response body conversion.
* @return the stream of message writers
*/
Supplier<Stream<HttpMessageWriter<?>>> messageWriters();
/**
* Supply a {@linkplain Stream stream} of {@link ViewResolver}s to be used for view name
* resolution.
* @return the stream of view resolvers
*/
Supplier<Stream<ViewResolver>> viewResolvers();
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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.web.reactive.function.server.support;
import java.util.Collections;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
/**
* {@code HandlerMapping} implementation that supports {@link RouterFunction}s.
* <p>If no {@link RouterFunction} is provided at
* {@linkplain #RouterFunctionMapping(RouterFunction) construction time}, this mapping will detect
* all router functions in the application context, and consult them in
* {@linkplain org.springframework.core.annotation.Order order}.
*
* @author Arjen Poutsma
* @since 5.0
*/
public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean {
private RouterFunction<?> routerFunction;
private ServerCodecConfigurer messageCodecConfigurer;
/**
* Create an empty {@code RouterFunctionMapping}.
* <p>If this constructor is used, this mapping will detect all {@link RouterFunction} instances
* available in the application context.
*/
public RouterFunctionMapping() {
}
/**
* Create a {@code RouterFunctionMapping} with the given {@link RouterFunction}.
* <p>If this constructor is used, no application context detection will occur.
* @param routerFunction the router function to use for mapping
*/
public RouterFunctionMapping(RouterFunction<?> routerFunction) {
this.routerFunction = routerFunction;
}
/**
* Configure HTTP message readers to de-serialize the request body with.
* <p>By default this is set to {@link ServerCodecConfigurer} with defaults.
*/
public void setMessageCodecConfigurer(ServerCodecConfigurer configurer) {
this.messageCodecConfigurer = configurer;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.messageCodecConfigurer == null) {
this.messageCodecConfigurer = ServerCodecConfigurer.create();
}
if (this.routerFunction == null) {
initRouterFunctions();
}
}
/**
* Initialized the router functions by detecting them in the application context.
*/
protected void initRouterFunctions() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for router functions in application context: " +
getApplicationContext());
}
List<RouterFunction<?>> routerFunctions = routerFunctions();
if (!CollectionUtils.isEmpty(routerFunctions) && logger.isInfoEnabled()) {
routerFunctions.forEach(routerFunction1 -> {
logger.info("Mapped " + routerFunction1);
});
}
this.routerFunction = routerFunctions.stream()
.reduce(RouterFunction::andOther)
.orElse(null);
}
private List<RouterFunction<?>> routerFunctions() {
SortedRouterFunctionsContainer container = new SortedRouterFunctionsContainer();
getApplicationContext().getAutowireCapableBeanFactory().autowireBean(container);
return CollectionUtils.isEmpty(container.routerFunctions) ? Collections.emptyList() :
container.routerFunctions;
}
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
if (this.routerFunction != null) {
ServerRequest request = ServerRequest.create(exchange, this.messageCodecConfigurer.getReaders());
exchange.getAttributes().put(RouterFunctions.REQUEST_ATTRIBUTE, request);
return this.routerFunction.route(request);
}
else {
return Mono.empty();
}
}
private static class SortedRouterFunctionsContainer {
private List<RouterFunction<?>> routerFunctions;
@Autowired(required = false)
public void setRouterFunctions(List<RouterFunction<?>> routerFunctions) {
this.routerFunctions = routerFunctions;
}
}
}

View File

@@ -16,13 +16,22 @@
package org.springframework.web.reactive.function.server.support;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
/**
@@ -31,26 +40,53 @@ import org.springframework.web.server.ServerWebExchange;
* @author Arjen Poutsma
* @since 5.0
*/
public class ServerResponseResultHandler implements HandlerResultHandler {
public class ServerResponseResultHandler implements HandlerResultHandler, InitializingBean,
Ordered {
private final HandlerStrategies strategies;
private ServerCodecConfigurer messageCodecConfigurer;
private List<ViewResolver> viewResolvers;
private int order = LOWEST_PRECEDENCE;
/**
* Create a {@code ServerResponseResultHandler} with default strategies.
* Configure HTTP message readers to de-serialize the request body with.
* <p>By default this is set to {@link ServerCodecConfigurer} with defaults.
*/
public ServerResponseResultHandler() {
this(HandlerStrategies.builder().build());
public void setMessageCodecConfigurer(ServerCodecConfigurer configurer) {
this.messageCodecConfigurer = configurer;
}
public void setViewResolvers(List<ViewResolver> viewResolvers) {
this.viewResolvers = viewResolvers;
}
/**
* Create a {@code ServerResponseResultHandler} with the given strategies.
* Set the order for this result handler relative to others.
* <p>By default set to {@link Ordered#LOWEST_PRECEDENCE}, however see
* Javadoc of sub-classes which may change this default.
* @param order the order
*/
public ServerResponseResultHandler(HandlerStrategies strategies) {
Assert.notNull(strategies, "HandlerStrategies must not be null");
this.strategies = strategies;
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.messageCodecConfigurer == null) {
throw new IllegalArgumentException("'messageCodecConfigurer' is required");
}
if (this.viewResolvers == null) {
this.viewResolvers = Collections.emptyList();
}
}
@Override
public boolean supports(HandlerResult result) {
@@ -61,7 +97,16 @@ public class ServerResponseResultHandler implements HandlerResultHandler {
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
ServerResponse response = (ServerResponse) result.getReturnValue();
Assert.state(response != null, "No ServerResponse");
return response.writeTo(exchange, this.strategies);
}
return response.writeTo(exchange, new ServerResponse.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return messageCodecConfigurer.getWriters()::stream;
}
@Override
public Supplier<Stream<ViewResolver>> viewResolvers() {
return viewResolvers::stream;
}
});
}
}

View File

@@ -23,6 +23,8 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.Test;
import org.reactivestreams.Publisher;
@@ -40,10 +42,12 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.result.view.ViewResolver;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.*;
@@ -190,6 +194,17 @@ public class DefaultEntityResponseBuilderTests {
MockServerWebExchange exchange = MockServerHttpRequest.get("http://localhost").toExchange();
ServerResponse.Context context = new ServerResponse.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return Collections.<HttpMessageWriter<?>>singletonList(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()))::stream;
}
@Override
public Supplier<Stream<ViewResolver>> viewResolvers() {
return Collections.<ViewResolver>emptyList()::stream;
}
};
HandlerStrategies strategies = HandlerStrategies.empty()
.customCodecs(configurer -> configurer.writer(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())))
.build();
@@ -200,7 +215,7 @@ public class DefaultEntityResponseBuilderTests {
.expectNext(body)
.expectComplete()
.verify();
response.writeTo(exchange, strategies);
response.writeTo(exchange, context);
})
.expectComplete()
.verify();

View File

@@ -270,9 +270,9 @@ public class DefaultServerResponseBuilderTests {
ServerWebExchange exchange = mock(ServerWebExchange.class);
MockServerHttpResponse response = new MockServerHttpResponse();
when(exchange.getResponse()).thenReturn(response);
HandlerStrategies strategies = mock(HandlerStrategies.class);
ServerResponse.Context context = mock(ServerResponse.Context.class);
result.flatMap(res -> res.writeTo(exchange, strategies)).block();
result.flatMap(res -> res.writeTo(exchange, context)).block();
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals("MyValue", response.getHeaders().getFirst("MyKey"));
@@ -287,9 +287,9 @@ public class DefaultServerResponseBuilderTests {
ServerWebExchange exchange = mock(ServerWebExchange.class);
MockServerHttpResponse response = new MockServerHttpResponse();
when(exchange.getResponse()).thenReturn(response);
HandlerStrategies strategies = mock(HandlerStrategies.class);
ServerResponse.Context context = mock(ServerResponse.Context.class);
result.flatMap(res -> res.writeTo(exchange, strategies)).block();
result.flatMap(res -> res.writeTo(exchange, context)).block();
StepVerifier.create(response.getBody()).expectComplete().verify();
}

View File

@@ -36,16 +36,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.HandlerAdapter;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurationSupport;
import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter;
import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.junit.Assert.*;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.BodyInserters.fromPublisher;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@@ -108,8 +102,9 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr
}
@EnableWebFlux
@Configuration
static class TestConfiguration extends WebFluxConfigurationSupport {
static class TestConfiguration {
@Bean
public PersonHandler personHandler() {
@@ -122,31 +117,23 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr
}
@Bean
public HandlerAdapter handlerAdapter() {
return new HandlerFunctionAdapter();
public RouterFunction<EntityResponse<Person>> monoRouterFunction(PersonHandler personHandler) {
return route(RequestPredicates.GET("/mono"), personHandler::mono);
}
@Bean
public HandlerMapping handlerMapping() {
PersonHandler personHandler = personHandler();
return RouterFunctions.toHandlerMapping(
route(RequestPredicates.GET("/mono"), personHandler::mono)
.and(route(RequestPredicates.GET("/flux"), personHandler::flux)));
public RouterFunction<ServerResponse> fluxRouterFunction(PersonHandler personHandler) {
return route(RequestPredicates.GET("/flux"), personHandler::flux);
}
@Bean
public ServerResponseResultHandler responseResultHandler() {
return new ServerResponseResultHandler();
}
}
private static class PersonHandler {
public Mono<ServerResponse> mono(ServerRequest request) {
public Mono<EntityResponse<Person>> mono(ServerRequest request) {
Person person = new Person("John");
return ServerResponse.ok().body(fromObject(person));
return EntityResponse.fromObject(person).build();
}
public Mono<ServerResponse> flux(ServerRequest request) {

View File

@@ -33,7 +33,6 @@ public class HandlerStrategiesTests {
assertEquals(Optional.empty(), strategies.messageReaders().get().findFirst());
assertEquals(Optional.empty(), strategies.messageWriters().get().findFirst());
assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst());
assertNull(strategies.localeResolver().get());
}
@Test
@@ -42,7 +41,6 @@ public class HandlerStrategiesTests {
assertNotEquals(Optional.empty(), strategies.messageReaders().get().findFirst());
assertNotEquals(Optional.empty(), strategies.messageWriters().get().findFirst());
assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst());
assertNotNull(strategies.localeResolver().get());
}
}

View File

@@ -19,7 +19,10 @@ package org.springframework.web.reactive.function.server;
import java.io.IOException;
import java.nio.file.Files;
import java.util.EnumSet;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -29,9 +32,11 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.reactive.result.view.ViewResolver;
import static org.junit.Assert.*;
@@ -44,6 +49,25 @@ public class ResourceHandlerFunctionTests {
private final ResourceHandlerFunction handlerFunction = new ResourceHandlerFunction(this.resource);
private ServerResponse.Context context;
@Before
public void createContext() {
HandlerStrategies strategies = HandlerStrategies.withDefaults();
context = new ServerResponse.Context() {
@Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return strategies.messageWriters();
}
@Override
public Supplier<Stream<ViewResolver>> viewResolvers() {
return strategies.viewResolvers();
}
};
}
@Test
public void get() throws IOException {
@@ -59,7 +83,7 @@ public class ResourceHandlerFunctionTests {
assertTrue(response instanceof EntityResponse);
EntityResponse<Resource> entityResponse = (EntityResponse<Resource>) response;
assertEquals(this.resource, entityResponse.entity());
return response.writeTo(exchange, HandlerStrategies.withDefaults());
return response.writeTo(exchange, context);
});
StepVerifier.create(result)
@@ -94,7 +118,7 @@ public class ResourceHandlerFunctionTests {
assertTrue(response instanceof EntityResponse);
EntityResponse<Resource> entityResponse = (EntityResponse<Resource>) response;
assertEquals(this.resource.getFilename(), entityResponse.entity().getFilename());
return response.writeTo(exchange, HandlerStrategies.withDefaults());
return response.writeTo(exchange, context);
});
StepVerifier.create(result).expectComplete().verify();
@@ -116,7 +140,7 @@ public class ResourceHandlerFunctionTests {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS),
response.headers().getAllow());
return response.writeTo(exchange, HandlerStrategies.withDefaults());
return response.writeTo(exchange, context);
});

View File

@@ -199,7 +199,7 @@ public class RouterFunctionsTests {
@Override
public Mono<Void> writeTo(ServerWebExchange exchange,
HandlerStrategies strategies) {
Context context) {
return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found"));
}
});
@@ -232,7 +232,7 @@ public class RouterFunctionsTests {
@Override
public Mono<Void> writeTo(ServerWebExchange exchange,
HandlerStrategies strategies) {
Context context) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found");
}
});

View File

@@ -0,0 +1,108 @@
/*
* 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.web.reactive.function.server.support;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Arjen Poutsma
*/
public class RouterFunctionMappingTests {
private List<HttpMessageReader<?>> messageReaders;
private ServerWebExchange exchange;
private ServerCodecConfigurer codecConfigurer;
@Before
public void setUp() {
this.messageReaders =
Collections.singletonList(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
this.exchange = new MockServerWebExchange(MockServerHttpRequest.get("http://example.com/match").build());
codecConfigurer = ServerCodecConfigurer.create();
}
@Test
public void normal() {
HandlerFunction<ServerResponse> handlerFunction = request -> ServerResponse.ok().build();
RouterFunction<ServerResponse> routerFunction = request -> Mono.just(handlerFunction);
RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction);
mapping.setMessageCodecConfigurer(this.codecConfigurer);
Mono<Object> result = mapping.getHandler(this.exchange);
StepVerifier.create(result)
.expectNext(handlerFunction)
.expectComplete()
.verify();
}
@Test
public void noMatch() {
RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();
RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction);
mapping.setMessageCodecConfigurer(this.codecConfigurer);
Mono<Object> result = mapping.getHandler(this.exchange);
StepVerifier.create(result)
.expectComplete()
.verify();
}
@Configuration
@EnableWebFlux
private static class TestConfig {
public RouterFunction<ServerResponse> match() {
HandlerFunction<ServerResponse> handlerFunction = request -> ServerResponse.ok().build();
return RouterFunctions.route(RequestPredicates.GET("/match"), handlerFunction);
}
public RouterFunction<ServerResponse> noMatch() {
HandlerFunction<ServerResponse> handlerFunction = request -> ServerResponse.ok().build();
RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();
return RouterFunctions.route(RequestPredicates.GET("/no-match"), handlerFunction);
}
}
}