实用!在 Spring WebFlux 中实现函数式端点的最佳实践

科技   2024-10-24 07:31   河北  


实用!在 Spring WebFlux 中实现函数式端点的最佳实践

WebFlux 是一种响应式编程模型,旨在处理高并发的应用场景,适用于微服务架构和实时数据处理。在这个背景下,传统的阻塞式处理方式往往无法满足性能和可扩展性的要求,因此 WebFlux 提供了一种非阻塞的方式来处理 HTTP 请求。

在这一过程中,函数式端点的引入不仅使路由和处理逻辑得以清晰分离,还提高了代码的可维护性和可读性。通过灵活的请求谓词,我们可以根据不同条件动态地配置路由,实现更精细的请求处理。同时,利用过滤器机制,我们可以在请求处理过程中轻松加入安全、日志等跨切面功能,从而提升应用的整体架构设计。

本文将深入探讨如何在 WebFlux 中有效利用函数式端点,通过实际代码示例展示其在 API 开发中的优势,以便能够在实际项目中灵活应用这一强大工具。

通常,我们习惯通过注解控制器来暴露 API,如下所示:

@GetMapping("/products")
public Flux<ProductDto> allProducts() {
return this.productService.getAllProducts();
}

这种方式简单明了,我们总能预期结果。

WebFlux 提供了一种替代方式来暴露 API,即函数式端点。其主要逻辑围绕 RouterFunction 和 HandlerFunction 编写。

  • HandlerFunction 是一个函数接口,为每个传入请求返回生成的响应 — Mono<ServerResponse>

  • RouterFunction 是 @RequestMapping 注解的等价物。

以下是示例:

@Bean
public RouterFunction<ServerResponse> route(ProductService productService) {
return RouterFunctions.route()
.GET("/products",
req ->
this.productService
.getAllProducts()
.as(productFlux -> ServerResponse.ok().body(productFlux, ProductDto.class)))
.build();
}

实际上,我们将所有路由逻辑作为 Beans 以函数式风格暴露。

ServerRequest

函数式端点使用 ServerRequest 从 HTTP 请求中检索数据。该类提供访问请求所有部分的方法:路径变量、查询参数、头部、cookies 及请求体。

路径变量

String productId = request.pathVariable("id");

此方法允许您从 URL 的部分获取数据。例如,从 /products/{id} 中获取 {id}

查询参数

String page = request.queryParam("page").orElse("0");

此方法允许您从 ? 字符后的查询字符串中获取数据。

头部

String contentType = request.headers()
.contentType()
.orElse(MediaType.APPLICATION_JSON).toString();

请求体

Mono<User> productMono = request.bodyToMono(Product.class);
Flux<User> productFlux = request.bodyToFlux(Product.class);

请求体可以使用 bodyToMono 或 bodyToFlux 方法转换为对象,具体取决于我们是否期望单个对象或数据流。

ServerResponse

用于向客户端发送 HTTP 响应。可以通过多种方法自定义响应状态、头部、响应体及其他方面。

ServerResponse 的基本方法

创建简单响应并设置响应状态

Mono<ServerResponse> responseOk = ServerResponse.ok().build();
Mono<ServerResponse> responseNotFound = ServerResponse.notFound().build();
Mono<ServerResponse> responseConflict = ServerResponse.status(HttpStatus.CONFLICT).build();

添加头部

Mono<ServerResponse> responseOkWithHeadersV1 =
ServerResponse.ok()
.header("Custom-Header1", "value1")
.header("Custom-Header2", "value2")
.build();

Mono<ServerResponse> responseOkWithHeadersV2 =
ServerResponse.ok()
.headers(httpHeaders -> {
httpHeaders.add("Custom-Header1", "value1");

httpHeaders.setBasicAuth("user", "password");
httpHeaders.setBearerAuth("some-value");
})
.build();

优先选择第二种,因为 httpHeaders 已经有许多现成的方法,可以满足大多数业务场景。

添加响应体

Mono<ServerResponse> responseOkWithBodyValue =
ServerResponse.ok().bodyValue(product);

