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
ServerRequest
和 ServerResponse
是不可变接口,提供了与 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();
您还可以使用异步结果作为主体,可以是 CompletableFuture
、Publisher
,或者任何其他类型,只要被 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
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();
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
你可以通过在路由函数构建器上使用 before
、after
或 filter
方法来过滤处理函数。
使用注解时,你可以通过使用 @ControllerAdvice
、ServletFilter
或两者来实现类似的功能。
该过滤器将应用于由构建器构建的所有路由。
这意味着嵌套路由中定义的过滤器不适用于“顶级”路由。
例如,请考虑以下示例:
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
:一个接受 ServerRequest
和 HandlerFunction
并返回 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
提供的。