跳到主要内容
版本:7.0.3

功能端点

Hunyuan 7b 中英对照 Functional Endpoints

Spring WebFlux 包含了 WebFlux.fn,这是一种轻量级的函数式编程模型,其中使用函数来路由和处理请求,并且契约设计遵循不可变性原则。它是基于注解的编程模型的一种替代方案,但在其他方面仍然运行在相同的 Reactive Core 基础之上。

概述

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

传入的请求会被路由到一个名为RouterFunction的处理函数中:这个函数接收ServerRequest,并返回一个延迟执行的HandlerFunction(即Mono<HandlerFunction>)。当路由函数匹配到请求时,就会返回相应的处理函数;否则则返回一个空值MonoRouterFunction相当于@RequestMapping注解,但主要区别在于RouterFunction不仅提供数据,还提供相应的处理逻辑(行为)。

RouterFunctions.route() 提供了一个路由器构建器,便于创建路由器,如下例所示:

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) {
// ...
}
}
  • 使用 route() 创建路由。

运行RouterFunction的一种方法是将其转换为HttpHandler,然后通过内置的服务器适配器之一进行安装:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

大多数应用程序都可以通过WebFlux Java配置来运行,详见运行服务器

HandlerFunction

ServerRequestServerResponse 是不可变的接口,它们提供了对 HTTP 请求和响应的访问方式,且与 JDK 8 兼容。请求和响应都支持通过 Reactive Streams 对请求体流(body streams)进行背压(back pressure)操作。请求体由 Reactor 的 FluxMono 表示;响应体则可由任何 Reactive Streams 的 Publisher 表示,包括 FluxMono。有关更多详细信息,请参阅 Reactive Libraries

ServerRequest

ServerRequest 提供了对 HTTP 方法、URI、请求头(headers)和查询参数(query parameters)的访问权限,而通过 body 方法则可以访问请求体(body)。

以下示例将请求体提取为Mono<String>

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

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

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

前面的例子都是使用了更通用的 ServerRequest.body(BodyExtractor) 的快捷方式,该方法接受 BodyExtractor 函数式策略接口。工具类 BodyExtractors 提供了对多个这类实例的访问。例如,前面的例子也可以写成如下形式:

Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

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

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

以下示例展示了如何将多部分数据作为映射(map)来访问:

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

以下示例展示了如何以流式方式一次访问一个部分的多部分数据:

request.bodyToFlux(PartEvent.class).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
}
return Mono.empty();
}));
备注

必须完全处理、传递或释放PartEvent对象的正文内容,以避免内存泄漏。

以下展示了如何通过DataBinder绑定请求参数、URI变量或头部信息,同时也展示了如何自定义DataBinder

Mono<Pet> pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));

ServerResponse

ServerResponse 提供了对 HTTP 响应的访问,由于它是不可变的,因此你可以使用 build 方法来创建它。你可以通过这个构建器设置响应状态、添加响应头,或者提供响应体。以下示例创建了一个包含 JSON 内容的 200(OK)响应:

Mono<Person> person = getPerson();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);

以下示例展示了如何构建一个包含Location头部但不包含正文的201(已创建)响应:

URI location = ...
return ServerResponse.created(location).build();

根据所使用的编解码器,可以传递提示参数来自定义数据体的序列化或反序列化方式。例如,要指定一个Jackson JSON 视图

return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);

处理器类

我们可以像下面的例子所示,将处理函数写成lambda表达式:

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

那确实很方便,但在实际应用中我们需要多个函数,而过多的内联lambda表达式会让人代码阅读起来变得混乱。因此,将相关的处理函数组合到一个处理类中会很有用,这个处理类的作用类似于基于注解的应用程序中的@Controller。例如,以下类暴露了一个响应式的Person仓库:

public class PersonHandler {

private final PersonRepository repository;

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

// listPeople is a handler function that returns all Person objects found
// in the repository as JSON
public Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}

// 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)
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}

// 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.
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}

验证

一个功能性的端点可以使用Spring的验证机制来对请求体进行验证。例如,对于一个自定义的Person类型的Spring Validator实现:

public class PersonHandler {

// Create Validator instance
private final Validator validator = new PersonValidator();

private final PersonRepository repository;

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

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

private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
// Raise exception for a 400 response
throw new ServerWebInputException(errors.toString());
}
}
}

处理程序还可以通过创建并注入基于LocalValidatorFactoryBean的全局Validator实例来使用标准的Bean验证API(JSR-303)。请参阅Spring Validation

RouterFunction

路由器函数用于将请求路由到相应的HandlerFunction。通常,你不需要自己编写路由器函数,而是使用RouterFunctions工具类中的方法来创建它。RouterFunctions.route()(无参数)提供了一个流畅的构建器来创建路由器函数,而Router Functions.route(RequestPredicate, HandlerFunction)则提供了一种直接创建路由器的方法。

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

除了基于HTTP方法的映射之外,路由构建器还提供了一种在映射请求时引入额外谓词(predicate)的方法。对于每种HTTP方法,都有一个重载版本,该版本接受一个RequestPredicate作为参数,通过这种方式可以表达额外的约束条件。

谓语

你可以编写自己的 RequestPredicate,但 RequestPredicates 工具类提供了针对常见需求的内置选项,这些选项可以根据 HTTP 方法、请求路径、请求头、API 版本 等来进行匹配。

以下示例使用了Accept头部和请求谓词:

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

你可以通过使用以下方法将多个请求谓词组合在一起:

  • RequestPredicate.and(RequestPredicate) — 两者都必须匹配。

  • RequestPredicate.or(RequestPredicate) — 只要有一个匹配即可。

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

路由