Mono<ServerResponse> responseOkWithBodyMono =
ServerResponse.ok().body(productMono);

body 和 bodyValue 方法用于指定响应的主体。您可以将主体作为对象或数据流(反应式类型)发送。

路由函数中的顺序重要性

路由的顺序很重要——第一个匹配的路由将被调用,路由是从上到下处理的。

@Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions.route()
.GET("/products", this.requestHandler::allProducts)
.GET("/products/{id}", this.requestHandler::getOneProduct)
.GET("/products/paginated", this.requestHandler::pageProducts)
.build();
}

如果反转顺序,那么 GET /products/paginated 将永远无法被访问,因为 GET /products/{id} 将拦截请求。

因此,在设置端点时需要小心。

多个路由函数

如果有多个端点,可以创建多个路由函数并将其设置为 Beans。这种方法更易理解,因为我们将代码分成逻辑块。

@Configuration
public class RouteConfiguration {

@Bean
public RouterFunction<ServerResponse> productRoute(ProductHandler productHandler, ExceptionHandler exceptionHandler) {
return RouterFunctions.route()
.GET("/products", request -> productHandler.getAll())
.GET("/products/{id}", productHandler::getOne)
.POST("/products", productHandler::save)
.PUT("/products/{id}", productHandler::update)
.DELETE("/products/{id}", productHandler::delete)
.onError(EntityNotFoundException.class, exceptionHandler::handleException)
.build();
}

@Bean
public RouterFunction<ServerResponse> orderRoute(OrderHandler orderHandler, ExceptionHandler exceptionHandler) {
return RouterFunctions.route()
.GET("/orders", orderHandler::getAllForUser)
.GET("/orders/{id}", orderHandler::getOne)
.POST("/orders", orderHandler::save)
.PUT("/orders/{id}", orderHandler::update)
.DELETE("/orders/{id}", orderHandler::delete)
.onError(EntityNotFoundException.class, exceptionHandler::handleException)
.build();
}
}

如果产品和订单的端点抛出 EntityNotFoundException,则错误处理程序需要添加到两个路由器中。

嵌套路由函数

如果不喜欢将其作为单独的 Beans 暴露,那么可以拥有一个高层路由器,可以路由到子路由函数。让我们使用嵌套路由函数改造我们的 RouteConfiguration

@Bean
public RouterFunction<ServerResponse> nestedRoute(
ProductHandler productHandler, OrderHandler orderHandler, ExceptionHandler exceptionHandler) {
return RouterFunctions.route()
.GET("/products", request -> productHandler.getAll())
.GET("/products/{id}", productHandler::getOne)
.POST("/products", productHandler::save)
.PUT("/products/{id}", productHandler::update)
.DELETE("/products/{id}", productHandler::delete)
.path("orders", () -> orderRouteNested(orderHandler))
.onError(EntityNotFoundException.class, exceptionHandler::handleException)
.build();
}

private RouterFunction<ServerResponse> orderRouteNested(OrderHandler orderHandler) {
return RouterFunctions.route()
.GET(orderHandler::getAllForUser)
.GET("/{id}", orderHandler::getOne)
.POST(orderHandler::save)
.PUT("/{id}", orderHandler::update)
.DELETE("/{id}", orderHandler::delete)
.build();
}

在这种情况下,错误处理只需要在一个地方进行。

WebFilters

还可以配置过滤器以实现横切逻辑

过滤器可以像往常一样添加——实现 WebFilter 并重写方法。我们还有能力在单个路由器的级别为函数式端点添加过滤器。

我们将实现一个仅适用于 /orders 的授权过滤器:

@Component
public class SecurityFilter {

public Mono<ServerResponse> adminRoleFilter(ServerRequest request, HandlerFunction<ServerResponse> next) {
return SecurityFilter.requireRole("ADMIN", request.exchange())
.flatMap(exchange -> next.handle(request))
.onErrorResume(SecurityException.class, ex -> ServerResponse.status(HttpStatus.FORBIDDEN).build());
}

private static Mono<ServerWebExchange> requireRole(String role, ServerWebExchange exchange) {
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Authentication authentication = securityContext.getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails userDetails) {
if (userDetails.getAuthorities().stream()
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_" + role))) {
return Mono.just(exchange);
}
}
}
return Mono.error(new SecurityException("Access Denied"));
});
}
}

