Introduce RouterFunction builder

This commit introduces RouterFunctions.Builder, a new way to build
router functions that does not require static imports, thus being more
discoverable and convenient.

Issue: SPR-16953
This commit is contained in:
Arjen Poutsma
2018-06-28 11:04:34 +02:00
parent a89e716cc7
commit 8202052b38
3 changed files with 705 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2002-2018 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;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
* Default implementation of {@link RouterFunctions.Builder}.
* @author Arjen Poutsma
* @since 5.1
*/
class RouterFunctionBuilder implements RouterFunctions.Builder {
private List<RouterFunction<ServerResponse>> routerFunctions = new ArrayList<>();
private List<HandlerFilterFunction<ServerResponse, ServerResponse>> filterFunctions = new ArrayList<>();
@Override
public RouterFunctions.Builder route(RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
this.routerFunctions.add(RouterFunctions.route(predicate, handlerFunction));
return this;
}
@Override
public RouterFunctions.Builder routeGet(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.GET), handlerFunction);
}
@Override
public RouterFunctions.Builder routeGet(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.GET(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder routeHead(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.HEAD), handlerFunction);
}
@Override
public RouterFunctions.Builder routeHead(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.HEAD(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder routePost(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.POST), handlerFunction);
}
@Override
public RouterFunctions.Builder routePost(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.POST(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder routePut(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.PUT), handlerFunction);
}
@Override
public RouterFunctions.Builder routePut(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.PUT(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder routePatch(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.PATCH), handlerFunction);
}
@Override
public RouterFunctions.Builder routePatch(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.PATCH(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder routeDelete(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.DELETE), handlerFunction);
}
@Override
public RouterFunctions.Builder routeDelete(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.DELETE(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder routeOptions(HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.method(HttpMethod.OPTIONS), handlerFunction);
}
@Override
public RouterFunctions.Builder routeOptions(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return route(RequestPredicates.OPTIONS(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder nest(RequestPredicate predicate,
Consumer<RouterFunctions.Builder> builderConsumer) {
Assert.notNull(builderConsumer, "'builderConsumer' must not be null");
RouterFunctionBuilder nestedBuilder = new RouterFunctionBuilder();
builderConsumer.accept(nestedBuilder);
RouterFunction<ServerResponse> nestedRoute = nestedBuilder.build();
this.routerFunctions.add(RouterFunctions.nest(predicate, nestedRoute));
return this;
}
@Override
public RouterFunctions.Builder nest(RequestPredicate predicate,
Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier) {
Assert.notNull(routerFunctionSupplier, "'routerFunctionSupplier' must not be null");
RouterFunction<ServerResponse> nestedRoute = routerFunctionSupplier.get();
this.routerFunctions.add(RouterFunctions.nest(predicate, nestedRoute));
return this;
}
@Override
public RouterFunctions.Builder nestPath(String pattern,
Consumer<RouterFunctions.Builder> builderConsumer) {
return nest(RequestPredicates.path(pattern), builderConsumer);
}
@Override
public RouterFunctions.Builder nestPath(String pattern,
Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier) {
return nest(RequestPredicates.path(pattern), routerFunctionSupplier);
}
@Override
public RouterFunctions.Builder filter(HandlerFilterFunction<ServerResponse, ServerResponse> filterFunction) {
Assert.notNull(filterFunction, "'filterFunction' must not be null");
this.filterFunctions.add(filterFunction);
return this;
}
@Override
public RouterFunctions.Builder before(
Function<ServerRequest, Mono<ServerRequest>> requestProcessor) {
Assert.notNull(requestProcessor, "Function must not be null");
return filter((request, next) -> requestProcessor.apply(request).flatMap(next::handle));
}
@Override
public RouterFunctions.Builder after(
BiFunction<ServerRequest, ServerResponse, Mono<ServerResponse>> responseProcessor) {
return filter((request, next) -> next.handle(request)
.flatMap(serverResponse -> responseProcessor.apply(request, serverResponse)));
}
@Override
public <T extends Throwable> RouterFunctions.Builder exception(
Class<T> exceptionType,
BiFunction<T, ServerRequest, Mono<ServerResponse>> fallback) {
Assert.notNull(exceptionType, "'exceptionType' must not be null");
Assert.notNull(fallback, "'fallback' must not be null");
return filter((request, next) -> next.handle(request)
.onErrorResume(exceptionType, t -> fallback.apply(t, request)));
}
@Override
public RouterFunction<ServerResponse> build() {
RouterFunction<ServerResponse> result = this.routerFunctions.stream()
.reduce(RouterFunction::and)
.orElseThrow(IllegalStateException::new);
if (this.filterFunctions.isEmpty()) {
return result;
}
else {
HandlerFilterFunction<ServerResponse, ServerResponse> filter =
this.filterFunctions.stream()
.reduce(HandlerFilterFunction::andThen)
.orElseThrow(IllegalStateException::new);
return result.filter(filter);
}
}
}

View File

@@ -20,6 +20,8 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -69,6 +71,14 @@ public abstract class RouterFunctions {
private static final HandlerFunction<ServerResponse> NOT_FOUND_HANDLER = request -> ServerResponse.notFound().build();
/**
* Return a {@linkplain Builder builder} that offers a discoverable way to create router
* functions.
* @return a router function builder
*/
public static Builder builder() {
return new RouterFunctionBuilder();
}
/**
* Route to the given handler function if the given request predicate applies.
@@ -270,6 +280,326 @@ public abstract class RouterFunctions {
return (HandlerFunction<T>) handlerFunction;
}
/**
* Represents a builder for router functions.
* <p>Each invocation of {@code route} creates a new {@link RouterFunction} that is
* {@linkplain RouterFunction#and(RouterFunction) composed} with any previously built functions.
* @since 5.1
*/
public interface Builder {
/**
* Adds a route to the given handler function that matches if the given request predicate
* applies.
* <p>For instance, the following example routes GET requests for "/user" to the
* {@code listUsers} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.builder()
* .route(RequestPredicates.GET("/user"), userController::listUsers)
* .build();
* </pre>
* @param predicate the predicate to test
* @param handlerFunction the handler function to route to if the predicate applies
* @return this builder
* @see RequestPredicates
*/
Builder route(RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code GET} requests.
* @param handlerFunction the handler function to handle all {@code GET} requests
* @return this builder
*/
Builder routeGet(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code GET} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code GET} requests that
* match {@code pattern}
* @return this builder
*/
Builder routeGet(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code HEAD} requests.
* @param handlerFunction the handler function to handle all {@code HEAD} requests
* @return this builder
*/
Builder routeHead(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code HEAD} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code HEAD} requests that
* match {@code pattern}
* @return this builder
*/
Builder routeHead(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code POST} requests.
* @param handlerFunction the handler function to handle all {@code POST} requests
* @return this builder
*/
Builder routePost(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code POST} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code POST} requests that
* match {@code pattern}
* @return this builder
*/
Builder routePost(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PUT} requests.
* @param handlerFunction the handler function to handle all {@code PUT} requests
* @return this builder
*/
Builder routePut(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PUT} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code PUT} requests that
* match {@code pattern}
* @return this builder
*/
Builder routePut(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PATCH} requests.
* @param handlerFunction the handler function to handle all {@code PATCH} requests
* @return this builder
*/
Builder routePatch(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code PATCH} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code PATCH} requests that
* match {@code pattern}
* @return this builder
*/
Builder routePatch(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code DELETE} requests.
* @param handlerFunction the handler function to handle all {@code DELETE} requests
* @return this builder
*/
Builder routeDelete(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code DELETE} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code DELETE} requests that
* match {@code pattern}
* @return this builder
*/
Builder routeDelete(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code OPTIONS} requests.
* @param handlerFunction the handler function to handle all {@code OPTIONS} requests
* @return this builder
*/
Builder routeOptions(HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code OPTIONS} requests
* that match the given pattern.
* @param pattern the pattern to match to
* @param handlerFunction the handler function to handle all {@code OPTIONS} requests that
* match {@code pattern}
* @return this builder
*/
Builder routeOptions(String pattern, HandlerFunction<ServerResponse> handlerFunction);
/**
* Route to the supplied router function if the given request predicate applies. This method
* can be used to create <strong>nested routes</strong>, where a group of routes share a
* common path (prefix), header, or other request predicate.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.builder()
* .nest(RequestPredicates.path("/user"), () ->
* RouterFunctions.builder()
* .routeGet(this::listUsers)
* .routePost(this::createUser);
* .build();
* )
* .build();
* </pre>
* @param predicate the predicate to test
* @param routerFunctionSupplier supplier for the nested router function to delegate to if
* the predicate applies
* @return this builder
* @see RequestPredicates
*/
Builder nest(RequestPredicate predicate, Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier);
/**
* Route to a built router function if the given request predicate applies.
* This method can be used to create <strong>nested routes</strong>, where a group of routes
* share a common path (prefix), header, or other request predicate.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.builder()
* .nest(RequestPredicates.path("/user"), builder ->
* builder.routeGet(this::listUsers)
* .routePost(this::createUser);
* )
* .build();
* </pre>
* @param predicate the predicate to test
* @param builderConsumer consumer for a {@code Builder} that provides the nested router
* function
* @return this builder
* @see RequestPredicates
*/
Builder nest(RequestPredicate predicate, Consumer<Builder> builderConsumer);
/**
* Route to the supplied router function if the given path prefix pattern applies. This method
* can be used to create <strong>nested routes</strong>, where a group of routes share a
* common path (prefix).
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.builder()
* .nestPath("/user", () ->
* RouterFunctions.builder()
* .routeGet(this::listUsers)
* .routePost(this::createUser);
* .build();
* )
* .build();
* </pre>
* @param pattern the pattern to match to
* @param routerFunctionSupplier supplier for the nested router function to delegate to if
* the pattern matches
* @return this builder
*/
Builder nestPath(String pattern, Supplier<RouterFunction<ServerResponse>> routerFunctionSupplier);
/**
* Route to a built router function if the given path prefix pattern applies.
* This method can be used to create <strong>nested routes</strong>, where a group of routes
* share a common path (prefix), header, or other request predicate.
* <p>For instance, the following example creates a nested route with a "/user" path
* predicate, so that GET requests for "/user" will list users,
* and POST request for "/user" will create a new user.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; nestedRoute =
* RouterFunctions.builder()
* .nestPath("/user", builder ->
* builder.routeGet(this::listUsers)
* .routePost(this::createUser);
* )
* .build();
* </pre>
* @param pattern the pattern to match to
* @param builderConsumer consumer for a {@code Builder} that provides the nested router
* function
* @return this builder
*/
Builder nestPath(String pattern, Consumer<Builder> builderConsumer);
/**
* Filters all routes created by this builder with the given filter function. Filter
* functions are typically used to address cross-cutting concerns, such as logging,
* security, etc.
* <p>For instance, the following example creates a filter that logs the request before
* the handler function executes, and logs the response after.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.builder()
* .routeGet("/user", this::listUsers)
* .filter((request, next) -> {
* log(request);
* Mono&lt;ServerResponse&gt; responseMono = next.handle(request);
* return responseMono.doOnNext(response -> log(response);
* })
* .build();
* </pre>
* @param filterFunction the function to filter all routes built by this builder
* @return this builder
*/
Builder filter(HandlerFilterFunction<ServerResponse, ServerResponse> filterFunction);
/**
* Filters the request for all routes created by this builder with the given request
* processing function. Filters are typically used to address cross-cutting concerns, such
* as logging, security, etc.
* <p>For instance, the following example creates a filter that logs the request before
* the handler function executes.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.builder()
* .routeGet("/user", this::listUsers)
* .before(request -> {
* log(request);
* return Mono.just(request);
* })
* .build();
* </pre>
* @param requestProcessor a function that transforms the request
* @return this builder
*/
Builder before(Function<ServerRequest, Mono<ServerRequest>> requestProcessor);
/**
* Filters the response for all routes created by this builder with the given response
* processing function. Filters are typically used to address cross-cutting concerns, such
* as logging, security, etc.
* <p>For instance, the following example creates a filter that logs the response after
* the handler function executes.
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; filteredRoute =
* RouterFunctions.builder()
* .routeGet("/user", this::listUsers)
* .after((request, response) -> {
* log(response);
* return Mono.just(response);
* })
* .build();
* </pre>
* @param responseProcessor a function that transforms the response
* @return this builder
*/
Builder after(BiFunction<ServerRequest, ServerResponse, Mono<ServerResponse>> responseProcessor);
<T extends Throwable> Builder exception(Class<T> exceptionType,
BiFunction<T, ServerRequest, Mono<ServerResponse>> fallback);
/**
* Builds the {@code RouterFunction}. All created routes are
* {@linkplain RouterFunction#and(RouterFunction) composed} with one another, and filters
* (f any) are applied to the result.
* @return the built router function
*/
RouterFunction<ServerResponse> build();
}
/**
* Receives notifications from the logical structure of router functions.

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2002-2018 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;
import java.net.URI;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
*/
public class RouterFunctionBuilderTests {
private RouterFunctionBuilder builder = new RouterFunctionBuilder();
@Test
public void route() {
RouterFunction<ServerResponse> route = this.builder
.routeGet("/foo", request -> ServerResponse.ok().build())
.routePost(request -> ServerResponse.noContent().build())
.build();
MockServerRequest fooRequest = MockServerRequest.builder().
method(HttpMethod.GET).
uri(URI.create("http://localhost/foo"))
.build();
Mono<Integer> responseMono = route.route(fooRequest)
.flatMap(handlerFunction -> handlerFunction.handle(fooRequest))
.map(ServerResponse::statusCode)
.map(HttpStatus::value);
StepVerifier.create(responseMono)
.expectNext(200)
.verifyComplete();
MockServerRequest barRequest = MockServerRequest.builder().
method(HttpMethod.POST).
uri(URI.create("http://localhost"))
.build();
responseMono = route.route(barRequest)
.flatMap(handlerFunction -> handlerFunction.handle(barRequest))
.map(ServerResponse::statusCode)
.map(HttpStatus::value);
StepVerifier.create(responseMono)
.expectNext(204)
.verifyComplete();
}
@Test
public void nest() {
RouterFunction<?> route = this.builder
.nestPath("/foo", builder -> {
builder.nestPath("/bar",
() -> RouterFunctions.builder()
.routeGet("/baz", request -> ServerResponse.ok().build())
.build());
})
.build();
MockServerRequest fooRequest = MockServerRequest.builder().
method(HttpMethod.GET).
uri(URI.create("http://localhost/foo/bar/baz"))
.build();
Mono<Integer> responseMono = route.route(fooRequest)
.flatMap(handlerFunction -> handlerFunction.handle(fooRequest))
.map(ServerResponse::statusCode)
.map(HttpStatus::value);
StepVerifier.create(responseMono)
.expectNext(200)
.verifyComplete();
}
@Test
public void filters() {
AtomicInteger filterCount = new AtomicInteger();
RouterFunction<?> route = this.builder
.routeGet("/foo", request -> ServerResponse.ok().build())
.routeGet("/bar", request -> Mono.error(new IllegalStateException()))
.before(request -> {
int count = filterCount.getAndIncrement();
assertEquals(0, count);
return Mono.just(request);
})
.after((request, response) -> {
int count = filterCount.getAndIncrement();
assertEquals(3, count);
return Mono.just(response);
})
.filter((request, next) -> {
int count = filterCount.getAndIncrement();
assertEquals(1, count);
Mono<ServerResponse> responseMono = next.handle(request);
count = filterCount.getAndIncrement();
assertEquals(2, count);
return responseMono;
})
.exception(IllegalStateException.class, (e, request) -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build())
.build();
MockServerRequest fooRequest = MockServerRequest.builder().
method(HttpMethod.GET).
uri(URI.create("http://localhost/foo"))
.build();
Mono<ServerResponse> fooResponseMono = route.route(fooRequest)
.flatMap(handlerFunction -> handlerFunction.handle(fooRequest));
StepVerifier.create(fooResponseMono)
.consumeNextWith(serverResponse -> {
assertEquals(4, filterCount.get());
})
.verifyComplete();
filterCount.set(0);
MockServerRequest barRequest = MockServerRequest.builder().
method(HttpMethod.GET).
uri(URI.create("http://localhost/bar"))
.build();
Mono<Integer> barResponseMono = route.route(barRequest)
.flatMap(handlerFunction -> handlerFunction.handle(barRequest))
.map(ServerResponse::statusCode)
.map(HttpStatus::value);
StepVerifier.create(barResponseMono)
.expectNext(500)
.verifyComplete();
}
}