在前后端分离架构中,接口返回值的规范性直接影响协作效率 —— 若每个接口返回格式不统一,前端需针对不同接口做差异化解析,后端也需重复编写返回逻辑。本文将带你设计一套 “通用、灵活、易扩展” 的接口返回值体系,结合异常统一处理,实现接口返回的标准化。
一、设计背景:为什么需要通用返回值?
先看 “无通用返回值” 的典型问题,理解标准化的必要性:
1. 前后端协作效率低
- 问题:接口 A 返回{code:200, data:{}, msg:""},接口 B 返回{success:true, result:{}, message:""},接口 C 直接返回业务数据(如{id:1, name:""});
- 后果:前端需为每个接口编写不同的解析逻辑,遇到异常时(如登录失败),无法通过统一逻辑判断状态,协作成本高。
2. 异常处理不统一
- 问题:业务异常(如 “用户名不存在”)返回自定义格式,系统异常(如空指针)返回 Spring 默认的 500 错误页面,参数校验异常返回零散提示;
- 后果:前端无法通过统一方式捕获所有异常,用户体验差,后端也难以追溯异常原因。
3. 扩展性差
- 问题:后续需新增 “错误编码”“请求 ID” 等字段时,需修改所有接口的返回逻辑;
二、通用返回值设计原则
基于上述痛点,通用返回值需遵循以下原则:
- 统一性:所有接口返回相同的顶层格式,包含 “状态标识、业务数据、提示信息、错误编码”;
- 灵活性:业务数据支持泛型,适配不同接口的返回类型(如列表、对象、单个值);
- 可识别性:通过明确的状态标识(如success)和错误编码(如code),让前端快速判断接口状态;
- 兼容性:支持异常场景的统一封装,确保异常返回格式与正常返回一致。
三、核心实现:通用返回值体系
通用返回值体系包含 “返回值模型、工具类、异常统一处理” 三部分,基于 Spring Boot 实现。
1. 定义通用返回值模型(泛型支持)
设计Result<T>类作为所有接口的返回类型,包含 4 个核心字段,适配正常与异常场景:
[AppleScript] 纯文本查看 复制代码 package com.itsoku.lesson013.common;
import lombok.Data;
/**
* 接口通用返回值模型
* @param <T> 业务数据类型(支持泛型,适配不同接口)
*/
@Data
public class Result<T> {
/**
* 接口状态标识:true=成功,false=失败
* 前端可通过此字段快速判断接口是否正常执行
*/
private boolean success;
/**
* 业务数据:成功时返回的业务数据(如用户列表、订单详情)
* 失败时为null
*/
private T data;
/**
* 提示信息:失败时返回的用户友好提示(如“用户名不存在”)
* 成功时可为null或空字符串
*/
private String msg;
/**
* 错误编码:失败时返回的具体错误编码(如“1001”=用户名错误,“500”=系统异常)
* 前端可根据此编码做差异化处理(如跳转登录页、显示特定弹窗)
* 成功时为null
*/
private String code;
// 私有构造方法:避免外部直接创建对象,需通过工具类创建
private Result() {}
// 成功时的构造方法(无业务数据)
private Result(boolean success) {
this.success = success;
this.data = null;
this.msg = null;
this.code = null;
}
// 成功时的构造方法(带业务数据)
private Result(boolean success, T data) {
this.success = success;
this.data = data;
this.msg = null;
this.code = null;
}
// 失败时的构造方法(带错误编码和提示信息)
private Result(boolean success, String code, String msg) {
this.success = success;
this.data = null;
this.code = code;
this.msg = msg;
}
}
2. 工具类:简化返回逻辑
编写ResultUtils工具类,提供静态方法快速创建 “成功”“失败” 类型的Result对象,避免重复编码:
[Java] 纯文本查看 复制代码 package com.itsoku.lesson013.common;
/**
* 通用返回值工具类:简化Result对象的创建
*/
public class ResultUtils {
/**
* 成功:无业务数据
* 适用场景:接口仅需告知成功(如删除操作、注销操作)
*/
public static <T> Result<T> success() {
return new Result<>(true);
}
/**
* 成功:带业务数据
* 适用场景:接口需返回业务数据(如查询用户列表、获取订单详情)
* @param data 业务数据(支持任意类型)
*/
public static <T> Result<T> success(T data) {
return new Result<>(true, data);
}
/**
* 失败:带错误编码和提示信息
* 适用场景:业务异常(如“用户名不存在”“参数校验失败”)
* @param code 错误编码(如“1001”=用户名错误)
* @param msg 提示信息(用户友好文案)
*/
public static <T> Result<T> error(String code, String msg) {
return new Result<>(false, code, msg);
}
/**
* 失败:仅带提示信息(默认错误编码为“9999”)
* 适用场景:简单异常,无需区分具体错误类型
* @param msg 提示信息
*/
public static <T> Result<T> error(String msg) {
return new Result<>(false, "9999", msg);
}
}
3. 错误编码枚举:集中管理错误码
将系统中所有错误编码集中到ErrorCode枚举类,避免硬编码,便于维护:
[Java] 纯文本查看 复制代码 package com.itsoku.lesson013.common;
/**
* 错误编码枚举:集中管理所有错误码,便于维护和查找
*/
public enum ErrorCode {
// 系统通用错误(1000~1999)
SUCCESS("200", "操作成功"),
SYSTEM_ERROR("500", "系统异常,请稍后重试"),
PARAM_ERROR("1001", "参数校验失败"),
// 用户相关错误(2000~2999)
USER_NOT_FOUND("2001", "用户名不存在"),
PASSWORD_ERROR("2002", "密码错误"),
USER_DISABLED("2003", "账号已被禁用"),
// 订单相关错误(3000~3999)
ORDER_NOT_FOUND("3001", "订单不存在"),
ORDER_STATUS_ERROR("3002", "订单状态异常");
/**
* 错误编码
*/
private final String code;
/**
* 错误提示信息
*/
private final String msg;
ErrorCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
// Getter方法
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
四、异常统一处理:确保异常返回格式一致
通过 Spring Boot 的@RestControllerAdvice实现全局异常处理,将所有异常(业务异常、系统异常、参数校验异常)统一封装为Result格式,避免前端看到非标准化的错误信息。
1. 自定义业务异常类
业务场景中(如 “用户名不存在”),主动抛出BusinessException,全局异常处理器捕获后封装为Result:
[Java] 纯文本查看 复制代码 package com.itsoku.lesson013.common;
/**
* 自定义业务异常:用于业务场景中的主动异常(如参数校验失败、业务规则不满足)
*/
public class BusinessException extends RuntimeException {
/**
* 错误编码(关联ErrorCode枚举)
*/
private final String code;
/**
* 构造方法:传入错误编码和提示信息
*/
public BusinessException(String code, String msg) {
super(msg);
this.code = code;
}
/**
* 构造方法:传入ErrorCode枚举(推荐使用,避免硬编码)
*/
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMsg());
this.code = errorCode.getCode();
}
// Getter方法
public String getCode() {
return code;
}
}
2. 全局异常处理器
定义GlobalExceptionHandler类,通过@ExceptionHandler注解捕获不同类型的异常,统一返回Result格式:
[Java] 纯文本查看 复制代码 package com.itsoku.lesson013.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* 全局异常处理器:统一处理所有接口的异常,返回标准化的Result格式
*/
@RestControllerAdvice // 作用于所有@RestController注解的类
@Slf4j // 日志记录
public class GlobalExceptionHandler {
/**
* 1. 处理自定义业务异常(BusinessException)
* 适用场景:主动抛出的业务异常(如“用户名不存在”“订单状态异常”)
*/
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
// 记录异常日志(包含请求URL、错误编码、提示信息)
log.warn("请求URL: {}, 业务异常 - 编码: {}, 信息: {}",
request.getRequestURL(), e.getCode(), e.getMessage());
// 封装为Result格式返回
return ResultUtils.error(e.getCode(), e.getMessage());
}
/**
* 2. 处理Spring参数校验异常(BindException)
* 适用场景:使用@Validated/@Valid注解进行参数校验时,校验失败抛出的异常
*/
@ExceptionHandler(BindException.class)
public Result<?> handleBindException(BindException e, HttpServletRequest request) {
// 获取参数校验失败的第一个提示信息(如“用户名不能为空”)
String msg = e.getAllErrors().get(0).getDefaultMessage();
// 记录异常日志
log.warn("请求URL: {}, 参数校验异常 - 信息: {}", request.getRequestURL(), msg);
// 封装为Result格式返回(使用 PARAM_ERROR 错误编码)
return ResultUtils.error(ErrorCode.PARAM_ERROR.getCode(), msg);
}
/**
* 3. 处理系统异常(Exception)
* 适用场景:未捕获的系统异常(如空指针、数据库连接异常),作为兜底处理
*/
@ExceptionHandler(Exception.class)
public Result<?> handleSystemException(Exception e, HttpServletRequest request) {
// 记录异常日志(包含堆栈信息,便于排查问题)
log.error("请求URL: {}, 系统异常 - 信息: {}", request.getRequestURL(), e.getMessage(), e);
// 封装为Result格式返回(使用 SYSTEM_ERROR 错误编码,避免暴露敏感信息)
return ResultUtils.error(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMsg());
}
}
五、实战案例:接口使用通用返回值
结合 “登录”“用户注册”“查询用户列表” 三个典型接口,展示通用返回值的使用流程。
1. 正常业务接口:查询用户列表
接口需返回用户列表数据,使用ResultUtils.success(data)返回成功结果:
[Java] 纯文本查看 复制代码 package com.itsoku.lesson013.controller;
import com.itsoku.lesson013.common.Result;
import com.itsoku.lesson013.common.ResultUtils;
import com.itsoku.lesson013.dto.UserDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 查询用户列表:返回业务数据(List<UserDTO>)
*/
@GetMapping("/list")
public Result<List<UserDTO>> getUserList() {
// 模拟数据库查询结果
UserDTO user1 = new UserDTO(1L, "路人", "13800138000");
UserDTO user2 = new UserDTO(2L, "张三", "13800138001");
List<UserDTO> userList = Arrays.asList(user1, user2);
// 使用工具类返回成功结果(带业务数据)
return ResultUtils.success(userList);
}
}
接口返回结果:
[AppleScript] 纯文本查看 复制代码 {
"success": true,
"data": [
{"id": 1, "name": "路人", "phone": "13800138000"},
{"id": 2, "name": "张三", "phone": "13800138001"}
],
"msg": null,
"code": null
}
2. 业务异常接口:用户登录
登录时若用户名不存在,主动抛出BusinessException,全局异常处理器捕获后返回失败结果:
[AppleScript] 纯文本查看 复制代码 package com.itsoku.lesson013.controller;
import com.itsoku.lesson013.common.BusinessException;
import com.itsoku.lesson013.common.ErrorCode;
import com.itsoku.lesson013.common.Result;
import com.itsoku.lesson013.common.ResultUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
/**
* 用户登录:业务异常场景(用户名不存在、密码错误)
*/
@GetMapping("/login")
public Result<String> login(
@RequestParam String username,
@RequestParam String password
) {
// 模拟业务逻辑:校验用户名
if (!"路人".equals(username)) {
// 主动抛出业务异常(使用ErrorCode枚举,避免硬编码)
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
// 模拟业务逻辑:校验密码
if (!"123456".equals(password)) {
throw new BusinessException(ErrorCode.PASSWORD_ERROR);
}
// 登录成功:返回token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
return ResultUtils.success(token);
}
}
用户名不存在时的返回结果:
[AppleScript] 纯文本查看 复制代码 {
"success": false,
"data": null,
"msg": "用户名不存在",
"code": "2001"
}
3. 参数校验接口:用户注册
使用 Spring 的@Validated进行参数校验,校验失败时抛出BindException,全局异常处理器捕获后返回失败结果:
[AppleScript] 纯文本查看 复制代码 package com.itsoku.lesson013.controller;
import com.itsoku.lesson013.common.Result;
import com.itsoku.lesson013.common.ResultUtils;
import com.itsoku.lesson013.dto.UserRegisterRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RegisterController {
/**
* 用户注册:参数校验场景(使用@Validated注解)
*/
@PostMapping("/register")
public Result<Void> register(@Validated @RequestBody UserRegisterRequest request) {
// 模拟注册逻辑:保存用户到数据库
System.out.println("用户注册:" + request.getUsername() + "," + request.getPhone());
// 注册成功:无业务数据,返回成功标识
return ResultUtils.success();
}
}
// 参数校验DTO
package com.itsoku.lesson013.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
public class UserRegisterRequest {
// 用户名不能为空
@NotBlank(message = "用户名不能为空")
private String</doubaocanvas>
|