SpringBoot接口通用返回对象Resp设计

SpringBoot接口通用返回对象Resp设计

Spring Boot 项目统一返回一个 Resp 包装对象,只解决了 JSON 外形一致,并没有自动解决 HTTP 状态、业务错误码、错误消息和异常分类。

关键风险: 如果所有失败都返回 HTTP 200,网关、监控和客户端难以区分网络成功与业务失败;如果只依赖 HTTP 状态,业务又缺少稳定可识别的错误语义。企业接口需要同时管理传输协议和业务协议。

MetaLite 用统一响应对象承载业务结果,同时通过入口处理链和异常转换控制外部响应。本文先拆清 HTTP 与业务码的职责,再用 Resp 及异常链说明统一返回真正应该统一什么。

一、业务码和 HTTP 状态分别回答什么

HTTP 状态描述协议层结果,例如参数无效、未认证、无权限或服务器异常;业务码描述应用规则,例如用户不存在、状态不允许或重复操作。

两者不是二选一:

text 复制代码
HTTP 400 + PARAM_INVALID
HTTP 401 + TOKEN_INVALID
HTTP 403 + PERMISSION_DENIED
HTTP 500 + SERVER_ERROR

当前 MetaLite 更强调 Resp.code 的应用层语义。实际 HTTP 状态如何映射仍需结合 GlobalExceptionHandler 和 Controller 行为统一验证,不能看到统一 Resp 就假设监控能按 4xx/5xx 正确统计。

ErrorCode 当前约定公共错误码使用 1000~1999,例如成功、参数无效、重复提交和服务器错误。业务模块可以使用其他区间。这个分段便于定位来源,但 ErrorCode 是普通类,并不会自动阻止不同模块声明重复编号;仍需要文档、测试或构建检查治理。

二、message 为什么不能承载内部堆栈

message 通常会展示给调用方,应稳定、可理解且不包含表名、SQL、文件路径和内部地址。

MetaLite 对意外异常生成服务器错误响应,并把 ThrowableUtil.getMessage 放进 errorReason。设计意图是将用户消息与开发诊断分开。

但字段分开不代表天然安全。任何对外序列化链都必须确认 errorReason 被清理或隐藏;仅靠 OpenAPI 的 hidden=true 不会自动阻止 JSON 输出。

MetaLite 当前还有两条错误出口:进入 ApiReceiveAspect 的应用接口会把异常转换为 Resp;没有进入该切面的 MVC 框架异常由 GlobalExceptionHandler 处理,可能保留 HTTP 状态并返回字符串正文。客户端、网关和监控不能假设所有错误天然都是同一种 JSON。

三、为什么成功无数据响应使用共享实例

Resp.ok() 返回全局 NO_DATA_OK_INSTANCE,避免大量无数据成功响应重复创建对象。

源码同时禁止对这个共享实例调用 setData

java 复制代码
if (NO_DATA_OK_INSTANCE == this) {
    throw new IllegalStateException(...);
}

否则一次请求修改共享对象,会污染其他请求。

这是一个小而典型的基础设施细节:共享不可变对象可以节省分配,但必须真正不可变。当前类仍有其他 Lombok setter,团队应避免对共享实例修改 code、message 等字段;更严格的做法是使用不可变类型。

四、为什么同时存在 data 与 encryptData

业务代码先按普通 Java 对象返回 data,网关响应加密处理器再把它序列化并写入 encryptDataApiClearRespDataHandler 在密文存在时清空明文 data:

java 复制代码
if (resp.getEncryptData() != null) {
    resp.setData(null);
}

这样 Service 不需要知道调用方是否启用响应加密,协议安全由网关处理器链完成。

处理器顺序非常关键:必须先生成密文,再清空明文;日志是在加密前还是加密后输出,也决定敏感数据是否进入日志。

五、Resp.error 与抛异常如何选择

可预期业务拒绝可以直接返回:

java 复制代码
return Resp.error("用户不存在");

也可以抛带业务码的 ServiceException,由统一入口转换。

