Functional Endpoints

Infra Web MVC 包括 WebMvc.fn,这是一个轻量级的函数式编程模型,在该模型中,函数用于路由和处理请求,设计为不可变性。 它是基于注解编程模型的另一种选择,但在其他方面运行在相同的 DispatcherHandler 上。

概述

在 WebMvc.fn 中,一个 HTTP 请求通过 HandlerFunction 处理:一个接受 ServerRequest 并返回 ServerResponse 的函数。 请求和响应对象都具有不可变的合同,提供了与 JDK 8 兼容的访问 HTTP 请求和响应的方式。 HandlerFunction 相当于注解-based 编程模型中 @RequestMapping 方法的主体。

传入的请求通过 RouterFunction 路由到处理函数:一个接受 ServerRequest 并返回可选的 HandlerFunction(即 Optional<HandlerFunction>)的函数。 当路由函数匹配时,会返回一个处理函数;否则返回一个空的 Optional。 RouterFunction 相当于 @RequestMapping 注解,但主要区别在于路由函数不仅提供数据,还提供行为。

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

import static cn.taketoday.http.MediaType.APPLICATION_JSON;
import static cn.taketoday.web.handler.function.ReqestPredicates.*;
import static cn.taketoday.web.handler.function.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 ServerResponse listPeople(ServerRequest request) {
    // ...
  }

  public ServerResponse createPerson(ServerRequest request) {
    // ...
  }

  public ServerResponse getPerson(ServerRequest request) {
    // ...
  }
}
1 使用 route() 创建路由。

如果您将 RouterFunction 注册为一个 bean,例如通过在 @Configuration 类中公开它, 它将被 IoC 自动检测到,如 Running a Server 中所解释的那样。

HandlerFunction

ServerRequestServerResponse 是不可变接口,提供了与 JDK 8 兼容的访问 HTTP 请求和响应的方式,包括头、主体、方法和状态码。

ServerRequest

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

以下示例将请求主体提取为 String

String string = request.body(String.class);

以下示例将主体提取为 List<Person>,其中 Person 对象从序列化形式(如 JSON 或 XML)解码:

List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});

以下示例演示了如何访问参数:

MultiValueMap<String, String> params = request.params();

ServerResponse

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

Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

以下示例演示了如何构建一个带有 Location 头且没有主体的 201 (CREATED) 响应:

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

您还可以使用异步结果作为主体,可以是 CompletableFuturePublisher,或者任何其他类型,只要被 ReactiveAdapterRegistry 支持。例如:

Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

如果不仅主体,而且状态或头也是基于异步类型的,您可以使用 ServerResponse 上的静态 async 方法, 它接受 CompletableFuture<ServerResponse>Publisher<ServerResponse>,或者任何其他由 ReactiveAdapterRegistry 支持的异步类型。例如:

Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
  .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);

通过 ServerResponse 上的静态 sse 方法可以提供 Server-Sent Events。 该方法提供的构建器允许您发送字符串或其他对象作为 JSON。例如:

public RouterFunction<ServerResponse> sse() {
  return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
        // Save the sseBuilder object somewhere..
      }));
}

// In some other thread, sending a String
sseBuilder.send("Hello world");

// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);

// Customize the event by using the other methods
sseBuilder.id("42")
    .event("sse event")
    .data(person);

// and done at some point
sseBuilder.complete();

Handler Classes

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

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

这很方便,但在应用程序中我们需要多个函数,多个内联 lambda 会变得混乱。 因此,将相关的处理函数组合到一个处理器类中是有用的,它的作用类似于注解驱动的应用程序中的 @Controller。 例如,下面的类暴露了一个响应式的 Person 存储库:

import static cn.taketoday.http.MediaType.APPLICATION_JSON;
import static cn.taketoday.web.handler.function.ServerResponse.ok;

public class PersonHandler {

  private final PersonRepository repository;

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

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

  public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
    Person person = request.body(Person.class);
    repository.savePerson(person);
    return ok().build();
  }

  public ServerResponse getPerson(ServerRequest request) { (3)
    int personId = Integer.parseInt(request.pathVariable("id"));
    Person person = repository.getPerson(personId);
    if (person != null) {
      return ok().contentType(APPLICATION_JSON).body(person);
    }
    else {
      return ServerResponse.notFound().build();
    }
  }

}
1 listPeople 是一个处理函数,它以 JSON 格式返回存储库中找到的所有 Person 对象。
2 createPerson 是一个处理函数,用于存储请求体中包含的新 Person
3 getPerson 是一个处理函数,它返回由 id 路径变量标识的单个人。 如果找到该 Person,我们从存储库中检索并创建一个 JSON 响应。如果未找到,我们返回一个 404 Not Found 响应。

Validation

一个 functional endpoint 可以使用 验证设施 来对请求体应用验证。 例如,给定一个针对 Person 的自定义 Infra 验证器 实现:

