Functional Endpoints

  • ServerResponse

  • RouterFunction

  • RouterFunctions

  • PathVariable

  • Mono

  • Flux

  • Validator :description: Spring WebFlux.fn 是一个轻量级的函数式编程模型,其中函数用于路由和处理,而契约被设计为不可变。它在 Reactive Core 的基础上运行,旨在替代基于注解的编程模型。WebFlux.fn 通过 HandlerFunctions 和 ServerResponse 来处理 HTTP 请求和响应。HandlerFunction 是一个获取 ServerRequest 并返回延迟 ServerResponse 的函数。RouterFunction 是一个获取 ServerRequest 并返回延迟 HandlerFunction 的函数。可以使用 RouterFunctions.route() 来创建路由,其中路由函数接受谓词和处理函数。通过使用嵌套路由,可以将常见的谓词分组到具有共享路径的路由器函数中。WebFlux.fn 还允许将请求重定向到资源,并提供对在根位置提供服务的资源的支持。 See equivalent in the Servlet stack

Spring WebFlux 包含 WebFlux.fn,这是一个轻量级的函数式编程模型,其中函数用于路由和处理请求,并且合约旨在不可变。它是一种基于注释的编程模型的替代方法,但在其他方面运行在相同的 Reactive Core 基础上。

Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. It is an alternative to the annotation-based programming model but otherwise runs on the same Reactive Core foundation.

Overview

在 WebFlux.fn 中,通过 HandlerFunction 处理 HTTP 请求:一个取用`ServerRequest` 并返回延迟的 ServerResponse 的函数(即 Mono<ServerResponse>)。请求和响应对象都具有不可变契约,这些契约为 HTTP 请求和响应提供了符合 JDK 8 的访问。HandlerFunction 等效于基于注解的编程模型中 @RequestMapping 方法的主体。

In WebFlux.fn, an HTTP request is handled with a HandlerFunction: a function that takes ServerRequest and returns a delayed ServerResponse (i.e. Mono<ServerResponse>). Both the request and the response object have immutable contracts that offer JDK 8-friendly access to the HTTP request and response. HandlerFunction is the equivalent of the body of a @RequestMapping method in the annotation-based programming model.

传入请求将路由到带有 RouterFunction 的处理程序函数:一个将接受 ServerRequest 并返回延迟的 HandlerFunction(即 Mono<HandlerFunction>)的函数。当路由器函数匹配时,将返回处理程序函数;否则为空 MonoRouterFunction 相当于一个 @RequestMapping 注解,但主要区别在于,路由器函数不仅仅提供数据,而且还提供行为。

Incoming requests are routed to a handler function with a RouterFunction: a function that takes ServerRequest and returns a delayed HandlerFunction (i.e. Mono<HandlerFunction>). When the router function matches, a handler function is returned; otherwise an empty Mono. RouterFunction is the equivalent of a @RequestMapping annotation, but with the major difference that router functions provide not just data, but also behavior.

RouterFunctions.route() 提供了一个路由构建器,它使用户能够轻松创建路由,如下图例所示:

RouterFunctions.route() provides a router builder that facilitates the creation of routers, as the following example shows:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route() 1
	.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
	.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
	.POST("/person", handler::createPerson)
	.build();


public class PersonHandler {

	// ...

	public Mono<ServerResponse> listPeople(ServerRequest request) {
		// ...
	}

	public Mono<ServerResponse> createPerson(ServerRequest request) {
		// ...
	}

	public Mono<ServerResponse> getPerson(ServerRequest request) {
		// ...
	}
}
1 Create router using route().
Kotlin
val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = coRouter { (1)
	accept(APPLICATION_JSON).nest {
		GET("/person/{id}", handler::getPerson)
		GET("/person", handler::listPeople)
	}
	POST("/person", handler::createPerson)
}


class PersonHandler(private val repository: PersonRepository) {

	// ...

	suspend fun listPeople(request: ServerRequest): ServerResponse {
		// ...
	}

	suspend fun createPerson(request: ServerRequest): ServerResponse {
		// ...
	}

	suspend fun getPerson(request: ServerRequest): ServerResponse {
		// ...
	}
}
2 Create router using Coroutines router DSL; a Reactive alternative is also available via router { }.

运行`RouterFunction`的一种方法是将其转换为`HttpHandler`,并通过一个内置的server adapters安装:

One way to run a RouterFunction is to turn it into an HttpHandler and install it through one of the built-in server adapters:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

大多数应用程序可以通过 WebFlux Java 配置运行,请参阅Running a Server

Most applications can run through the WebFlux Java configuration, see Running a Server.

HandlerFunction

ServerRequest`和`ServerResponse`是不可变接口,可提供对 HTTP 请求和响应的 JDK 8 友好访问。请求和响应均对正文流提供https://www.reactive-streams.org[反应流]背压。请求正文使用 Reactor `Flux`或`Mono`表示。响应正文使用任何反应流`Publisher`表示,包括`Flux`和`Mono。有关更多详细信息,请参阅Reactive Libraries

ServerRequest and ServerResponse are immutable interfaces that offer JDK 8-friendly access to the HTTP request and response. Both request and response provide Reactive Streams back pressure against the body streams. The request body is represented with a Reactor Flux or Mono. The response body is represented with any Reactive Streams Publisher, including Flux and Mono. For more on that, see Reactive Libraries.

ServerRequest

ServerRequest 提供对 HTTP 方法、URI、标头和查询参数的访问,而对主体的访问是通过 body 方法提供的。

ServerRequest provides access to the HTTP method, URI, headers, and query parameters, while access to the body is provided through the body methods.

以下示例将请求正文提取到 Mono<String>

The following example extracts the request body to a Mono<String>:

  • Java

  • Kotlin

Mono<String> string = request.bodyToMono(String.class);
val string = request.awaitBody<String>()

以下示例将正文提取到 Flux<Person>(或 Kotlin 中的 Flow<Person>),其中`Person` 对象是从某种已序列化形式(例如 JSON 或 XML)解码而来:

The following example extracts the body to a Flux<Person> (or a Flow<Person> in Kotlin), where Person objects are decoded from some serialized form, such as JSON or XML:

  • Java

  • Kotlin

Flux<Person> people = request.bodyToFlux(Person.class);
val people = request.bodyToFlow<Person>()

前面的示例是快捷方式,它使用了更通用的 ServerRequest.body(BodyExtractor),它接受 BodyExtractor 函数式策略接口。实用程序类 BodyExtractors 可访问许多实例。例如,前面的示例也可以写成以下形式:

The preceding examples are shortcuts that use the more general ServerRequest.body(BodyExtractor), which accepts the BodyExtractor functional strategy interface. The utility class BodyExtractors provides access to a number of instances. For example, the preceding examples can also be written as follows:

  • Java

  • Kotlin

Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
	val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
	val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()

以下示例展示如何访问表单数据:

The following example shows how to access form data:

  • Java

  • Kotlin

Mono<MultiValueMap<String, String>> map = request.formData();
val map = request.awaitFormData()

以下示例展示如何以映射形式访问 multipart 数据:

The following example shows how to access multipart data as a map:

  • Java

  • Kotlin

Mono<MultiValueMap<String, Part>> map = request.multipartData();
val map = request.awaitMultipartData()

以下示例展示如何一次访问 multipart 数据并以流式传输形式访问:

The following example shows how to access multipart data, one at a time, in streaming fashion:

  • Java

  • Kotlin

Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
allPartsEvents.windowUntil(PartEvent::isLast)
      .concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
          if (signal.hasValue()) {
              PartEvent event = signal.get();
              if (event instanceof FormPartEvent formEvent) {
                  String value = formEvent.value();
                  // handle form field
              }
              else if (event instanceof FilePartEvent fileEvent) {
                  String filename = fileEvent.filename();
                  Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
                  // handle file upload
              }
              else {
                  return Mono.error(new RuntimeException("Unexpected event: " + event));
              }
          }
          else {
              return partEvents; // either complete or error signal
          }
      }));
val parts = request.bodyToFlux<PartEvent>()
allPartsEvents.windowUntil(PartEvent::isLast)
    .concatMap {
        it.switchOnFirst { signal, partEvents ->
            if (signal.hasValue()) {
                val event = signal.get()
                if (event is FormPartEvent) {
                    val value: String = event.value();
                    // handle form field
                } else if (event is FilePartEvent) {
                    val filename: String = event.filename();
                    val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
                    // handle file upload
                } else {
                    return Mono.error(RuntimeException("Unexpected event: " + event));
                }
            } else {
                return partEvents; // either complete or error signal
            }
        }
    }
}

