本文目录导读:

- 最基础的实现:通用响应类
- 使用枚举管理状态码
- 利用 Spring Boot 的全局异常处理
- 使用 Spring 的 ResponseBodyAdvice 自动包装
- 结合 DTO 与 Jackson 注解(精细化控制)
- 完整企业级示例(整合以上所有技术)
- 最佳实践建议
最基础的实现:通用响应类
这是最直接的方式,定义一个泛型类包装所有返回数据。
public class ApiResult<T> {
private int code; // 状态码
private String message; // 提示信息
private T data; // 数据主体
// 构造方法私有化,强制使用静态工厂方法
private ApiResult() {}
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<>();
result.code = 200;
result.message = "success";
result.data = data;
return result;
}
public static <T> ApiResult<T> success(String message, T data) {
ApiResult<T> result = new ApiResult<>();
result.code = 200;
result.message = message;
result.data = data;
return result;
}
public static <T> ApiResult<T> error(int code, String message) {
ApiResult<T> result = new ApiResult<>();
result.code = code;
result.message = message;
result.data = null;
return result;
}
// getter/setter 省略
}
使用示例:
@RestController
public class UserController {
@GetMapping("/user/{id}")
public ApiResult<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return ApiResult.error(404, "用户不存在");
}
return ApiResult.success(user);
}
}
使用枚举管理状态码
定义统一的状态码枚举,避免硬编码数字。
public enum ResultCode {
SUCCESS(200, "操作成功"),
FAIL(500, "操作失败"),
NOT_FOUND(404, "资源不存在"),
UNAUTHORIZED(401, "未授权");
private final int code;
private final String message;
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
public int code() { return code; }
public String message() { return message; }
}
然后修改 ApiResult:
public static <T> ApiResult<T> success(T data) {
return success(ResultCode.SUCCESS.message(), data);
}
public static <T> ApiResult<T> error(ResultCode resultCode) {
return error(resultCode.code(), resultCode.message());
}
利用 Spring Boot 的全局异常处理
结合 @RestControllerAdvice 和 @ExceptionHandler,实现所有异常统一响应。
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ApiResult<?> handleBusinessException(BusinessException e) {
return ApiResult.error(e.getCode(), e.getMessage());
}
// 处理参数校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<?> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult()
.getFieldErrors().stream()
.map(er -> er.getField() + ": " + er.getDefaultMessage())
.collect(Collectors.joining(", "));
return ApiResult.error(400, msg);
}
// 兜底处理所有未捕获异常
@ExceptionHandler(Exception.class)
public ApiResult<?> handleException(Exception e) {
log.error("系统异常", e);
return ApiResult.error(500, "服务器内部错误");
}
}
自定义业务异常类:
public class BusinessException extends RuntimeException {
private int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}
这样控制器只需正常抛出异常即可,无需手动包装:
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
throw new BusinessException(404, "用户不存在");
}
return user;
}
Spring 会自动将返回的 User 对象包装进 ApiResult,异常也按配置的格式返回。
使用 Spring 的 ResponseBodyAdvice 自动包装
如果你不想在每个控制器方法上都写 ApiResult.success(...),可以实现 ResponseBodyAdvice 对返回结果统一包装。
@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 对所有返回值都生效(可排除某些类或包)
return !returnType.getParameterType().equals(ApiResult.class);
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 如果已经是 ApiResult 类型,不再包装
if (body instanceof ApiResult) {
return body;
}
// 返回 void 的方法 body 为 null,可以改为成功
if (body == null) {
return ApiResult.success(null);
}
return ApiResult.success(body);
}
}
优点: 控制器方法可以返回纯粹的实体对象,框架自动包装。 注意: 需要妥善处理返回 String 类型时的序列化问题(因为 StringHttpMessageConverter 会优先处理)。
结合 DTO 与 Jackson 注解(精细化控制)
可以使用 @JsonInclude、@JsonProperty 控制序列化行为:
@Data
@JsonInclude(JsonInclude.Include.NON_NULL) // null字段不序列化
public class ApiResult<T> {
private int code;
private String message;
private T data;
private Long timestamp; // 可加时间戳
public ApiResult() {
this.timestamp = System.currentTimeMillis();
}
// 静态工厂方法同上...
}
完整企业级示例(整合以上所有技术)
// ========= 1. 状态码枚举 =========
public enum ResultCode {
SUCCESS(200, "成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未登录"),
FORBIDDEN(403, "权限不足"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_ERROR(500, "服务器内部错误");
final int code;
final String message;
// 构造、getter...
}
// ========= 2. 统一响应类 =========
public class R<T> {
private int code;
private String message;
private T data;
private long timestamp;
private R() { this.timestamp = System.currentTimeMillis(); }
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.code = ResultCode.SUCCESS.code;
r.message = ResultCode.SUCCESS.message;
r.data = data;
return r;
}
public static <T> R<T> fail(ResultCode resultCode) {
return fail(resultCode.code, resultCode.message);
}
public static <T> R<T> fail(int code, String message) {
R<T> r = new R<>();
r.code = code;
r.message = message;
return r;
}
// getter...
}
// ========= 3. 全局异常处理 =========
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<?> handle(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("; "));
return R.fail(ResultCode.BAD_REQUEST.code, msg);
}
@ExceptionHandler(BusinessException.class)
public R<?> handle(BusinessException e) {
return R.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public R<?> handle(Exception e) {
log.error("异常", e);
return R.fail(ResultCode.INTERNAL_ERROR);
}
}
// ========= 4. 控制器示例 =========
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public R<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return R.fail(ResultCode.NOT_FOUND);
}
return R.ok(user);
}
@PostMapping
public R<Void> createUser(@Valid @RequestBody UserCreateDTO dto) {
userService.create(dto);
return R.ok(null);
}
}
前端接收到的 JSON 格式统一为:
{
"code": 200,
"message": "成功",
"data": { ... },
"timestamp": 1695000000000
}
最佳实践建议
| 场景 | 推荐方案 |
|---|---|
| 小型项目 | 方案一 + 方案三(纯手工包装) |
| 中型项目 | 方案二(枚举) + 方案三(全局异常处理) |
| 大型微服务 | 方案四(ResponseBodyAdvice自动包装)+ 方案三 |
| 前后端分离 | 方案五(Null控制) + 详细的状态码文档 |
| 需要国际化 | 消息从配置文件读取,结合 locale 动态设置 |
核心原则:
- 所有接口响应格式完全一致(code + message + data)
- 成功和异常都要统一(异常由全局处理器接管)
- 状态码要有明确的业务含义,不要仅用 200/500
- 避免暴露内部实现细节(如数据库错误堆栈)
这样前端可以写一个统一的 request.js 拦截器,根据 code 做出全局响应处理(如 401 跳登录、500 弹错误提示等)。