当前位置: 首页 > news >正文

文海问津项目日志(四)

本次主要实现了网关的错误归一化与统一 JSON Envelope

功能目标

  • 所有失败请求都返回一致的 JSON 结构,便于前端统一处理
  • 错误 body 必含requestId,便于定位链路
  • 网关级错误(鉴权/限流/未知异常)不依赖下游服务

关键代码原文 + 解读

1 统一 JSON 写出

代码位置:[JsonResponseWriter.java](file:///f:/Gitee/PaperFlow/PaperFlow/backend/services/api-gateway/src/main/java/com/paperflow/gateway/http/JsonResponseWriter.java)

package com.paperflow.gateway.http; import com.fasterxml.jackson.databind.ObjectMapper; import com.paperflow.gateway.filter.RequestIdGlobalFilter; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public final class JsonResponseWriter { private final ObjectMapper objectMapper; public JsonResponseWriter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } public Mono<Void> writeError(ServerWebExchange exchange, HttpStatus status, String code, String message, Map<String, Object> details) { Map<String, Object> error = new LinkedHashMap<>(); error.put("code", code); error.put("message", message); if (details != null && !details.isEmpty()) { error.put("details", details); } Map<String, Object> root = new LinkedHashMap<>(); root.put("requestId", requestId(exchange)); root.put("error", error); return write(exchange, status, root); } public Mono<Void> write(ServerWebExchange exchange, HttpStatus status, Object body) { exchange.getResponse().setStatusCode(status); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); byte[] bytes; try { bytes = objectMapper.writeValueAsBytes(body); } catch (Exception e) { bytes = ("{\"requestId\":\"" + requestId(exchange) + "\",\"error\":{\"code\":\"SYS_INTERNAL_ERROR\",\"message\":\"serialization_failed\"}}") .getBytes(StandardCharsets.UTF_8); } DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return exchange.getResponse().writeWith(Mono.just(buffer)); } private String requestId(ServerWebExchange exchange) { Object v = exchange.getAttributes().get(RequestIdGlobalFilter.ATTR); if (v == null) { return ""; } return String.valueOf(v); } }

逐段解释:

  • writeError(...)
    • 组装统一结构:{ requestId, error: { code, message, details? } }
    • 这里用LinkedHashMap是为了输出字段顺序稳定(便于阅读/调试)
  • write(...)
    • 设置 HTTP status +application/json
    • 使用 Jackson 序列化(Spring Boot 默认提供ObjectMapperBean)
    • 若序列化失败(理论上很少发生),返回一个最小可读错误 JSON,避免空响应
  • requestId(exchange)
    • RequestIdGlobalFilter写入的 exchange 属性里取 requestId
    • 这就是为什么 RequestId 过滤器要尽量早执行

2 兜底异常处理