请注意,PartEvent 对象的正文内容必须完全被消耗、中继或释放,以避免内存泄漏。

Note that the body contents of the PartEvent objects must be completely consumed, relayed, or released to avoid memory leaks.

ServerResponse

ServerResponse 提供对 HTTP 响应的访问,而且因为它不可变,所以你可以使用 build 方法来创建它。你可以使用该构建器设置响应状态、添加响应标头或提供一个主体。下例创建了一个 200 (OK) 响应(带有 JSON 内容):

ServerResponse provides access to the HTTP response and, since it is immutable, you can use a build method to create it. You can use the builder to set the response status, to add response headers, or to provide a body. The following example creates a 200 (OK) response with JSON content:

  • Java

  • Kotlin

Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)

下例展示了如何构建一个 201 (CREATED) 响应(带有 Location 标头和空主体):

The following example shows how to build a 201 (CREATED) response with a Location header and no body:

  • Java

  • Kotlin

URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()

根据所使用的编解码器,可以传递提示参数来自定义主体的序列化或反序列化方式。例如,要指定一个https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON 视图]:

Depending on the codec used, it is possible to pass hint parameters to customize how the body is serialized or deserialized. For example, to specify a Jackson JSON view:

  • Java

  • Kotlin

ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)

Handler Classes

我们可以将一个处理函数编写为一个 lambda,如下图例所示:

We can write a handler function as a lambda, as the following example shows:

  • Java

  • Kotlin

HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().bodyValue("Hello World");
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }

这样做很方便,但在应用程序中,我们需要多个函数,并且多个内联 lambda 可能很混乱。因此,将相关的处理函数分组在一个处理函数类中很有用,这个类在基于注解的应用程序中所扮演的角色与 @Controller 类似。例如,下例公开了一个响应式的 Person 存储库:

That is convenient, but in an application we need multiple functions, and multiple inline lambda’s can get messy. Therefore, it is useful to group related handler functions together into a handler class, which has a similar role as @Controller in an annotation-based application. For example, the following class exposes a reactive Person repository:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

	private final PersonRepository repository;

	public PersonHandler(PersonRepository repository) {
		this.repository = repository;
	}

	public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
		Flux<Person> people = repository.allPeople();
		return ok().contentType(APPLICATION_JSON).body(people, Person.class);
	}

	public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
		Mono<Person> person = request.bodyToMono(Person.class);
		return ok().build(repository.savePerson(person));
	}

	public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
		int personId = Integer.valueOf(request.pathVariable("id"));
		return repository.getPerson(personId)
			.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
			.switchIfEmpty(ServerResponse.notFound().build());
	}
}
1 listPeople is a handler function that returns all Person objects found in the repository as JSON.
2 createPerson is a handler function that stores a new Person contained in the request body. Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty Mono that emits a completion signal when the person has been read from the request and stored. So we use the build(Publisher<Void>) method to send a response when that completion signal is received (that is, when the Person has been saved).
3 getPerson is a handler function that returns a single person, identified by the id path variable. We retrieve that Person from the repository and create a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono<T>) to return a 404 Not Found response.
Kotlin
class PersonHandler(private val repository: PersonRepository) {

	suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
		val people: Flow<Person> = repository.allPeople()
		return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
	}

	suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
		val person = request.awaitBody<Person>()
		repository.savePerson(person)
		return ok().buildAndAwait()
	}

	suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
		val personId = request.pathVariable("id").toInt()
		return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
				?: ServerResponse.notFound().buildAndAwait()

	}
}
4 listPeople is a handler function that returns all Person objects found in the repository as JSON.
5 createPerson is a handler function that stores a new Person contained in the request body. Note that PersonRepository.savePerson(Person) is a suspending function with no return type.
6 getPerson is a handler function that returns a single person, identified by the id path variable. We retrieve that Person from the repository and create a JSON response, if it is found. If it is not found, we return a 404 Not Found response.

Validation

函数式端点可以使用 Spring 的 validation facilities 对请求正文进行验证。例如,给定 Person 的自定义 Spring Validator 实现:

A functional endpoint can use Spring’s validation facilities to apply validation to the request body. For example, given a custom Spring Validator implementation for a Person:

Java
public class PersonHandler {

	private final Validator validator = new PersonValidator(); (1)

	// ...

