视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在前两篇中,我们分别讲了全局异常处理和参数校验,解决了“出错怎么返回”和“非法请求怎么拦截”的问题。
但还有一个更基础、却常被忽视的问题:
所有接口的返回格式五花八门,前端每次都要猜字段含义!
今天我们就来解决这个痛点——如何设计统一、清晰、可扩展的 RESTful API 响应结构。
一、需求场景
你正在开发一个电商系统,有以下接口:
- 获取商品列表 → 返回
List<Product> - 用户登录 → 返回 token
- 下单 → 返回订单号
- 删除商品 → 成功或失败
问题来了:
- 有的接口直接返回数据,有的返回
{code:200, data:...}; - 错误时有的返回
{"error": "xxx"},有的返回{"msg": "xxx"}; - 前端无法用一套逻辑处理所有响应!
后果:联调效率低、Bug 多、体验差。
二、解决方案:统一封装响应体(CommonResult)
✅ 正确做法(业界标准)
1. 定义通用返回类
// CommonResult.java import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class CommonResult<T> { private int code; // 状态码:200=成功,400=参数错误,500=服务器错误等 private String message; // 提示信息 private T data; // 业务数据 private long timestamp; // 时间戳(可选) // 成功:无数据 public static <T> CommonResult<T> success() { return new CommonResult<>(200, "操作成功", null); } // 成功:带数据 public static <T> CommonResult<T> success(T data) { return new CommonResult<>(200, "操作成功", data); } // 失败 public static <T> CommonResult<T> error(int code, String message) { return new CommonResult<>(code, message, null); } // 私有构造 private CommonResult(int code, String message, T data) { this.code = code; this.message = message; this.data = data; this.timestamp = System.currentTimeMillis(); } }💡 使用 Lombok 的
@Data自动生成 getter/setter/toString,减少样板代码。
2. Controller 统一返回 CommonResult
@RestController @RequestMapping("/api/product") public class ProductController { @GetMapping public CommonResult<List<Product>> listProducts() { List<Product> products = productService.findAll(); return CommonResult.success(products); } @PostMapping public CommonResult<String> createProduct(@Valid @RequestBody ProductDTO dto) { String id = productService.create(dto); return CommonResult.success(id); } @DeleteMapping("/{id}") public CommonResult<Void> deleteProduct(@PathVariable String id) { productService.deleteById(id); return CommonResult.success(); // 无数据返回 } }3. 与全局异常处理器联动(回顾上一篇)
@ExceptionHandler(BusinessException.class) public CommonResult<Void> handleBusiness(BusinessException e) { return CommonResult.error(e.getCode(), e.getMessage()); }这样,无论成功还是失败,前端收到的都是同一套结构!
三、反例(千万别这么写!)
❌ 反例1:直接返回实体对象
@GetMapping("/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); // 直接返回 User 对象 }问题:
- 成功时返回
{id:1, name:"张三"}; - 失败时可能返回
{"timestamp":"...", "status":500, "error":"..."}(Spring Boot 默认错误页); - 前端无法区分是“业务成功”还是“系统异常”。
❌ 反例2:每个接口自定义返回结构
public class LoginResponse { private String token; private boolean success; } public class ProductListResponse { private List<Product> items; private int total; }问题:
- 无法复用;
- 前端要为每个接口写解析逻辑;
- 后期加字段(如 traceId、version)需全量修改。
四、进阶设计:支持分页、元信息、泛型嵌套
场景:列表接口需要返回总数、分页信息
方案:使用泛型包装器
// PageResult.java @Data public class PageResult<T> { private List<T> list; private long total; private int pageNum; private int pageSize; } // Controller @GetMapping("/page") public CommonResult<PageResult<Product>> pageProducts( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize) { PageResult<Product> page = productService.page(pageNum, pageSize); return CommonResult.success(page); }前端收到:
{ "code": 200, "message": "操作成功", "data": { "list": [...], "total": 100, "pageNum": 1, "pageSize": 10 }, "timestamp": 1703489832123 }✅ 清晰、可扩展、前后端契约明确!
五、注意事项(面试加分项!)
不要把敏感信息放入 message
比如数据库错误堆栈、内部路径等,防止信息泄露。code 建议与 HTTP 状态码一致
- 200:成功
- 400:客户端错误(参数、校验)
- 401:未认证
- 403:无权限
- 404:资源不存在
- 500:服务器内部错误
这样即使不看
code字段,仅看 HTTP 状态也能判断大类。data 为 null 时,JSON 中是否保留字段?
使用 Jackson 配置:@JsonInclude(JsonInclude.Include.NON_NULL) public class CommonResult<T> { ... }避免返回
"data": null,更简洁。国际化支持(高阶)
message可通过MessageSource根据用户语言动态返回,适合多语言系统。与 Swagger 集成
在 Controller 方法上加上:@Operation(summary = "获取商品列表") @ApiResponse(responseCode = "200", description = "成功", content = @Content(schema = @Schema(implementation = CommonResult.class)))
六、总结
| 优势 | 说明 |
|---|---|
| ✅ 前后端解耦 | 接口契约清晰,降低沟通成本 |
| ✅ 易于调试 | 所有响应结构一致,日志/监控好处理 |
| ✅ 可扩展性强 | 加字段(如 traceId、version)只需改 CommonResult |
| ✅ 提升专业度 | 体现工程规范意识,面试官眼前一亮 |
记住:好的 API 不只是“能用”,而是“好用、易用、稳定用”。
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!