代码位置:[GlobalErrorHandler.java](file:///f:/Gitee/PaperFlow/PaperFlow/backend/services/api-gateway/src/main/java/com/paperflow/gateway/error/GlobalErrorHandler.java)

package com.paperflow.gateway.error; import com.paperflow.gateway.http.JsonResponseWriter; import java.util.Map; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component @Order(Ordered.HIGHEST_PRECEDENCE) public final class GlobalErrorHandler implements ErrorWebExceptionHandler { private final JsonResponseWriter writer; public GlobalErrorHandler(JsonResponseWriter writer) { this.writer = writer; } @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { if (exchange.getResponse().isCommitted()) { return Mono.error(ex); } return writer.writeError(exchange, HttpStatus.INTERNAL_SERVER_ERROR, "SYS_INTERNAL_ERROR", "Internal error", Map.of()); } }

逐段解释:

  • ErrorWebExceptionHandler:WebFlux(Gateway 基于 WebFlux)异常兜底处理。
  • @Order(HIGHEST_PRECEDENCE):尽量优先处理异常,避免被默认 handler 覆盖。
  • exchange.getResponse().isCommitted():如果响应已开始写出,不能再改 body,只能把异常继续抛给框架。
  • 兜底错误码固定为SYS_INTERNAL_ERROR,避免把内部异常细节暴露给外部(安全与稳定性)。

前端如何消费这套错误格式

  • SPA 根据error.code做分支:
    • AUTH_*:触发登录/刷新 token
    • RATE_LIMITED:提示稍后再试
    • REQ_VALIDATION_FAILED:表单高亮
  • 在报错弹窗/日志里展示requestId,用于和服务端日志对齐排障
http://www.jsqmd.com/news/761101/

相关文章:

  • 工业芯片SSD202D在复古游戏机中的逆向创新应用
  • Taotoken模型广场在项目技术选型中的实际使用感受
  • K2.6快速 LeetCode 2106.摘水果 public int maxTotalFruits(int[][] fruits, int startPos, int k)
  • 2026住人集装箱应用白皮书交通基建场景剖析:集装箱租赁、集装箱活动房、租赁用集装箱、集装箱房屋、住人集装箱、集装箱定制选择指南 - 优质品牌商家
  • 保姆级教程:在Ubuntu 22.04上搞定Playwright Python环境(含依赖安装避坑指南)
  • Arduino UNO SPE Shield:工业物联网通信解决方案
  • 前端光标平滑算法实战:Catmull-Rom插值与perfect-cursor应用
  • JFrog Artifactory与CI/CD深度集成:fastci工具实战与制品管理优化
  • 3步永久备份微信聊天记录:免费开源工具WeChatExporter终极指南
  • 深入解析Refine框架:基于React的企业级应用开发实践
  • 2026年Q2可移动垃圾房权威供应梯队:可移动垃圾房/吸烟亭/环卫休息室/移动厕所/移动垃圾分类房/保安岗亭/移动卫生间/选择指南 - 优质品牌商家
  • STM32H743飞控DIY避坑:ICM42688P的SPI引脚映射与DMA配置实战(附完整代码)
  • 轻量级规则引擎dev-rules:动态业务逻辑与配置化实践
  • 智能多平台文件解析引擎:基于模块化架构的高性能网盘直链获取解决方案
  • 豆包付费订阅背后,藏着一个反直觉的真相:给你顶配AI,你用得动吗?
  • 魔兽争霸III地图制作革命:为什么HiveWE是每个地图创作者必备的终极编辑器
  • 用MATLAB处理GLDAS Noah数据:从NASA官网下载到绘制全球土壤水分分布图
  • 从30mV到3mV:手把手教你评估和提升NTC测温精度(以MM32F0130的ADC为例)
  • 为Claude Code配置Taotoken聚合端点实现稳定智能编程辅助
  • 从单片机到Linux内核:一文搞懂原子操作atomic_t的前世今生与实战
  • 阴阳师自动化脚本终极指南:3分钟快速部署,彻底解放双手
  • 从静态地图到4D动态轨迹图,R 4.5新geoviews 0.14接口全拆解,6步实现城市出租车流实时热力回溯
  • 2026耐低温密封圈选型:耐高压密封圈/耐高温密封圈/聚四氟乙烯密封圈/铁氟龙密封圈/防尘密封圈/高分子材料密封圈/选择指南 - 优质品牌商家
  • MAGNet多模态智能体导航:跨模态注意力与连续动作控制
  • AI赋能Git提交:aicommit2工具原理、配置与实战指南
  • 儿童疫苗接种溯源程序,批次,厂家,接种时间上链,杜绝问题疫苗。
  • 对比直接使用官方api体验taotoken在容灾与路由上的差异
  • 深入paho.mqtt.c源码:自动重连机制是如何在C语言层面实现的?
  • 从ResolvePackageNotFound到Found conflicts:一文读懂Conda环境迁移的底层依赖冲突原理与排查思路
  • 告别玄学调试:用示波器实测PCIe 3.0/4.0参考时钟(REFCLK)的12个关键参数