打造 Java Spring Boot 标准化 API : 告别请求、响应重复造轮子!ResultVO + RequestVO
告别重复造轮子!ResultVO + RequestVO 打造 Spring Boot 标准化 API 设计体系
前言:每个 Java 项目都在重复的那件事
打开任何一个 Spring Boot 项目的 common 包,你大概率会看到这样的代码:
public class Result<T> {
private Integer code;
private String message;
private T data;
// 省略 getter/setter...
public static <T> Result<T> success(T data) { ... }
public static <T> Result<T> error(String msg) { ... }
}
每个团队都在写,字段名却各不相同:有的叫 code,有的叫 retCode;有的分页数据嵌套在 data 里,有的平铺在顶层。前后端联调时,接口文档写的是 data.list,前端拿到的却是 data.records——这类低效沟通,几乎每天都在发生。
更麻烦的是请求参数的解析:Controller 里 @RequestParam、@RequestBody、@PathVariable 各自为政,遇到 XML、FormData、文件上传,又得重新写一套解析逻辑。
ResultVO + RequestVO 就是为了解决这两个问题而生的。它们一起构成了一个完整的 API 设计规范:
ResultVO:统一响应结构,支持状态码自适应(Integer/String)、字段名可配置、分页平铺RequestVO:统一请求封装,支持 JSON/Form/FormData/XML/二进制流全类型,提供嵌套路径取值、对象自动转换
本文将以实际项目场景为线索,带你彻底搞懂这套 API 设计体系。
一、ResultVO:统一响应,一套规范走天下
1.1 核心设计
ResultVO 的响应结构只有 4 个字段,简洁但够用:
{
"code": 200,
"msg": "成功",
"data": { ... },
"pager": { "page": 1, "pageSize": 10, "total": 100, "totalPages": 10 }
}
| 字段 | 类型 | 说明 |
|---|---|---|
code |
Object |
状态码,支持 Integer(200)或 String(“SYS_OK”) |
msg |
String |
响应消息 |
data |
T |
业务数据,为空时不参与 JSON 序列化 |
pager |
Pager |
分页信息,与 data 平级,为空时不参与序列化 |
两个关键设计点:
code类型自适应:互联网项目用200(Integer),企业级/银行项目用"SYS_ORDER_404"(String),一个类全兼容。data和pager为空时不参与序列化(@JsonInclude(NON_EMPTY)):响应干净,不会给前端一堆"data": null。
1.2 快速上手:suc / fail 全系列
ResultVO 提供丰富的静态方法,覆盖几乎所有响应场景:
// ========== 成功响应 ==========
// 无数据成功(默认 code=200, msg="成功")
ResultVO<Void> res1 = ResultVO.suc();
// 仅自定义消息(无数据)
ResultVO<Void> res2 = ResultVO.sucMsg("操作成功");
// 携带数据
ResultVO<User> res3 = ResultVO.suc(user);
// 携带数据 + 分页
ResultVO<List<User>> res4 = ResultVO.suc(userList, pager);
// 自定义状态码 + 消息
ResultVO<User> res5 = ResultVO.suc(201, "创建成功", user);
// ========== 失败响应 ==========
// 默认失败(code=500, msg="失败")
ResultVO<Void> err1 = ResultVO.fail();
// 仅自定义消息
ResultVO<Void> err2 = ResultVO.fail("用户名不能为空");
// 自定义状态码 + 消息
ResultVO<Void> err3 = ResultVO.fail(400, "参数错误");
// 携带错误数据(如校验失败字段)
ResultVO<Map<String, Object>> err4 = ResultVO.failData(errorMap);
方法命名约定(看名字就知道用途):
| 前缀 | 含义 | 示例 |
|---|---|---|
suc() |
成功,无数据 | ResultVO.suc() |
suc(T) |
成功,携带数据 | ResultVO.suc(user) |
sucMsg() |
成功,仅消息 | ResultVO.sucMsg("操作成功") |
fail() |
失败,无数据 | ResultVO.fail() |
failData() |
失败,携带数据 | ResultVO.failData(map) |
1.3 业务状态码管理:ResultMessage 接口
当项目变复杂后,状态码不能散落在业务代码里。ResultMessage<C> 接口让你统一管理所有业务状态码:
// 定义业务错误枚举,实现 ResultMessage 接口(String 类型的状态码)
public enum BizError implements ResultMessage<String> {
SUCCESS("200", "成功"),
ORDER_NOT_FOUND("SYS_ORDER_404", "订单不存在"),
STOCK_INSUFFICIENT("SYS_STOCK_500", "库存不足"),
USER_DISABLED("SYS_USER_403", "用户已禁用");
private final String code;
private final String message;
BizError(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String getCode() { return code; }
@Override
public String getMessage() { return message; }
}
使用时,直接把枚举传给 ResultVO,无需每次手写状态码:
// 成功
return ResultVO.suc(BizError.SUCCESS);
// 失败
return ResultVO.fail(BizError.ORDER_NOT_FOUND);
// 失败 + 携带数据
return ResultVO.failData(BizError.STOCK_INSUFFICIENT, orderInfo);
内置默认状态码(DefaultResultMessage)已覆盖常用 HTTP 状态:
| 枚举值 | code | message |
|---|---|---|
SUC |
200 | 成功 |
BAD_REQUEST |
400 | 请求参数错误 |
UNAUTHORIZED |
401 | 未授权 |
FORBIDDEN |
403 | 禁止访问 |
NOT_FOUND |
404 | 资源不存在 |
ERROR |
500 | 服务器内部错误 |
FAIL |
500 | 失败 |
1.4 字段名自定义:适配不同团队的规范
有些团队要求返回 retCode 而不是 code,有些要求 body 而不是 data。不需要改源码,只需实现 ResultFieldMapping 接口并注入 Spring:
@Component
public class BankFieldMapping implements ResultFieldMapping {
@Override public String getCodeFieldName() { return "retCode"; }
@Override public String getMsgFieldName() { return "retMsg"; }
@Override public String getDataFieldName() { return "body"; }
@Override public String getPagerFieldName() { return "pageInfo"; }
}
配置生效后,原本返回:
{"code": 200, "msg": "成功", "data": {...}}
自动变为:
{"retCode": 200, "retMsg": "成功", "body": {...}}
全局生效,无需修改任何 Controller 代码。 这对多项目/多团队协作非常实用。
1.5 Pager 分页:与 Vue 生态完美对齐
Pager 对象与 data 平级返回,前端拿到的结构一目了然:
{
"code": 200,
"msg": "成功",
"data": [...],
"pager": {
"startRow": 0,
"endRow": 10,
"page": 1,
"pageSize": 10,
"total": 100,
"totalPages": 10
}
}
Pager 提供以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
startRow |
int | 起始行号(用于 SQL LIMIT) |
endRow |
int | 结束行号 |
page |
int | 当前页码(默认 1) |
pageSize |
int | 每页条数(默认 10) |
total |
int | 数据总条数 |
totalPages |
int | 总页数(自动计算) |
setTotal() 会自动计算总页数:
Pager pager = new Pager();
pager.setPage(1);
pager.setPageSize(20);
pager.setTotal(95); // 自动计算 totalPages = 5
二、RequestVO:一个对象搞定所有请求类型
2.1 核心设计
RequestVO 配合 @RequestVo 注解,将 HTTP 请求数据自动注入到 Controller 方法参数中。它的设计哲学是:不管前端用什么 Content-Type,后端用同一套 API 取参数。
支持的请求类型:
| Content-Type | 场景 | 取值方式 |
|---|---|---|
application/json |
JSON 请求体 | getStr(key) / toBean() |
application/x-www-form-urlencoded |
Form 表单 | getStr(key) |
multipart/form-data |
文件上传 + 表单 | getWebFile() / getStr(key) |
application/xml |
XML 请求体 | getXml() / getXml(Class) |
text/plain |
纯文本 | getText() |
application/octet-stream |
二进制流 | getBinaryStream() |
| URL 路径参数 | @PathVariable |
自动合并到 params |
| URL Query 参数 | ?key=value |
getStr(key) |
2.2 快速上手
@RestController
public class UserController {
@PostMapping("/user")
public ResultVO<?> createUser(@RequestVo RequestVO vo) {
// 单值参数(类型自动转换)
String name = vo.getStr("name");
Integer age = vo.getInt("age");
Long uid = vo.getLong("uid");
Boolean vip = vo.getBoolean("vip");
// 数组参数(逗号分隔自动拆分)
String[] tags = vo.getStrArray("tags");
Long[] ids = vo.getLongArray("ids");
// 整体转换为 JavaBean
User user = vo.toBean(User.class);
// 获取分页
Pager pager = vo.getPager();
return ResultVO.suc(user);
}
}
2.3 请求头:三种方式
// 1. 获取请求头对象
RequestHeader header = vo.getHeader();
String auth = header.get("Authorization");
// 2. 直接获取指定请求头(更简洁)
String auth = vo.getHeader("Authorization");
// 3. 获取所有请求头
Map<String, String> allHeaders = vo.getHeaders();
2.4 类型安全取值:get 方法家族
RequestVO 提供完整的类型安全取值方法,自动处理类型转换:
// String 取值(Number 自动 toString)
String name = vo.getStr("name");
// Integer 取值(String 自动解析,无效抛 ParseException)
Integer age = vo.getInt("age");
// Long 取值
Long uid = vo.getLong("uid");
// Boolean 取值(支持 true/false、1/0)
Boolean vip = vo.getBoolean("vip");
// 指定类型取值(Map/List 自动通过 JSON 库转换)
UserDTO dto = vo.get("user", UserDTO.class);
// 数组取值(逗号分隔自动拆分)
String[] tags = vo.getStrArray("tags"); // "java,spring" → ["java", "spring"]
Integer[] ids = vo.getIntArray("ids"); // "1,2,3" → [1, 2, 3]
Long[] uids = vo.getLongArray("uids"); // "100,200" → [100L, 200L]
Boolean 转换规则:
Boolean类型:直接返回String类型:"true"→true,"false"→falseNumber类型:非 0 →true,0 →false
2.5 嵌套路径取值:点号语法
当请求参数包含嵌套对象时,无需手动层层取值,直接用点号路径:
// 请求体 JSON:
// {
// "user": {
// "address": {
// "city": "北京",
// "street": "长安街"
// }
// }
// }
// 直接取值
String city = vo.getPathNode("user.address.city", String.class); // "北京"
String street = vo.getPathNode("user.address.street", String.class); // "长安街"
规则:
- 路径中每层必须是 Map 类型(JSON 对象)
- 不支持数组索引(如
user.cityList[0].city) - 路径不存在时返回
null,不会抛异常
2.6 对象转换:toBean / toListBean
// 将全部请求参数转换为 JavaBean(JSON 反序列化)
User user = vo.toBean(User.class);
// 将 JSON 数组请求体转换为 List(仅 JSON 请求有效)
List<User> users = vo.toListBean(User.class);
2.7 文件上传与二进制流
// 获取 FormData 中的文件
WebFile webFile = vo.getWebFile();
if (webFile != null) {
MultipartFile avatar = webFile.getFile("avatar");
// 处理文件...
}
// 获取原始二进制流(application/octet-stream)
InputStream binaryStream = vo.getBinaryStream();
// 注意:二进制流需要调用方自行关闭
2.8 XML 处理
// 获取原始 XML 字符串
String xml = vo.getXml();
// XML 自动转 Bean(默认根节点为 <data>)
// 请求体:<data><name>张三</name><age>25</age></data>
User user = vo.getXml(User.class);
// 自定义根节点别名
// 请求体:<root><name>张三</name></root>
User user2 = vo.getXml(User.class, "root");
2.9 自定义分页参数名
@RequestVo 注解支持自定义分页参数名,适配不同前端团队的命名习惯:
// 前端传 pageNum / size
@PostMapping("/list")
public ResultVO<?> list(
@RequestVo(page = "pageNum", pageSize = "size", defaultPageSize = 20) RequestVO vo
) {
Pager pager = vo.getPager(); // 自动用 pageNum / size 解析
List<User> users = userService.list(pager);
return ResultVO.suc(users, pager);
}
三、实战:完整 Controller 示例
以一个用户管理模块为例,展示 ResultVO + RequestVO 的完整配合使用:
@RestController
@RequestMapping("/api/users")
public class UserController {
/**
* 查询用户详情
* GET /api/users/123
*/
@GetMapping("/{id}")
public ResultVO<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return ResultVO.fail(404, "用户不存在");
}
return ResultVO.suc(user);
}
/**
* 分页查询用户列表
* GET /api/users?page=1&pageSize=20
*/
@GetMapping
public ResultVO<List<User>> listUsers(@RequestVo RequestVO requestVO) {
Pager pager = requestVO.getPager();
List<User> users = userService.list(pager);
return ResultVO.suc(users, pager);
}
/**
* 创建用户(JSON 请求)
* POST /api/users
* Body: {"name": "张三", "age": 25, "address": {"city": "北京"}}
*/
@PostMapping
public ResultVO<User> createUser(@RequestVo RequestVO vo) {
// 取嵌套字段
String city = vo.getPathNode("address.city", String.class);
// 整体转 Bean
User user = vo.toBean(User.class);
User created = userService.create(user);
return ResultVO.suc(201, "创建成功", created);
}
/**
* 更新用户(Form 表单)
* PUT /api/users/123
* Content-Type: application/x-www-form-urlencoded
* Body: name=李四&age=30
*/
@PutMapping("/{id}")
public ResultVO<User> updateUser(@PathVariable Long id, @RequestVo RequestVO vo) {
String name = vo.getStr("name");
Integer age = vo.getInt("age");
User updated = userService.update(id, name, age);
return ResultVO.suc(updated);
}
/**
* 上传头像(FormData)
* POST /api/users/123/avatar
* Content-Type: multipart/form-data
*/
@PostMapping("/{id}/avatar")
public ResultVO<String> uploadAvatar(@PathVariable Long id, @RequestVo RequestVO vo) {
WebFile webFile = vo.getWebFile();
if (webFile == null) {
return ResultVO.fail(400, "请选择文件");
}
MultipartFile file = webFile.getFile("avatar");
String url = fileService.upload(file);
return ResultVO.suc(url);
}
/**
* 批量删除
* DELETE /api/users?ids=1,2,3
*/
@DeleteMapping
public ResultVO<Void> deleteUsers(@RequestVo RequestVO vo) {
Long[] ids = vo.getLongArray("ids");
if (ids == null || ids.length == 0) {
return ResultVO.fail(400, "请选择要删除的用户");
}
userService.deleteByIds(ids);
return ResultVO.suc();
}
/**
* 接收 XML 请求
* POST /api/users/xml
* Content-Type: application/xml
* Body: <data><name>王五</name><age>28</age></data>
*/
@PostMapping("/xml")
public ResultVO<User> createFromXml(@RequestVo RequestVO vo) {
User user = vo.getXml(User.class);
User created = userService.create(user);
return ResultVO.suc(created);
}
}
四、API 速查表
ResultVO 方法速查
| 场景 | 方法 | 示例 |
|---|---|---|
| 默认成功 | suc() |
ResultVO.suc() |
| 仅成功消息 | sucMsg(String) |
ResultVO.sucMsg("操作成功") |
| 成功携带数据 | suc(T) |
ResultVO.suc(user) |
| 成功携带数据+分页 | suc(T, Pager) |
ResultVO.suc(list, pager) |
| 自定义状态码+消息 | suc(Object, String) |
ResultVO.suc(200, "成功") |
| 自定义状态接口 | suc(ResultMessage) |
ResultVO.suc(BizError.SUCCESS) |
| 默认失败 | fail() |
ResultVO.fail() |
| 仅失败消息 | fail(String) |
ResultVO.fail("错误") |
| 失败携带数据 | failData(T) |
ResultVO.failData(errorMap) |
| 失败自定义消息+数据 | failData(String, T) |
ResultVO.failData("部分失败", data) |
| 失败自定义状态码+消息 | fail(Object, String) |
ResultVO.fail(400, "无效请求") |
| 失败自定义状态接口 | fail(ResultMessage) |
ResultVO.fail(BizError.ORDER_NOT_FOUND) |
RequestVO 方法速查
| 分类 | 方法 | 返回类型 | 说明 |
|---|---|---|---|
| 请求头 | getHeader() |
RequestHeader |
获取请求头对象 |
getHeader(String key) |
String |
获取指定请求头的值 | |
getHeaders() |
Map<String, String> |
获取所有请求头 | |
| 单值参数 | get(String key) |
Object |
获取原始参数值 |
get(String key, Class<T>) |
T |
获取参数并转换为目标类型 | |
getStr(String key) |
String |
获取字符串参数 | |
getInt(String key) |
Integer |
获取整型参数 | |
getLong(String key) |
Long |
获取长整型参数 | |
getBoolean(String key) |
Boolean |
获取布尔参数 | |
| 数组参数 | getStrArray(String key) |
String[] |
获取逗号分隔的字符串数组 |
getIntArray(String key) |
Integer[] |
获取逗号分隔的整型数组 | |
getLongArray(String key) |
Long[] |
获取逗号分隔的长整型数组 | |
| 嵌套路径 | getPathNode(String path, Class<T>) |
T |
点号路径取值 |
| 对象转换 | toBean(Class<T>) |
T |
全部参数转 JavaBean |
toListBean(Class<T>) |
List<T> |
JSON 数组转 List | |
| 请求体原始内容 | getBody() |
String |
原始请求体字符串 |
getText() |
String |
纯文本请求体 | |
getXml() |
String |
XML 请求体字符串 | |
| XML 处理 | getXml(Class<T>) |
T |
XML 转 Bean(默认根节点 <data>) |
getXml(Class<T>, String) |
T |
XML 转 Bean(自定义根节点别名) | |
| 分页 | getPager() |
Pager |
获取分页对象 |
| 文件与流 | getWebFile() |
WebFile |
获取表单上传文件对象 |
getBinaryStream() |
InputStream |
获取原始二进制输入流 |
五、最佳实践与注意事项
5.1 响应设计最佳实践
- 统一使用
ResultVO封装所有接口返回,包括异常响应。配合@RestControllerAdvice全局异常处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResultVO<?> handleBizException(BizException e) {
return ResultVO.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<?> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return ResultVO.fail(400, msg);
}
}
- 业务状态码用枚举管理,实现
ResultMessage接口,避免硬编码。 - 分页数据与列表数据平铺返回,前端直接取
pager.total,无需嵌套解析。
5.2 请求参数最佳实践
- 优先使用
toBean()整体转换,减少逐个字段取值。 - 嵌套取值用
getPathNode(),避免手写多层Map.get()。 - 文件上传和 JSON 不要混用,
multipart/form-data中的 JSON 字段需要特殊处理。 - 二进制流需要调用方自行关闭,避免资源泄漏。
5.3 注意事项
| 问题 | 说明 |
|---|---|
code 类型不一致 |
同一个项目中应保持 code 类型统一(Integer 或 String),不要混用 |
toBean() 仅对 JSON 有效 |
Form 表单参数需逐个 getStr() 取值,或先封装为 Map |
toListBean() 仅对 JSON 数组有效 |
其他请求类型返回空 List,不会抛异常 |
getPathNode() 不支持数组索引 |
路径中不能包含 [0] 这样的数组下标 |
ResultFieldMapping 全局生效 |
一个 Spring 容器中只能有一个实现,多项目需注意 Bean 名称冲突 |
六、总结
ResultVO + RequestVO 提供了一套完整的 API 设计规范:
| 维度 | ResultVO | RequestVO |
|---|---|---|
| 核心职责 | 统一响应结构 | 统一请求封装 |
| 类型支持 | code 支持 Integer / String | 支持 JSON/Form/FormData/XML/二进制流 |
| 分页设计 | Pager 与 data 平铺 | 自动解析分页参数,支持自定义参数名 |
| 字段自定义 | ResultFieldMapping 接口 | - |
| 状态管理 | ResultMessage 接口 | - |
| 参数取值 | - | 类型安全取值 + 嵌套路径 + 对象转换 |
两者的配合使用,可以让你的 API 设计更加规范、前后端协作更加高效。无论是互联网项目还是企业级应用,这套设计体系都能很好地适配。
源码地址:pan-common
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,后续将持续更新 Java 工具类实战系列。
更多推荐
所有评论(0)