差异在于事务与调用链:返回失败对象仍是正常 Java 返回,声明式事务通常不会自动回滚;抛异常更容易触发回滚,却需要调用层明确哪些异常保留、哪些转换。

同一类业务规则应统一约定,否则调用方无法仅凭代码风格判断事务结果。

日志也需要分级:可预期的 4xx 或业务拒绝不应制造大量系统故障告警,5xx 和意外异常则应保留堆栈,并与 TraceId、URI、调用方和业务 code 一起检索。

六、泛型响应为什么在 RPC 中并不简单

Resp<PageResultDto<SysUserEntity>> 经过 JSON 和 HTTP 后存在泛型擦除。MetaLite 的 InternalServiceClient 分别提供单对象、列表和分页结果调用方法,并要求传入元素 Class,用明确入口恢复类型。

这比直接把所有结果反序列化成 Map 更安全,但复杂嵌套泛型仍需要专门类型信息。统一响应不能消除 Java 泛型在运行时的限制。

七、返回 Entity 会破坏统一响应的安全边界

即使外层统一为 Resp,内部 data 若直接放 Entity,新增数据库字段仍可能自动暴露。

因此:

text 复制代码
Resp<Entity> 不是天然安全
Resp<DTO> 才能建立输出字段白名单

MetaLite 当前部分管理接口仍返回 Entity,这一边界已在 094 中公开,后续应逐步收敛。

八、统一响应至少要通过哪些测试

  1. 无数据成功实例不会被修改;
  2. 参数、认证、权限、业务与服务器错误使用稳定代码;
  3. 意外异常不向外返回 errorReason、堆栈或 SQL;
  4. 响应加密后 data 被清空,失败响应策略一致;
  5. 列表、分页和嵌套泛型能正确恢复类型;
  6. HTTP 状态与网关、监控、客户端重试策略一致;
  7. DTO 不包含数据库内部字段。

统一响应的目标不是让所有 HTTP 都长得一样,而是让调用方能稳定判断成功、业务拒绝和系统失败,让内部诊断信息停留在可信边界内。

九、让三类错误分别走一遍完整链路

统一响应最容易在异常场景失真。建议用同一接口构造三次请求:参数校验失败、Service 主动抛出业务异常、DAO 或 RPC 抛出未预期异常。

检查点不是"最终都有 code",而是:参数错误是否在目标方法前终止;业务异常是否保留稳定业务码;未知异常是否只向外返回安全信息,同时在内部日志中保留 TraceId 和堆栈。HTTP 状态、Resp.codemessage 和内部 errorReason 必须分别承担职责。

如果三种失败最后都变成 HTTP 200 加一句任意 message,调用方无法区分重试、修正参数还是停止操作;如果内部堆栈直接进入 message,统一响应反而扩大了泄露面。


框架简介

MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。

源码基线

JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目 backend-bom 为准。

作者简介

15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。

持续更新

MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。

在线演示

演示地址: https://admin.metalite.top/

演示账号: guess

演示密码: admin@2026

相关推荐
姚杨11 分钟前
聊了三年 DDD,代码里全是贫血模型:老陈一句话点破,落地先过这几关
后端·orm
liangbo711 分钟前
JVM规范第 2章:从 class 文件到运行时数据区
java·jvm
DotNet10017 分钟前
别再只会 new List() 了!C#15 的 with(capacity:) 到底香在哪?
后端
思考着亮21 分钟前
1.路由与请求参数校验
后端
吴声子夜歌21 分钟前
Java面试——Flink原理及应用(二)
java·面试·flink
半个落月22 分钟前
NestJS 入门实战:从工厂模式到 Todo CRUD,讲透模块化、依赖注入与测试
后端·nestjs
思考着亮23 分钟前
11.MVCC、行锁与事务隔离级别
后端
一开24 分钟前
一个自己开发的 Agent Harness-持久化与恢复篇
后端
一开25 分钟前
一个自己开发的 Agent Harness-模型降级篇
后端