	public Mono<ServerResponse> createPerson(ServerRequest request) {
		Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); (2)
		return ok().build(repository.savePerson(person));
	}

	private void validate(Person person) {
		Errors errors = new BeanPropertyBindingResult(person, "person");
		validator.validate(person, errors);
		if (errors.hasErrors()) {
			throw new ServerWebInputException(errors.toString()); (3)
		}
	}
}
1 Create Validator instance.
2 Apply validation.
3 Raise exception for a 400 response.
Kotlin
class PersonHandler(private val repository: PersonRepository) {

	private val validator = PersonValidator() (1)

	// ...

	suspend fun createPerson(request: ServerRequest): ServerResponse {
		val person = request.awaitBody<Person>()
		validate(person) (2)
		repository.savePerson(person)
		return ok().buildAndAwait()
	}

	private fun validate(person: Person) {
		val errors: Errors = BeanPropertyBindingResult(person, "person");
		validator.validate(person, errors);
		if (errors.hasErrors()) {
			throw ServerWebInputException(errors.toString()) (3)
		}
	}
}
4 Create Validator instance.
5 Apply validation.
6 Raise exception for a 400 response.

处理程序还可以创建一个全局`Validator`实例并将其注入,进而使用标准 Bean 验证 API (JSR-303),该实例基于`LocalValidatorFactoryBean`。请参见Spring Validation

Handlers can also use the standard bean validation API (JSR-303) by creating and injecting a global Validator instance based on LocalValidatorFactoryBean. See Spring Validation.

RouterFunction

路由函数用于将请求路由到对应的 HandlerFunction。通常,您不会自己编写路由函数,而是使用 RouterFunctions 实用类上的一个方法来创建一个。RouterFunctions.route()(无参数)为一个流畅的构建器,用于创建一个路由函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction) 提供了一个创建一个路由的直接方式。

Router functions are used to route the requests to the corresponding HandlerFunction. Typically, you do not write router functions yourself, but rather use a method on the RouterFunctions utility class to create one. RouterFunctions.route() (no parameters) provides you with a fluent builder for creating a router function, whereas RouterFunctions.route(RequestPredicate, HandlerFunction) offers a direct way to create a router.

通常建议使用 route() 构建器,因为它为典型的映射场景提供方便的快捷方式,而无需使用难以发现的静态导入。例如,路由函数构建器提供 GET(String, HandlerFunction) 方法来为 GET 请求创建一个映射;POST(String, HandlerFunction) 为 POST 请求创建一个映射。

Generally, it is recommended to use the route() builder, as it provides convenient short-cuts for typical mapping scenarios without requiring hard-to-discover static imports. For instance, the router function builder offers the method GET(String, HandlerFunction) to create a mapping for GET requests; and POST(String, HandlerFunction) for POSTs.

除了基于 HTTP 方法的映射之外,路由生成器还提供了一种方法,以便在映射到请求时引入附加谓词。对于每个 HTTP 方法,都有一个重载变量,它将 RequestPredicate 作为参数,尽管可以表达哪些附加约束条件。

Besides HTTP method-based mapping, the route builder offers a way to introduce additional predicates when mapping to requests. For each HTTP method there is an overloaded variant that takes a RequestPredicate as a parameter, though which additional constraints can be expressed.

Predicates

您可以编写自己的 RequestPredicate,但 RequestPredicates 实用类提供基于请求路径、HTTP 方法、内容类型等的常用实现。以下示例使用请求谓词基于 Accept 头创建约束:

You can write your own RequestPredicate, but the RequestPredicates utility class offers commonly used implementations, based on the request path, HTTP method, content-type, and so on. The following example uses a request predicate to create a constraint based on the Accept header:

  • Java

  • Kotlin

RouterFunction<ServerResponse> route = RouterFunctions.route()
	.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
		request -> ServerResponse.ok().bodyValue("Hello World")).build();
val route = coRouter {
	GET("/hello-world", accept(TEXT_PLAIN)) {
		ServerResponse.ok().bodyValueAndAwait("Hello World")
	}
}

您可以通过使用以下方法组合多个请求谓词:

You can compose multiple request predicates together by using:

  • RequestPredicate.and(RequestPredicate) — both must match.

  • RequestPredicate.or(RequestPredicate) — either can match.

RequestPredicates 中的许多谓词是组合的。例如,RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String) 组合而成。上面所示的示例还使用两个请求谓词,因为构建器在内部使用 RequestPredicates.GET,并将其与 accept 谓词组合。

