告别重复造轮子!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 平级,为空时不参与序列化

两个关键设计点:

  1. code 类型自适应:互联网项目用 200(Integer),企业级/银行项目用 "SYS_ORDER_404"(String),一个类全兼容。
  2. datapager 为空时不参与序列化@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"false
  • Number 类型:非 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 响应设计最佳实践

  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);
    }
}
  1. 业务状态码用枚举管理,实现 ResultMessage 接口,避免硬编码。
  2. 分页数据与列表数据平铺返回,前端直接取 pager.total,无需嵌套解析。

5.2 请求参数最佳实践

  1. 优先使用 toBean() 整体转换,减少逐个字段取值。
  2. 嵌套取值用 getPathNode(),避免手写多层 Map.get()
  3. 文件上传和 JSON 不要混用multipart/form-data 中的 JSON 字段需要特殊处理。
  4. 二进制流需要调用方自行关闭,避免资源泄漏。

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 工具类实战系列。

更多推荐