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,网关响应加密处理器再把它序列化并写入 encryptData。ApiClearRespDataHandler 在密文存在时清空明文 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 中公开,后续应逐步收敛。
八、统一响应至少要通过哪些测试
- 无数据成功实例不会被修改;
- 参数、认证、权限、业务与服务器错误使用稳定代码;
- 意外异常不向外返回 errorReason、堆栈或 SQL;
- 响应加密后 data 被清空,失败响应策略一致;
- 列表、分页和嵌套泛型能正确恢复类型;
- HTTP 状态与网关、监控、客户端重试策略一致;
- DTO 不包含数据库内部字段。
统一响应的目标不是让所有 HTTP 都长得一样,而是让调用方能稳定判断成功、业务拒绝和系统失败,让内部诊断信息停留在可信边界内。
九、让三类错误分别走一遍完整链路
统一响应最容易在异常场景失真。建议用同一接口构造三次请求:参数校验失败、Service 主动抛出业务异常、DAO 或 RPC 抛出未预期异常。
检查点不是"最终都有 code",而是:参数错误是否在目标方法前终止;业务异常是否保留稳定业务码;未知异常是否只向外返回安全信息,同时在内部日志中保留 TraceId 和堆栈。HTTP 状态、Resp.code、message 和内部 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