Many of the predicates from RequestPredicates are composed. For example, RequestPredicates.GET(String) is composed from RequestPredicates.method(HttpMethod) and RequestPredicates.path(String). The example shown above also uses two request predicates, as the builder uses RequestPredicates.GET internally, and composes that with the accept predicate.

Routes

路由函数按顺序评估:如果第一个路由不匹配,则评估第二个路由,依此类推。因此,在通用路由之前声明更具体的路由是有意义的。在将路由函数注册为 Spring bean 时,这一点也很重要,如下所述。请注意,此行为不同于基于注释的编程模型,其中“最具体”的控制器方法是自动选择的。

Router functions are evaluated in order: if the first route does not match, the second is evaluated, and so on. Therefore, it makes sense to declare more specific routes before general ones. This is also important when registering router functions as Spring beans, as will be described later. Note that this behavior is different from the annotation-based programming model, where the "most specific" controller method is picked automatically.

使用路由函数构建器时,所有已定义的路由都组合成一个 RouterFunction,该 RouterFunctionbuild() 返回。还有其他方法可以将多个路由函数组合在一起:

When using the router function builder, all defined routes are composed into one RouterFunction that is returned from build(). There are also other ways to compose multiple router functions together:

  • add(RouterFunction) on the RouterFunctions.route() builder

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) — shortcut for RouterFunction.and() with nested RouterFunctions.route().

以下示例显示了四个路由的组合:

The following example shows the composition of four routes:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
	.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
	.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
	.POST("/person", handler::createPerson) (3)
	.add(otherRoute) (4)
	.build();
1 GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
2 GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople
3 POST /person with no additional predicates is mapped to PersonHandler.createPerson, and
4 otherRoute is a router function that is created elsewhere, and added to the route built.
Kotlin
import org.springframework.http.MediaType.APPLICATION_JSON

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute: RouterFunction<ServerResponse> = coRouter {  }

