Spring Reactive WebClient无法获取响应体但Postman可获取的问题
问题
搭建了基于Netty运行的Spring Boot 3.3 WebFlux服务,核心代码及问题如下:
接口定义
@GetMapping("/{id}") public Mono<DataEntity> getData(@PathVariable("id") String extId) { return dataRepository.findByExtId(extId); }
响应式仓库定义
public interface DataRepository extends ReactiveCrudRepository<DataEntity, String> { Mono<DataEntity> findByExtId(String extId); }
响应式MySQL驱动依赖
<dependency> <groupId>io.asyncer</groupId> <artifactId>r2dbc-mysql</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-pool</artifactId> </dependency>
WebClient调用代码
webClient = webClientBuilder.baseUrl("http://localhost:8080").build(); Mono<DataDto> mono = webClient.get() .uri("/api/{id}", id) .retrieve() .bodyToMono(DataDto.class); DataDto dto = mono.block();
异常现象:
- WebClient、curl调用时,日志显示服务器已发送响应体,但客户端接收的响应体始终为null;Postman、wget可正常获取JSON响应
- 调整HTTP头(keep-alive、Accept等)无效果
- 若将仓库调用替换为
Mono.just(data),所有调用方式均可正常获取响应体
分析与解决方案
核心原因
问题出在响应式流的生命周期协同+序列化隐式失败的组合场景:
- R2DBC冷流与Netty连接生命周期不匹配:
R2DBC返回的Mono<DataEntity>是冷流,仅当订阅者(WebFlux的HTTP处理链)订阅时才触发数据库查询。若数据库查询耗时略长,Netty可能因背压策略提前标记连接为可关闭状态,导致WebClient/curl这类对连接生命周期敏感的客户端无法完整接收响应体;而Postman/wget的连接处理逻辑对这种延迟兼容性更好,能等待流完全写入。 - DataEntity序列化静默失败:
直接返回数据库查询得到的DataEntity时,若实体存在未正确映射的字段(如R2DBC实体中未配置的数据库字段、延迟加载字段),或字段类型与JSON序列化器不兼容(如未指定格式的LocalDateTime),序列化过程会静默失败,最终返回空响应体。而Mono.just(data)使用的是预先构造的完整对象,序列化无异常。
针对性解决步骤
统一实体与DTO的序列化逻辑
避免直接返回数据库实体,显式转换为与WebClient接收一致的DataDto,确保字段映射完整:@GetMapping("/{id}") public Mono<DataDto> getData(@PathVariable("id") String extId) { return dataRepository.findByExtId(extId) .map(entity -> { DataDto dto = new DataDto(); dto.setId(entity.getId()); dto.setContent(entity.getContent()); // 手动映射所有需要的字段,避免隐式序列化问题 return dto; }); }同时确保
DataEntity和DataDto的字段都有正确的getter/setter,或使用Lombok的@Data注解自动生成。强制响应流完成后再处理连接
配置R2DBC连接池超时参数,避免查询超时导致流中断:spring.r2dbc.query-timeout=5s spring.r2dbc.pool.max-idle-time=30m也可在接口中添加流完成的回调,确保数据完全写入后再结束响应:
@GetMapping("/{id}") public Mono<DataDto> getData(@PathVariable("id") String extId) { return dataRepository.findByExtId(extId) .map(this::convertToDto) .doOnTerminate(() -> { // 确保流完成后再释放相关资源 }); }WebClient端优化订阅逻辑
避免直接使用block()(会阻塞线程且无法正确处理背压),改用响应式订阅模式:webClient.get() .uri("/api/{id}", id) .retrieve() .bodyToMono(DataDto.class) .subscribe( dto -> { /* 处理正常响应 */ }, error -> { /* 处理异常 */ } );若需排查,可先打印原始响应体,确认是否是序列化问题:
webClient.get() .uri("/api/{id}", id) .retrieve() .bodyToMono(String.class) .subscribe(rawBody -> { System.out.println("原始响应体:" + rawBody); // 手动反序列化验证 ObjectMapper mapper = new ObjectMapper(); try { DataDto dto = mapper.readValue(rawBody, DataDto.class); } catch (JsonProcessingException e) { e.printStackTrace(); } });
内容的提问来源于stack exchange,提问作者Oleksii Pylypchuk
相关产品推荐
相关产品推荐