public class PersonHandler {

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

  // ...

  public ServerResponse createPerson(ServerRequest request) {
    Person person = request.body(Person.class);
    validate(person); (2)
    repository.savePerson(person);
    return ok().build();
  }

  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 创建 Validator 实例.
2 应用验证.
3 抛一个 400 异常的响应.

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

RouterFunction

路由函数用于将请求路由到相应的 HandlerFunction。通常情况下,您不需要自己编写路由函数, 而是使用 RouterFunctions 实用类上的方法来创建一个。

RouterFunctions.route()(无参数)为您提供了一个流畅的构建器,用于创建路由函数, 而 RouterFunctions.route(RequestPredicate, HandlerFunction) 则提供了一种直接创建路由的方式。

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

除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射到请求时引入额外断言的方法。 对于每个 HTTP 方法,都有一个重载变体,它接受一个 RequestPredicate 作为参数,通过该参数可以表达额外的约束。

Predicates

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

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

您可以通过以下方式将多个请求断言组合在一起:

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

  • RequestPredicate.or(RequestPredicate) — 任何一个都可以匹配。

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

Routes

路由函数按顺序进行评估:如果第一个路由不匹配,则评估第二个,依此类推。 因此,将更具体的路由声明在一般性路由之前是有意义的。 这也在将路由函数注册为 Infra bean 时很重要,稍后将进行描述。 请注意,此行为与基于注解的编程模型不同,在该模型中,“最具体”的控制器方法会自动选择。

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

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

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) — 使用嵌套的 RouterFunctions.route()RouterFunction.and() 的快捷方式。

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

import static cn.taketoday.http.MediaType.APPLICATION_JSON;
import static cn.taketoday.web.handler.function.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 当具有与 JSON 匹配的 Accept 头的 GET /person/{id} 请求通过时,路由到 PersonHandler.getPerson
2 当具有与 JSON 匹配的 Accept 头的 GET /person 请求通过时,路由到 PersonHandler.listPeople
3 当没有其他断言的 POST /person 请求映射到 PersonHandler.createPerson,以及
4 otherRoute 是在其他地方创建的路由函数,并添加到构建的路由中。

Nested Routes

一组路由函数经常具有共享的断言,例如共享路径。 在上面的示例中,共享的断言将是一个路径断言,匹配 /person,被三个路由使用。 在使用注解时,您可以通过使用类型级别的 @RequestMapping 注解将其映射到 /person 来消除此重复。 在 WebMvc.fn 中,路径断言可以通过路由函数构建器上的 path 方法共享。 例如,通过使用嵌套路由,上面示例的最后几行可以通过以下方式改进:

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 请注意,path 的第二个参数是一个消费者,它接受路由构建器。

虽然基于路径的嵌套是最常见的,但您可以通过在构建器上使用 nest 方法对任何类型的断言进行嵌套。 上面仍然包含一些重复,即共享的 Accept 头断言。 我们可以通过结合使用 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();

Serving Resources

WebMvc.fn 提供了内置支持用于提供资源。

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

重定向到资源

可以将匹配指定谓词的请求重定向到资源。例如,在单页应用程序中处理重定向时可能很有用。

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

从根路径提供资源服务

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

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

Running a Server

通常,在基于 DispatcherHandler 的设置中通过 MVC Config 运行路由器函数,该配置使用 Infra 配置声明处理请求所需的组件。 MVC Java 配置声明以下基础设施组件以支持功能端点:

  • RouterFunctionMapping:在 Infra 配置中检测一个或多个 RouterFunction<?> bean, 对其进行排序, 通过 RouterFunction.andOther 将它们组合,并将请求路由到生成的组合 RouterFunction

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

上述组件使功能端点适应 DispatcherHandler 请求处理生命周期,并且(可能)与已声明的注释控制器并行运行。 这也是通过 App Web 启动器启用功能端点的方式。

以下示例显示了一个 Web Java 配置:

@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {

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

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

  // ...

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    // configure message conversion...
  }

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

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
    // configure view resolution for HTML rendering...
  }
}

Filtering Handler Functions

你可以通过在路由函数构建器上使用 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();
1 添加自定义请求头的 before 过滤器仅应用于这两个 GET 路由。
2 记录响应的 after 过滤器应用于所有路由,包括嵌套的路由。

路由构建器上的 filter 方法接受一个 HandlerFilterFunction:一个接受 ServerRequestHandlerFunction 并返回 ServerResponse 的函数。 处理程序函数参数表示链中的下一个元素。 这通常是被路由到的处理程序,但如果应用了多个过滤器,则还可以是另一个过滤器。

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

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

前面的示例演示了调用 next.handle(ServerRequest) 是可选的。 只有在访问被允许时,我们才允许处理程序函数运行。

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

注意:对于功能端点的 CORS 支持是通过专用的 CorsFilter 提供的。