路由器的功能会按顺序进行评估:如果第一个路由不匹配,就会评估第二个路由,依此类推。因此,在定义通用路由之前,先定义更具体的路由是合理的。当将路由器功能注册为Spring Bean时,这一点也很重要,后面会有相关描述。需要注意的是,这种行为与基于注解的编程模型不同,在基于注解的模型中,“最具体”的控制器方法会自动被选中。

在使用路由器函数构建器时,所有定义的路由会被组合成一个RouterFunction,这个RouterFunction会从build()方法中返回。当然,还有其他方法可以将多个路由器函数组合在一起:

  • RouterFunctions.route() 构建器上使用 add(RouterFunction)

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) —— 这是带有嵌套 RouterFunctions.route()RouterFunction.and() 的快捷方式。

以下示例展示了四条路由的组成:

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

RouterFunction<ServerResponse> otherRoute = getOtherRoute();

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

嵌套路由

通常情况下,一组路由函数会共享一个相同的谓词(predicate),例如一个共同的路径。在上面的例子中,这个共享的谓词是一个与“/person”匹配的路径谓词,被三个路由所使用。当使用注解时,可以通过使用类型级别的@RequestMapping注解来消除这种重复,该注解会将请求路径映射到“/person”。在WebFlux.fn中,可以通过路由函数构建器(router function builder)上的path方法来共享路径谓词。例如,上面例子中的最后几行可以通过使用嵌套路由(nested routes)以以下方式进行优化:

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();
  • 注意 path 方法的第二个参数是一个消费者(consumer),它接收路由构建器(router builder)。

虽然基于路径的嵌套是最常见的方法,但通过使用构建器(builder)上的nest方法,你也可以对任何类型的谓词(predicate)进行嵌套。上述代码中仍然存在一些重复,比如共享的Accept头部谓词(shared Accept-header predicate)。我们可以通过结合使用nest方法和accept来进一步优化:

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();

API版本

路由器功能支持按API版本进行匹配。

首先,在WebFlux Config中启用API版本控制,然后你就可以如下使用version 谓词了:

RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", version("1.2"),
request -> ServerResponse.ok().bodyValue("Hello World")).build();

version谓词可以是:

  • 固定版本(“1.2”)——仅匹配给定的版本。
  • 基线版本(“1.2+”)——匹配给定版本及以上版本,直至最高支持的版本(见 webmvc/mvc-config/api-version.md)。

有关底层基础设施和API版本控制的更多详细信息,请参阅API版本控制

提供资源

WebFlux.fn提供了内置的支持来提供资源服务。

备注

除了下面描述的功能外,还可以借助RouterFunctions#resource(java.util.function.Function)来实现更加灵活的资源处理。

重定向到资源

可以将与指定谓词匹配的请求重定向到某个资源。例如,在单页应用程序(Single Page Applications)中处理重定向时,这种方式会非常有用。

ClassPathResource index = new ClassPathResource("static/index.html");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
.resource(spaPredicate, index)
.build();

从根位置提供资源

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

Resource location = new FileUrlResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);

运行服务器

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

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

然后,你可以使用返回的HttpHandler与多种服务器适配器一起使用,具体操作请参考HttpHandler中的服务器特定说明。

一个更为常见的选择(Spring Boot也采用这种方式)是通过WebFlux Config来运行基于DispatcherHandler的配置,该配置利用Spring配置来声明处理请求所需的组件。WebFlux Java配置声明了以下基础设施组件以支持函数式端点:

  • RouterFunctionMapping:在Spring配置中检测一个或多个RouterFunction<?>bean,对它们进行排序,通过RouterFunction.andOther将它们组合起来,然后将请求路由到组合后的RouterFunction

  • HandlerFunctionAdapter:一个简单的适配器,允许DispatcherHandler调用被映射到请求的HandlerFunction

  • ServerResponseResultHandler:通过调用ServerResponsewriteTo方法来处理HandlerFunction调用结果。

前述组件使得函数式端点能够适应DispatcherHandler的请求处理生命周期,并且(如果声明了注解控制器的话)还可以与这些控制器并行运行。Spring Boot WebFlux启动器正是通过这种方式来启用函数式端点的。

以下示例展示了一个WebFlux Java配置(有关如何运行它,请参见DispatcherHandler):

@Configuration
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...
}
}

过滤处理函数

你可以通过在路由函数构建器上使用beforeafterfilter方法来过滤处理函数。通过注释,你也可以使用@ControllerAdviceServletFilter或两者结合来实现类似的功能。这些过滤器将应用于构建器创建的所有路由。这意味着在嵌套路由中定义的过滤器不会应用于“顶级”路由。例如,考虑以下示例:

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();
}
  • before过滤器用于添加自定义请求头,仅应用于这两个GET路由。

  • after过滤器用于记录响应,适用于所有路由,包括嵌套路由。

路由器构建器(router builder)上的filter方法接受一个HandlerFilterFunction:这个函数接收一个ServerRequest和一个HandlerFunction,并返回一个ServerResponseHandlerFunction参数代表处理链中的下一个元素。这通常是被路由到的处理器(handler),但如果应用了多个过滤器,它也可能是一个其他的过滤器。

现在我们可以在我们的路由中添加一个简单的安全过滤器,前提是我们有一个SecurityManager,它可以判断某个路径是否被允许。以下示例展示了如何实现这一点:

SecurityManager securityManager = getSecurityManager();

RouterFunction<ServerResponse> route = RouterFunctions.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();

前面的例子表明,调用next.handle(ServerRequest)是可选的。我们只在允许访问时才运行处理函数。

除了在路由器函数构建器上使用filter方法外,还可以通过RouterFunction.filter(HandlerFilterFunction)对现有的路由器函数应用过滤器。

备注

函数端点的CORS支持是通过一个专用的CorsWebFilter提供的。