SpringBoot项目统一响应设计与全局异常处理实践
前言
在Spring Boot项目开发中,统一的响应格式和全局异常处理机制是构建健壮API的基础。本文将详细介绍如何设计并实现一套完整的统一响应结构和全局异常处理机制,以提高API的可读性、一致性和可维护性。
设计目标
- 统一响应格式,确保所有API返回结构一致
- 统一状态码定义,便于前端处理不同情况
- 全局异常处理,优雅地捕获并处理各类异常
- 自动响应包装,减少重复代码
统一响应结构
1. 状态码枚举(ApiCode)
首先,我们需要定义统一的状态码枚举,用于标识API响应的状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| package com.xingcy.app.common;
import lombok.Getter;
@Getter public enum ApiCode {
SUCCESS(200, "操作成功"),
FAILED(400, "操作失败"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "无权限访问"),
NOT_FOUND(404, "资源不存在"),
METHOD_NOT_ALLOWED(405, "方法不允许"),
PARAM_ERROR(406, "请求参数校验失败"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务不可用"),
BUSINESS_ERROR(10000, "业务异常");
private final int code;
private final String msg;
ApiCode(int code, String msg) { this.code = code; this.msg = msg; } }
|
2. 统一响应结果(ApiResult)
接下来,我们定义统一的响应结果类,用于封装所有API的返回结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| package com.xingcy.app.common;
import lombok.Data;
import java.io.Serializable; import java.util.HashMap;
@Data public class ApiResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String msg;
private T data;
private long timestamp;
public ApiResult() { this.timestamp = System.currentTimeMillis(); this.data = (T) new HashMap<String, Object>(); }
public ApiResult(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data == null ? (T) new HashMap<String, Object>() : data; this.timestamp = System.currentTimeMillis(); }
public static <T> ApiResult<T> of(int code, String msg, T data) { return new ApiResult<>(code, msg, data); }
public static <T> ApiResult<T> success(T data) { return of(ApiCode.SUCCESS.getCode(), ApiCode.SUCCESS.getMsg(), data); }
public static <T> ApiResult<T> success(String msg) { return of(ApiCode.SUCCESS.getCode(), msg, null); }
public static <T> ApiResult<T> success(String msg, T data) { return of(ApiCode.SUCCESS.getCode(), msg, data); }
public static <T> ApiResult<T> success() { return of(ApiCode.SUCCESS.getCode(), ApiCode.SUCCESS.getMsg(), null); }
public static <T> ApiResult<T> failed(String msg, T data) { return of(ApiCode.FAILED.getCode(), msg, data); }
public static <T> ApiResult<T> failed(String msg) { return of(ApiCode.FAILED.getCode(), msg, null); }
public static <T> ApiResult<T> failed() { return of(ApiCode.FAILED.getCode(), ApiCode.FAILED.getMsg(), null); }
public static <T> ApiResult<T> result(ApiCode apiCode, T data) { return of(apiCode.getCode(), apiCode.getMsg(), data); }
public static <T> ApiResult<T> result(ApiCode apiCode) { return of(apiCode.getCode(), apiCode.getMsg(), null); } }
|
3. 业务异常类(BusinessException)
为了在业务逻辑中抛出自定义异常,我们创建了业务异常类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| package com.xingcy.app.common;
import lombok.Getter;
import java.io.Serial;
public class BusinessException extends RuntimeException {
@Serial private static final long serialVersionUID = 1L;
@Getter private int code;
private final String message;
public BusinessException(ApiCode apiCode) { super(apiCode.getMsg()); this.code = apiCode.getCode(); this.message = apiCode.getMsg(); }
public BusinessException(ApiCode apiCode, String message) { super(message); this.code = apiCode.getCode(); this.message = message; }
public BusinessException(int code, String message) { super(message); this.code = code; this.message = message; }
@Override public String getMessage() { return message; } }
|
全局异常处理
1. 全局异常处理器(GlobalExceptionHandler)
全局异常处理器用于捕获并处理各种异常,返回统一的响应格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
| package com.xingcy.app.common;
import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Set;
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.OK) public ApiResult<Object> handleBusinessException(BusinessException e) { log.error("业务异常:{}", e.getMessage(), e); return ApiResult.of(e.getCode(), e.getMessage(), new HashMap<String, Object>()); }
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResult<List<String>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.error("参数校验异常:{}", e.getMessage(), e); return handleBindingResult(e.getBindingResult()); }
@ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResult<List<String>> handleBindException(BindException e) { log.error("表单绑定异常:{}", e.getMessage(), e); return handleBindingResult(e.getBindingResult()); }
private ApiResult<List<String>> handleBindingResult(BindingResult bindingResult) { List<String> errorMessages = new ArrayList<>(); for (FieldError fieldError : bindingResult.getFieldErrors()) { errorMessages.add(fieldError.getDefaultMessage()); } return ApiResult.of(ApiCode.PARAM_ERROR.getCode(), ApiCode.PARAM_ERROR.getMsg(), errorMessages); }
@ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResult<List<String>> handleConstraintViolationException(ConstraintViolationException e) { log.error("约束违反异常:{}", e.getMessage(), e); List<String> errorMessages = new ArrayList<>(); Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); for (ConstraintViolation<?> violation : violations) { errorMessages.add(violation.getMessage()); } return ApiResult.of(ApiCode.PARAM_ERROR.getCode(), ApiCode.PARAM_ERROR.getMsg(), errorMessages); }
@ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiResult<Object> handleNoHandlerFoundException(NoHandlerFoundException e) { log.error("404异常:{}", e.getMessage(), e); return ApiResult.of(ApiCode.NOT_FOUND.getCode(), ApiCode.NOT_FOUND.getMsg(), new HashMap<String, Object>()); }
@ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) public ApiResult<Object> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.error("请求方法不支持异常:{}", e.getMessage(), e); return ApiResult.of(ApiCode.METHOD_NOT_ALLOWED.getCode(), ApiCode.METHOD_NOT_ALLOWED.getMsg(), new HashMap<String, Object>()); }
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResult<Object> handleException(Exception e) { log.error("系统异常:{}", e.getMessage(), e); return ApiResult.of(ApiCode.INTERNAL_SERVER_ERROR.getCode(), ApiCode.INTERNAL_SERVER_ERROR.getMsg(), new HashMap<String, Object>()); } }
|
2. 响应通知器(ResponseAdvice)
响应通知器用于自动将非ApiResult类型的返回值包装成ApiResult:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package com.xingcy.app.common;
import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@Slf4j @RestControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override public boolean supports(MethodParameter returnType, Class converterType) { return !returnType.getParameterType().isAssignableFrom(ApiResult.class); }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body == null) { return ApiResult.success(new HashMap<String, Object>()); } if (body instanceof String) { return ApiResult.success((String) body); } return ApiResult.success(body); } }
|
实际应用示例
1. 直接使用ApiResult
1 2 3 4
| @GetMapping("/success") public ApiResult<String> success() { return ApiResult.success("这是成功的响应"); }
|
2. 返回普通对象(自动包装)
1 2 3 4 5 6 7
| @GetMapping("/object") public Map<String, Object> object() { Map<String, Object> map = new HashMap<>(); map.put("name", "张三"); map.put("age", 20); return map; }
|
3. 抛出业务异常
1 2 3 4 5
| @GetMapping("/business-error") public ApiResult<String> businessError() { throw new BusinessException(ApiCode.BUSINESS_ERROR, "这是一个业务异常示例"); }
|
4. 抛出自定义错误码的业务异常
1 2 3 4 5
| @GetMapping("/custom-error") public ApiResult<String> customError() { throw new BusinessException(1001, "这是一个自定义错误码的业务异常"); }
|
响应示例
成功响应
1 2 3 4 5 6 7 8 9
| { "code": 200, "msg": "操作成功", "data": { "name": "张三", "age": 20 }, "timestamp": 1623145678901 }
|
业务异常响应
1 2 3 4 5 6
| { "code": 10000, "msg": "这是一个业务异常示例", "data": {}, "timestamp": 1623145678901 }
|
参数验证失败响应
1 2 3 4 5 6 7 8
| { "code": 406, "msg": "请求参数校验失败", "data": [ "ID必须大于0" ], "timestamp": 1623145678901 }
|
特性和优势
- 统一格式:所有响应都具有相同的结构,便于前端处理
- 状态码标准化:使用统一的状态码体系,清晰表达响应状态
- 自动包装:自动将各种类型的返回值包装成统一格式,减少重复代码
- 全局异常处理:集中处理各类异常,避免异常泄露给客户端
- 详细错误信息:提供详细的错误信息,便于调试和问题排查
- 空数据一致性:data字段即使为空也返回空对象{},确保前端处理一致性
使用建议
- 在业务逻辑中,使用BusinessException抛出业务相关异常
- 对于简单返回,直接返回原始类型,由ResponseAdvice自动包装
- 对于复杂返回,可以直接使用ApiResult的静态方法构造响应
- 记录关键业务日志,便于问题排查
- 按需扩展ApiCode枚举,添加业务相关的状态码
总结
统一响应设计和全局异常处理是构建健壮API的重要基础设施。通过本文介绍的方法,可以快速在Spring Boot项目中实现标准化的API响应和异常处理机制,提高API的可用性和可维护性。这种设计不仅有利于前后端协作,也有助于应用的健壮性和可扩展性。