val route = coRouter {
	GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
	GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
	POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
5 GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
6 GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople
7 POST /person with no additional predicates is mapped to PersonHandler.createPerson, and
8 otherRoute is a router function that is created elsewhere, and added to the route built.

Nested Routes

通常,一组路由器函数具有共享谓词,例如共享路径。在上面的示例中,共享谓词将是路径谓词,它匹配 /person,三个路由使用该谓词。在使用注解时,可以通过使用映射到 /person 的类型级 @RequestMapping 注解来消除此重复行为。在 WebFlux.fn 中,可以通过路由函数生成器上的 path 方法共享路径谓词。例如,可以通过使用嵌套路由以以下方式改进上面示例的最后几行:

It is common for a group of router functions to have a shared predicate, for instance a shared path. In the example above, the shared predicate would be a path predicate that matches /person, used by three of the routes. When using annotations, you would remove this duplication by using a type-level @RequestMapping annotation that maps to /person. In WebFlux.fn, path predicates can be shared through the path method on the router function builder. For instance, the last few lines of the example above can be improved in the following way by using nested routes:

Java
RouterFunction<ServerResponse> route = route()
	.path("/person", builder -> builder (1)
		.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
		.GET(accept(APPLICATION_JSON), handler::listPeople)
		.POST(handler::createPerson))
	.build();
1 Note that second parameter of path is a consumer that takes the router builder.
Kotlin
val route = coRouter { (1)
	"/person".nest {
		GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
		GET(accept(APPLICATION_JSON), handler::listPeople)
		POST(handler::createPerson)
	}
}
2 Create router using Coroutines router DSL; a Reactive alternative is also available via router { }.

虽然基于路径的嵌套是最常见的,但是您可以通过使用构建器上的 nest 方法嵌套在任何类型的谓词上。上面仍然包含一些重复,形式为共享的 Accept 头谓词。我们可以通过将 nest 方法与 accept 一起使用来进一步改进:

Though path-based nesting is the most common, you can nest on any kind of predicate by using the nest method on the builder. The above still contains some duplication in the form of the shared Accept-header predicate. We can further improve by using the nest method together with accept:

  • Java

  • Kotlin

RouterFunction<ServerResponse> route = route()
	.path("/person", b1 -> b1
		.nest(accept(APPLICATION_JSON), b2 -> b2
			.GET("/{id}", handler::getPerson)
			.GET(handler::listPeople))
		.POST(handler::createPerson))
	.build();
val route = coRouter {
	"/person".nest {
		accept(APPLICATION_JSON).nest {
			GET("/{id}", handler::getPerson)
			GET(handler::listPeople)
			POST(handler::createPerson)
		}
	}
}

Serving Resources

WebFlux.fn 为提供服务提供内置支持。

WebFlux.fn provides built-in support for serving resources.

除了以下描述的功能之外,还可以实现更灵活的资源处理,这得益于 RouterFunctions#resource(java.util.function.Function)

In addition to the capabilities described below, it is possible to implement even more flexible resource handling thanks to RouterFunctions#resource(java.util.function.Function).

Redirecting to a resource

可以将与指定谓词匹配的请求重定向到资源。例如,这对于处理单页面应用程序中的重定向非常有用。

It is possible to redirect requests matching a specified predicate to a resource. This can be useful, for example, for handling redirects in Single Page Applications.

  • Java

  • Kotlin

ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
	.resource(spaPredicate, index)
	.build();
val redirectToIndex = router {
	val index = ClassPathResource("static/index.html")
	val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
	val spaPredicate = !(path("/api/**") or path("/error") or
		pathExtension(extensions::contains))
	resource(spaPredicate, index)
}

Serving resources from a root location

还可以将与给定模式匹配的请求路由到相对于给定根位置的资源。

It is also possible to route requests that match a given pattern to resources relative to a given root location.

  • Java

  • Kotlin

Resource location = new FileSystemResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
val location = FileSystemResource("public-resources/")
val resources = router { resources("/resources/**", location) }

Running a Server

如何在 HTTP 服务器中运行路由器函数?一种简单的选择是使用以下方法之一将路由器函数转换为一个 HttpHandler

How do you run a router function in an HTTP server? A simple option is to convert a router function to an HttpHandler by using one of the following:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

然后,你可以对返回的`HttpHandler`执行一系列服务器适配器,按照HttpHandler的服务器特定说明进行操作。

You can then use the returned HttpHandler with a number of server adapters by following HttpHandler for server-specific instructions.

另一个更典型的选项(也由 Spring Boot 使用)是通过使用 Spring 配置来声明处理请求所需的组件的WebFlux Config,基于DispatcherHandler运行设置。WebFlux Java 配置声明以下基础设施组件来支持功能性终端:

A more typical option, also used by Spring Boot, is to run with a DispatcherHandler-based setup through the WebFlux Config, which uses Spring configuration to declare the components required to process requests. The WebFlux Java configuration declares the following infrastructure components to support functional endpoints:

  • RouterFunctionMapping: Detects one or more RouterFunction<?> beans in the Spring configuration, orders them, combines them through RouterFunction.andOther, and routes requests to the resulting composed RouterFunction.

  • HandlerFunctionAdapter: Simple adapter that lets DispatcherHandler invoke a HandlerFunction that was mapped to a request.

  • ServerResponseResultHandler: Handles the result from the invocation of a HandlerFunction by invoking the writeTo method of the ServerResponse.

前面的组件允许功能性端点在 DispatcherHandler 请求处理生命周期中以及(可能)并排随注释控制器一起运行(如果声明了的话)。Spring Boot WebFlux 启动器启用功能性端点的方式也是如此。

The preceding components let functional endpoints fit within the DispatcherHandler request processing lifecycle and also (potentially) run side by side with annotated controllers, if any are declared. It is also how functional endpoints are enabled by the Spring Boot WebFlux starter.

下面的示例显示了 WebFlux Java 配置(请参见 DispatcherHandler 以了解如何运行它):

The following example shows a WebFlux Java configuration (see DispatcherHandler for how to run it):

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	@Bean
	public RouterFunction<?> routerFunctionA() {
		// ...
	}

	@Bean
	public RouterFunction<?> routerFunctionB() {
		// ...
	}

	// ...

	@Override
	public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
		// configure message conversion...
	}

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		// configure CORS...
	}

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		// configure view resolution for HTML rendering...
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	@Bean
	fun routerFunctionA(): RouterFunction<*> {
		// ...
	}

	@Bean
	fun routerFunctionB(): RouterFunction<*> {
		// ...
	}

	// ...

	override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
		// configure message conversion...
	}

	override fun addCorsMappings(registry: CorsRegistry) {
		// configure CORS...
	}

	override fun configureViewResolvers(registry: ViewResolverRegistry) {
		// configure view resolution for HTML rendering...
	}
}

