SpringBoot接口通用响应对象设计 - 一个 Resp 管理五种责任
响应原则: 一个字段只表达一种真相;业务结果、用户提示、内部诊断、明文载荷和密文载荷不能互相借位。
统一响应最容易做成 code + message + data。JSON 外形统一了,真正危险的问题却没有消失:业务是否成功靠哪个字段判断?异常详情会不会泄露?开启响应加密后,明文和密文能否同时出现?数据库 Entity 新增一个字段,会不会顺手变成公网契约?
如果这些问题仍由每个 Controller 临场决定,统一响应就只是统一包装,不是统一语义。
MetaLite 用 Resp<T> 的五个字段拆开五种责任,再让静态工厂、入口切面、响应处理器和 RPC 客户端共同维护状态;返回数据则用 Dto 标识传输边界。本文结合 Resp、Dto、BaseAspect、响应加密处理器和 InternalServiceClient 源码验证这套设计,也直面当前 Resp<T> 尚未强制 DTO 上界的缺口。
标题中的"敢谈首创"是作者对这套完整组合与责任划分的原创设计主张,不代表已经完成全球框架、论文或专利穷尽检索。设计是否成立,最终要看字段关系能否被测试证明。
一、五个字段,不是五个属性,而是五条权力边界
| 字段 | 唯一责任 | 主要读者 | 明确禁止承载 |
|---|---|---|---|
code |
应用层结果分类 | 程序调用方 | HTTP 状态、用户文案 |
message |
可安全展示的结果说明 | 用户或调用方 | SQL、堆栈、内部地址 |
errorReason |
未知异常诊断 | 内部开发人员 | 公网稳定契约 |
data |
类型化明文业务结果 | 内部链路与普通调用方 | 错误详情、密文 |
encryptData |
data 的密文形态 |
要求加密的外部调用方 | 第二份独立业务结果 |
最有冲击力的不是多了 errorReason 和 encryptData,而是明确规定:
text
code == 1000 才表示业务成功
errorReason 不得越过公网信任边界
encryptData != null 最终 data 必须为 null
data / encryptData 只能是同一结果的两种形态
一旦字段可以互相代班,调用方就会开始猜协议;一旦每个字段只有一种解释,响应对象才可能长期演进。
二、一张表看懂整个响应状态机
以下是逻辑状态,不承诺序列化器是否输出全部 null 字段:
| 状态 | code | message | errorReason | data | encryptData |
|---|---|---|---|---|---|
| 无数据成功 | 1000 |
ok |
null | null | null |
| 有数据成功 | 1000 |
ok 或业务说明 |
null | T |
null |
| 可预期业务失败 | 非 1000 |
可安全展示 | null | null | null |
| 未知系统异常 | 1999 |
系统内部错误 | 内部原因 | null | null |
| 加密成功最终态 | 1000 |
ok |
null | null | 密文字符串 |
这张表比几十个工厂方法更重要。它定义了哪些字段组合合法,也让测试可以直接破坏不变量,而不是只比较一份成功 JSON。
三、Dto 是响应数据的边界标识,但当前还不是硬门禁
MetaLite 把传输对象单独标记:
java
public interface Pojo extends Serializable {}
public interface Dto extends Pojo {}
LoginDto implements Dto,只携带登录结果需要的用户、Token、组织和资源信息;它不是数据库表的镜像。这种分离的工程价值很直接:
| DTO 约束目标 | 解决的问题 |
|---|---|
| 只返回调用方真正需要的字段 | 防止 Entity 新字段被自动暴露 |
| 不携带持久化注解和数据库语义 | 让接口契约独立于表结构 |
以 Dto 形成统一标识 |
便于代码搜索、文档生成和 ArchUnit 检查 |
继承 Pojo/Serializable |
保持公共对象的基础传输约定 |
但源码事实必须讲透:当前 Resp<T> 没有声明 T extends Dto,PageResultDto<E> 的元素类型也没有 DTO 上界,现有 Gateway/Admin 接口仍有 Resp<SysUserEntity>、Resp<PageResultDto<SysUserEntity>> 等返回。
所以,Dto 目前是明确的设计方向和可治理标识 ,还不是编译器强制的响应白名单。更稳妥的演进路径不是粗暴把 Resp 改成 Resp<T extends Dto>------Void、字符串和列表也要兼容------而是先对公网 Controller 建立架构测试:禁止直接返回 Entity,复杂业务结果必须落到专用 DTO。
四、Resp 真正统一的是状态流,不只是 JSON 外壳
text
业务方法返回或抛异常
↓
形成成功 / 业务失败 / 系统失败 Resp
↓
按调用方配置:data → encryptData
↓
SERVER 业务码映射 HTTP 500
↓
记录响应日志
↓
密文存在时清空明文 data
顺序本身就是契约。先清空 data 会导致无内容可加密;生成密文后不清空 data,会同时向公网暴露明文与密文;先记录、后转换还是先转换、后记录,也决定日志中能看到哪一种状态。
五、code、message 与 errorReason 为什么必须分家
成功码由源码固定为 1000:
java
public static final int CODE_OK = 1000;
public boolean isOk() {
return CODE_OK == this.code;
}
公共错误目前规划在 1000~1999,1999 表示未预期服务器错误,业务模块可继续规划 2000+。这个区间设计便于分类,但当前 ErrorCode 仍是普通类,没有注册中心或构建插件自动阻止重复编号,区间只能算治理约定,不能宣传成强约束。
message 与 errorReason 则服务不同受众:
text
message = 系统内部错误 // 可安全展示
errorReason = NullPointerException... // 仅内部诊断
Resp.error(Throwable) 对未知异常设置 errorReason。然而 @Schema(hidden = true) 只会隐藏 OpenAPI 字段,不等于禁止 JSON 序列化;当前扫描也没有发现该字段带 FastJson2/Jackson 忽略注解,或 Gateway 统一清理它的处理器。
因此,"诊断与展示分家"的字段设计是成立的,但公网隔离尚未闭环。必须增加序列化忽略、可信视图或返回前清理,并用未知异常测试证明公网拿不到内部原因。
六、data 与 encryptData 为什么必须同时存在
业务 Service 始终先产生正常 Java 对象:
java
return Resp.ok(loginDto);
Gateway 的响应处理链再根据调用方配置执行:
text
data
→ FastJson2 序列化
→ SM4-GCM / SM4-CBC / AES-GCM / AES-CBC
→ encryptData
→ 清空 data
data 服务内部类型检查、日志和序列化,encryptData 服务外部安全传输。两者不能合并成一个 Object payload,也不能被理解成两份业务结果。
data 使用泛型同样重要:
java
Resp<LoginDto> // 单对象 DTO
Resp<List<SelectOptionDto>> // DTO 列表
Resp<PageResultDto<UserDto>> // 分页 DTO
Resp<Void> // 无业务数据
方法签名由此表达成功结果的形态;DTO 再表达允许跨边界的数据内容。泛型解决"返回什么",DTO 解决"允许返回哪些字段",两者缺一不可。
七、异常出口和 HTTP 状态为什么不能混为一谈
入口切面 BaseAspect 捕获异常后调用 Resp.error(ex):
| 异常 | 响应语义 |
|---|---|
IllegalArgumentException |
参数无效 |
IllegalStateException |
操作失败 |
ServiceException |
保留显式业务 code/message |
| 其他异常 | 1999 + 安全 message + errorReason |
非入口调用切面仍继续抛异常,避免破坏事务回滚和内部传播语义。入口负责协议转换,调用层负责保留异常。
HTTP 状态则回答传输层问题。当前只有 1999 被 ApiRespStatusHandler 映射为 HTTP 500,参数、权限和登录等业务错误通常仍返回 HTTP 200,由调用方继续检查 Resp.code。这是当前取舍,不是 HTTP 的唯一正确答案;需要 400/401/403 时必须另建明确映射。
八、统一外壳之后,RPC 仍要显式恢复泛型
JSON 传输会擦除 Java 泛型。InternalServiceClient 因此拆出不同入口:
java
callOneInstanceRtnData(..., LoginDto.class)
callOneInstanceRtnListData(..., SelectOptionDto.class)
callOneInstanceRtnPageData(..., UserDto.class)
客户端先解析外层 Resp,检查业务码,再按单对象、列表或分页恢复元素类型;形态不匹配时返回 API_DATA_PARSE_FAIL。统一响应没有假装消除泛型擦除,而是给运行时类型恢复提供了稳定锚点。
九、共享成功实例暴露了不可变性边界
Resp.ok() 返回全局 NO_DATA_OK_INSTANCE,并在 setData 中阻止写入。这减少了无数据成功对象的创建,却留下一个真实缺口:Lombok @Data 仍会为 code、message、errorReason 和 encryptData 生成 setter。
共享对象只有真正不可变才安全。后续应选择不可变成功实例、收紧所有 setter,或放弃共享并每次创建新对象;不能只保护 data 就宣称并发安全已经闭环。
十、这套设计必须守住的八条断言
| 破坏场景 | 必须断言 |
|---|---|
Resp.ok() 被尝试修改 |
共享状态不受污染 |
Resp.ok(dto) |
DTO 可正确序列化并恢复 |
| 公网接口直接返回 Entity | 架构测试阻断 |
抛 ServiceException |
保留指定 code/message |
| 抛未知异常 | HTTP 500,公网不含 errorReason |
| 开启响应加密 | encryptData 非空,最终 data 为空 |
| 单对象入口收到数组 | 返回 API_DATA_PARSE_FAIL |
| Entity 新增敏感字段 | 公网 DTO 契约不变化 |
MetaLite 的统一响应哲学可以压缩成一句话:用字段责任稳定业务语义,用 DTO 稳定数据边界,用处理器顺序维持明密文状态,再用破坏性测试证明这些约束没有停留在文档里。
框架简介 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 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。
在线演示 演示地址: admin.metalite.top/ 演示账号: guess 演示密码: admin@2026