在 RouterFunction 中添加:

private RouterFunction<ServerResponse> orderRouteNested(
OrderHandler orderHandler, SecurityFilter securityFilter) {
return RouterFunctions.route()
.GET(orderHandler::getAllForUser)
.GET("/{id}", orderHandler::getOne)
.POST(orderHandler::save)
.PUT("/{id}", orderHandler::update)
.DELETE("/{id}", orderHandler::delete)
.filter(securityFilter::adminRoleFilter) //<--- 这个
.build();
}

在这个例子中,过滤器只会作用于 /orders 端点。

重要的是要注意,添加过滤器的顺序很重要,因此如果需要严格的过滤器顺序,应该逐个添加:


.filter(filter1) // 首先执行
.filter(filter2) // 第二个
.filter(filter3) // 第三个

请求谓词

请求谓词用于定义将请求路由到适当处理程序的条件。它们允许您根据 HTTP 请求的各种特征(如方法、路径、头部、请求参数等)灵活而准确地配置路由。

@Bean
public RouterFunction<ServerResponse> nestedRoute(
ProductHandler productHandler,
OrderHandler orderHandler,
ExceptionHandler exceptionHandler,
SecurityFilter securityFilter,
LoggingFilter loggingFilter) {
return RouterFunctions.route()
.GET(
"/products",
RequestPredicates.accept(MediaType.APPLICATION_JSON),
request -> productHandler.getAll())
.GET(
"/products/{id}",
RequestPredicates.headers(headers ->
null != headers.firstHeader("x-api-key")
&& headers.firstHeader("x-api-key").equals("some-val")
),
productHandler::getOne)
...
}

RequestPredicates 是一个具有许多辅助方法的类。

在这个例子中:

  1. **/products** — 仅当 Accept = application/json 时才能调用,否则会出现路径未找到的错误。

  2. **/products/{id}** — 只有在有 x-api-key 头且其值为 some-val 时才能调用,否则会返回 404

你也可以有相同的路径,但根据我们在谓词中放置的逻辑路由到不同的方法:

private RouterFunction<ServerResponse> orderRouteNested(
OrderHandler orderHandler, SecurityFilter securityFilter) {
return RouterFunctions.route()
.GET(isOperation("get-for-user"), orderHandler::getAllForUser)
.GET(isOperation("get-for-all"), orderHandler::getAll)
...
.build();
}

private RequestPredicate isOperation(String operation) {
return RequestPredicates.headers(h -> operation.equals(h.firstHeader("operation")));
}

结论

我们讨论了如何在 WebFlux 中使用函数式端点来创建 API。通过使用 RouterFunction 和 HandlerFunction 在代码中定义路由和处理程序,这种技术将路由与过滤器结合起来,并结合路由功能,使我们能够生成可扩展的 Web 应用程序。

我们在函数式端点的帮助下实现了这一目标:

  1. 灵活性和控制: — 动态路由定义和组合提高了对请求处理的控制。

  2. 可读性和支持: — 将逻辑分离到不同的 Bean 中改善了代码结构并简化了维护。

  3. 模块化和灵活性: — 为应用程序的不同部分创建多个 RouterFunctions 增加了代码的模块化和灵活性。


今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。


AI资源聚合站已经正式上线,该平台不仅仅是一个AI资源聚合站,更是一个为追求知识深度和广度的人们打造的智慧聚集地。通过访问 AI 资源聚合网站 https://ai-ziyuan.techwisdom.cn/,你将进入一个全方位涵盖人工智能和语言模型领域的宝藏库


作者:路条编程(转载请获本公众号授权,并注明作者与出处)

路条编程
路条编程是一个友好的社区,在这里你可以免费学习编程技能,我们旨在激励想学编程的人尝试新的想法和技术,在最短的时间学习到工作中使用到的技术!
 最新文章