Filtering Handler Functions

可使用路由函数生成器中的 beforeafterfilter 方法过滤处理程序函数。借助标注,可通过使用 @ControllerAdviceServletFilter 或同时使用两者,实现类似功能。过滤器将应用到生成器构建的所有路由。这意味着在嵌套路由中定义的过滤器不会应用到“顶级”路由。例如,考虑以下示例:

You can filter handler functions by using the before, after, or filter methods on the routing function builder. With annotations, you can achieve similar functionality by using @ControllerAdvice, a ServletFilter, or both. The filter will apply to all routes that are built by the builder. This means that filters defined in nested routes do not apply to "top-level" routes. For instance, consider the following example:

Java
RouterFunction<ServerResponse> route = route()
	.path("/person", b1 -> b1
		.nest(accept(APPLICATION_JSON), b2 -> b2
			.GET("/{id}", handler::getPerson)
			.GET(handler::listPeople)
			.before(request -> ServerRequest.from(request) (1)
				.header("X-RequestHeader", "Value")
				.build()))
		.POST(handler::createPerson))
	.after((request, response) -> logResponse(response)) (2)
	.build();
1 The before filter that adds a custom request header is only applied to the two GET routes.
2 The after filter that logs the response is applied to all routes, including the nested ones.
Kotlin
val route = router {
	"/person".nest {
		GET("/{id}", handler::getPerson)
		GET("", handler::listPeople)
		before { (1)
			ServerRequest.from(it)
					.header("X-RequestHeader", "Value").build()
		}
		POST(handler::createPerson)
		after { _, response -> (2)
			logResponse(response)
		}
	}
}
3 The before filter that adds a custom request header is only applied to the two GET routes.
4 The after filter that logs the response is applied to all routes, including the nested ones.

路由生成器中的 filter 方法接收一个 HandlerFilterFunction:一个接收 ServerRequestHandlerFunction 并返回 ServerResponse 的函数。处理程序函数参数代表链中的下一个元素。这通常是指被路由到的处理程序,但如果应用了多个过滤器,它也可以是另一个过滤器。

The filter method on the router builder takes a HandlerFilterFunction: a function that takes a ServerRequest and HandlerFunction and returns a ServerResponse. The handler function parameter represents the next element in the chain. This is typically the handler that is routed to, but it can also be another filter if multiple are applied.

现在,假设我们有一个可以确定特定路径是否被允许的 SecurityManager,我们可以向路由中添加一个简单的安全性过滤器。以下示例展示了如何执行此操作:

Now we can add a simple security filter to our route, assuming that we have a SecurityManager that can determine whether a particular path is allowed. The following example shows how to do so:

  • Java

  • Kotlin

SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
	.path("/person", b1 -> b1
		.nest(accept(APPLICATION_JSON), b2 -> b2
			.GET("/{id}", handler::getPerson)
			.GET(handler::listPeople))
		.POST(handler::createPerson))
	.filter((request, next) -> {
		if (securityManager.allowAccessTo(request.path())) {
			return next.handle(request);
		}
		else {
			return ServerResponse.status(UNAUTHORIZED).build();
		}
	})
	.build();
val securityManager: SecurityManager = ...

val route = router {
		("/person" and accept(APPLICATION_JSON)).nest {
			GET("/{id}", handler::getPerson)
			GET("", handler::listPeople)
			POST(handler::createPerson)
			filter { request, next ->
				if (securityManager.allowAccessTo(request.path())) {
					next(request)
				}
				else {
					status(UNAUTHORIZED).build();
				}
			}
		}
	}

上述示例表明调用 next.handle(ServerRequest) 是可选的。我们只有在允许访问时才运行处理程序函数。

The preceding example demonstrates that invoking the next.handle(ServerRequest) is optional. We only let the handler function be run when access is allowed.

除了在路由函数生成器中使用 filter 方法之外,还可通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用到现有路由函数。

Besides using the filter method on the router function builder, it is possible to apply a filter to an existing router function via RouterFunction.filter(HandlerFilterFunction).

通过专用 xref:web/webflux-cors.adoc#webflux-cors-webfilter[CorsWebFilter 提供对功能端点的 CORS 支持。

CORS support for functional endpoints is provided through a dedicated CorsWebFilter.