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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user