SpringBoot接口通用响应对象设计 - 一个 Resp 管理五种责任

96 阅读9分钟

SpringBoot接口通用响应对象设计 - 一个 Resp 管理五种责任

响应原则: 一个字段只表达一种真相;业务结果、用户提示、内部诊断、明文载荷和密文载荷不能互相借位。

统一响应最容易做成 code + message + data。JSON 外形统一了,真正危险的问题却没有消失:业务是否成功靠哪个字段判断?异常详情会不会泄露?开启响应加密后,明文和密文能否同时出现?数据库 Entity 新增一个字段,会不会顺手变成公网契约?

如果这些问题仍由每个 Controller 临场决定,统一响应就只是统一包装,不是统一语义。

MetaLite 用 Resp<T> 的五个字段拆开五种责任,再让静态工厂、入口切面、响应处理器和 RPC 客户端共同维护状态;返回数据则用 Dto 标识传输边界。本文结合 RespDtoBaseAspect、响应加密处理器和 InternalServiceClient 源码验证这套设计,也直面当前 Resp<T> 尚未强制 DTO 上界的缺口。

标题中的“敢谈首创”是作者对这套完整组合与责任划分的原创设计主张,不代表已经完成全球框架、论文或专利穷尽检索。设计是否成立,最终要看字段关系能否被测试证明。

一、五个字段,不是五个属性,而是五条权力边界

字段唯一责任主要读者明确禁止承载
code应用层结果分类程序调用方HTTP 状态、用户文案
message可安全展示的结果说明用户或调用方SQL、堆栈、内部地址
errorReason未知异常诊断内部开发人员公网稳定契约
data类型化明文业务结果内部链路与普通调用方错误详情、密文
encryptDatadata 的密文形态要求加密的外部调用方第二份独立业务结果

最有冲击力的不是多了 errorReasonencryptData,而是明确规定:

code == 1000              才表示业务成功
errorReason               不得越过公网信任边界
encryptData != null       最终 data 必须为 null
data / encryptData        只能是同一结果的两种形态

一旦字段可以互相代班,调用方就会开始猜协议;一旦每个字段只有一种解释,响应对象才可能长期演进。

二、一张表看懂整个响应状态机

以下是逻辑状态,不承诺序列化器是否输出全部 null 字段:

状态codemessageerrorReasondataencryptData
无数据成功1000oknullnullnull
有数据成功1000ok 或业务说明nullTnull
可预期业务失败1000可安全展示nullnullnull
未知系统异常1999系统内部错误内部原因nullnull
加密成功最终态1000oknullnull密文字符串

这张表比几十个工厂方法更重要。它定义了哪些字段组合合法,也让测试可以直接破坏不变量,而不是只比较一份成功 JSON。

三、Dto 是响应数据的边界标识,但当前还不是硬门禁

MetaLite 把传输对象单独标记:

public interface Pojo extends Serializable {}

public interface Dto extends Pojo {}

LoginDto implements Dto,只携带登录结果需要的用户、Token、组织和资源信息;它不是数据库表的镜像。这种分离的工程价值很直接:

DTO 约束目标解决的问题
只返回调用方真正需要的字段防止 Entity 新字段被自动暴露
不携带持久化注解和数据库语义让接口契约独立于表结构
Dto 形成统一标识便于代码搜索、文档生成和 ArchUnit 检查
继承 Pojo/Serializable保持公共对象的基础传输约定

但源码事实必须讲透:当前 Resp<T> 没有声明 T extends DtoPageResultDto<E> 的元素类型也没有 DTO 上界,现有 Gateway/Admin 接口仍有 Resp<SysUserEntity>Resp<PageResultDto<SysUserEntity>> 等返回。

所以,Dto 目前是明确的设计方向和可治理标识,还不是编译器强制的响应白名单。更稳妥的演进路径不是粗暴把 Resp 改成 Resp<T extends Dto>——Void、字符串和列表也要兼容——而是先对公网 Controller 建立架构测试:禁止直接返回 Entity,复杂业务结果必须落到专用 DTO。

四、Resp 真正统一的是状态流,不只是 JSON 外壳

业务方法返回或抛异常
        ↓
形成成功 / 业务失败 / 系统失败 Resp
        ↓
按调用方配置:data → encryptData
        ↓
SERVER 业务码映射 HTTP 500
        ↓
记录响应日志
        ↓
密文存在时清空明文 data

顺序本身就是契约。先清空 data 会导致无内容可加密;生成密文后不清空 data,会同时向公网暴露明文与密文;先记录、后转换还是先转换、后记录,也决定日志中能看到哪一种状态。

五、code、message 与 errorReason 为什么必须分家

成功码由源码固定为 1000

public static final int CODE_OK = 1000;

public boolean isOk() {
    return CODE_OK == this.code;
}

公共错误目前规划在 1000~19991999 表示未预期服务器错误,业务模块可继续规划 2000+。这个区间设计便于分类,但当前 ErrorCode 仍是普通类,没有注册中心或构建插件自动阻止重复编号,区间只能算治理约定,不能宣传成强约束。

messageerrorReason 则服务不同受众:

message     = 系统内部错误          // 可安全展示
errorReason = NullPointerException… // 仅内部诊断

Resp.error(Throwable) 对未知异常设置 errorReason。然而 @Schema(hidden = true) 只会隐藏 OpenAPI 字段,不等于禁止 JSON 序列化;当前扫描也没有发现该字段带 FastJson2/Jackson 忽略注解,或 Gateway 统一清理它的处理器。

因此,“诊断与展示分家”的字段设计是成立的,但公网隔离尚未闭环。必须增加序列化忽略、可信视图或返回前清理,并用未知异常测试证明公网拿不到内部原因。

六、data 与 encryptData 为什么必须同时存在

业务 Service 始终先产生正常 Java 对象:

return Resp.ok(loginDto);

Gateway 的响应处理链再根据调用方配置执行:

data
  → FastJson2 序列化
  → SM4-GCM / SM4-CBC / AES-GCM / AES-CBC
  → encryptData
  → 清空 data

data 服务内部类型检查、日志和序列化,encryptData 服务外部安全传输。两者不能合并成一个 Object payload,也不能被理解成两份业务结果。

data 使用泛型同样重要:

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 状态则回答传输层问题。当前只有 1999ApiRespStatusHandler 映射为 HTTP 500,参数、权限和登录等业务错误通常仍返回 HTTP 200,由调用方继续检查 Resp.code。这是当前取舍,不是 HTTP 的唯一正确答案;需要 400/401/403 时必须另建明确映射。

八、统一外壳之后,RPC 仍要显式恢复泛型

JSON 传输会擦除 Java 泛型。InternalServiceClient 因此拆出不同入口:

callOneInstanceRtnData(..., LoginDto.class)
callOneInstanceRtnListData(..., SelectOptionDto.class)
callOneInstanceRtnPageData(..., UserDto.class)

客户端先解析外层 Resp,检查业务码,再按单对象、列表或分页恢复元素类型;形态不匹配时返回 API_DATA_PARSE_FAIL。统一响应没有假装消除泛型擦除,而是给运行时类型恢复提供了稳定锚点。

九、共享成功实例暴露了不可变性边界

Resp.ok() 返回全局 NO_DATA_OK_INSTANCE,并在 setData 中阻止写入。这减少了无数据成功对象的创建,却留下一个真实缺口:Lombok @Data 仍会为 codemessageerrorReasonencryptData 生成 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