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

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

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

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

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

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

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

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

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

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

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

messageerrorReason 则服务不同受众:

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 状态则回答传输层问题。当前只有 1999ApiRespStatusHandler 映射为 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 仍会为 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

相关推荐
对象存储与RustFS1 小时前
把 RustFS 跑在 Kubernetes 上:官方 Helm Chart 从单机到分布式
后端·rust·开源
SimonKing1 小时前
将cURL 命令直接变成 Java 代码?用JQuick-Curl搞起来
java·后端·程序员
大牧师1 小时前
Nest.js 微服务入门教程
开发语言·javascript·后端·微服务·node.js·nest.js·nest
凤山老林2 小时前
企业级文件服务中台:Spring Boot 集成 MinIO 实现分片存储、生命周期管理与多租户隔离
java·spring boot·后端·minio·文件服务
IT_陈寒2 小时前
Redis的订阅丢失消息?你可能忘了这个配置
前端·人工智能·后端
好好沉淀2 小时前
巧用 Set 去重与复合键:高效统计分组内不重复元素的数量
java·spring boot·后端
FfHUCisI2 小时前
Golang 函数调用的瞬间:栈帧、拷贝栈与逃逸分析
开发语言·后端·golang
Csvn2 小时前
🐍 Day 12: 编码与字符集 — 告别乱码噩梦
后端·python