You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Webflux拦截请求体指定键值对返回400及架构设计疑问

问题原因分析

  • 你写的request.bodyToMono(Comment.class)链式调用只是构造了响应式流,没有被订阅也没有作为返回值返回,Webflux不会主动执行这部分没有被加入到处理链路中的流,所以逻辑完全不生效
  • 同步try-catch无法捕获响应式流内部发射的异常信号,NoSuchElementException是流在空数据时发出的错误信号,不会以同步异常的形式抛出,所以这部分捕获逻辑也无效
  • 当前逻辑没有在检测到commentId存在时中断请求流程,最后还是返回了next.handle(request)将请求转发给后续handler处理

修复后的路由过滤器代码

@Slf4j
@Configuration
public class CommentsRouter {

    @Bean
    public RouterFunction<ServerResponse> commentsRoute(CommentsHandler commentsHandler) {
        return RouterFunctions
                .route(GET("/v1/comments/{parentId}"), commentsHandler::getComments)
                .andRoute(GET("/v1/comments/{commentId}"), commentsHandler::getComment)
                .andRoute(POST("/v1/comments").and(accept(APPLICATION_JSON)), commentsHandler::addComment)
                .filter(((request, next) -> {
                    // 只拦截指定POST请求
                    if (request.path().equalsIgnoreCase("/v1/comments") && request.method() == HttpMethod.POST) {
                        Map<String, String> commentIdError = Collections.singletonMap("error", "Cannot assign commentId from client");
                        Map<String, String> emptyBodyError = Collections.singletonMap("error", "No content found while creating comment");
                        
                        // 读取请求体并做校验,将校验逻辑加入响应式链路
                        return request.bodyToMono(Comment.class)
                                // 处理空请求体的情况
                                .switchIfEmpty(Mono.defer(() -> {
                                    log.error("Empty body found for request {}", request.path());
                                    return ServerResponse.badRequest()
                                            .contentType(MediaType.APPLICATION_JSON)
                                            .bodyValue(emptyBodyError);
                                }))
                                // 校验commentId是否存在非空
                                .flatMap(comment -> {
                                    if (Objects.nonNull(comment.getCommentId()) 
                                            && !comment.getCommentId().isBlank()) {
                                        log.error("Pre-Assigned commentId found in payload {} for {} request {}", comment, request.methodName(), request.path());
                                        return ServerResponse.badRequest()
                                                .contentType(MediaType.APPLICATION_JSON)
                                                .bodyValue(commentIdError);
                                    }
                                    // 校验通过,将请求体重新包装后传给后续handler,否则后续handler读取不到body
                                    return next.handle(ServerRequest.from(request)
                                            .body(comment)
                                            .build());
                                });
                    }
                    // 非目标请求直接放行
                    return next.handle(request);
                }));
    }
}

注意必须把校验后的请求体重新包装到ServerRequest里传给后续handler,因为请求体的流默认只能被读取一次,如果直接读了不重写,后续handler会拿不到请求体内容

校验逻辑存放位置的设计建议

  • 如果这个commentId的校验规则是仅针对当前/v1/comments的POST接口的业务规则,建议迁移到Service层,更符合单一职责原则:路由层只负责请求转发,Handler层负责参数解析、响应封装,Service层负责业务校验和逻辑处理,后续维护业务规则时只需要修改Service层代码即可
  • 如果有多个不同接口都需要做"禁止客户端上传commentId"的通用校验,才适合放在Router的全局/局部过滤器中作为横切逻辑处理
  • 也可以选择把校验逻辑放在Handler层,和其他接口参数校验逻辑放在一起,适合接口层统一做参数合法性校验的架构规范

内容的提问来源于stack exchange,提问作者Anadi Misra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 18:36:04