SpringBoot项目统一响应设计与全局异常处理实践

前言

在Spring Boot项目开发中,统一的响应格式和全局异常处理机制是构建健壮API的基础。本文将详细介绍如何设计并实现一套完整的统一响应结构和全局异常处理机制,以提高API的可读性、一致性和可维护性。

设计目标

  1. 统一响应格式,确保所有API返回结构一致
  2. 统一状态码定义,便于前端处理不同情况
  3. 全局异常处理,优雅地捕获并处理各类异常
  4. 自动响应包装,减少重复代码

统一响应结构

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;

/**
* API 状态码枚举类
*
* @author xingcy
* @since 1.0.0
*/
@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;

/**
* 统一API响应结果
*
* @author xingcy
* @since 1.0.0
* @param <T> 数据类型
*/
@Data
public class ApiResult<T> implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 状态码
*/
private int code;

/**
* 消息
*/
private String msg;

/**
* 数据
* 不使用JsonInclude.Include.NON_NULL,确保即使为null时也返回该字段
*/
private T data;

/**
* 响应时间戳
*/
private long timestamp;

/**
* 无参构造函数
*/
public ApiResult() {
this.timestamp = System.currentTimeMillis();
// 默认设置为空对象
this.data = (T) new HashMap<String, Object>();
}

/**
* 全参构造函数
*
* @param code 状态码
* @param msg 消息
* @param data 数据
*/
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();
}

/**
* 构造一个自定义的API返回
*
* @param code 状态码
* @param msg 消息
* @param data 数据
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> of(int code, String msg, T data) {
return new ApiResult<>(code, msg, data);
}

/**
* 构造一个成功且带数据的API返回
*
* @param data 数据
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> success(T data) {
return of(ApiCode.SUCCESS.getCode(), ApiCode.SUCCESS.getMsg(), data);
}

/**
* 构造一个成功且自定义消息的API返回
*
* @param msg 消息
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> success(String msg) {
return of(ApiCode.SUCCESS.getCode(), msg, null);
}

/**
* 构造一个成功且带数据和自定义消息的API返回
*
* @param msg 消息
* @param data 数据
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> success(String msg, T data) {
return of(ApiCode.SUCCESS.getCode(), msg, data);
}

/**
* 构造一个成功且不带数据的API返回
*
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> success() {
return of(ApiCode.SUCCESS.getCode(), ApiCode.SUCCESS.getMsg(), null);
}

/**
* 构造一个失败且带数据的API返回
*
* @param msg 消息
* @param data 数据
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> failed(String msg, T data) {
return of(ApiCode.FAILED.getCode(), msg, data);
}

/**
* 构造一个失败且不带数据的API返回
*
* @param msg 消息
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> failed(String msg) {
return of(ApiCode.FAILED.getCode(), msg, null);
}

/**
* 构造一个失败且不带数据和使用默认消息的API返回
*
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> failed() {
return of(ApiCode.FAILED.getCode(), ApiCode.FAILED.getMsg(), null);
}

/**
* 构造一个自定义状态且带数据的API返回
*
* @param apiCode 状态码
* @param data 数据
* @param <T> 数据类型
* @return ApiResult
*/
public static <T> ApiResult<T> result(ApiCode apiCode, T data) {
return of(apiCode.getCode(), apiCode.getMsg(), data);
}

/**
* 构造一个自定义状态且不带数据的API返回
*
* @param apiCode 状态码
* @param <T> 数据类型
* @return ApiResult
*/
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;

/**
* 业务异常类
*
* @author xingcy
* @since 1.0.0
*/
public class BusinessException extends RuntimeException {

@Serial
private static final long serialVersionUID = 1L;

/**
* 错误码
*/
@Getter
private int code;

/**
* 错误消息
*/
private final String message;

/**
* 根据状态码枚举构造业务异常
*
* @param apiCode 状态码枚举
*/
public BusinessException(ApiCode apiCode) {
super(apiCode.getMsg());
this.code = apiCode.getCode();
this.message = apiCode.getMsg();
}

/**
* 自定义消息的业务异常
*
* @param apiCode 状态码枚举
* @param message 自定义消息
*/
public BusinessException(ApiCode apiCode, String message) {
super(message);
this.code = apiCode.getCode();
this.message = message;
}

/**
* 完全自定义的业务异常
*
* @param code 错误码
* @param message 错误消息
*/
public BusinessException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}

/**
* 获取错误消息
*
* @return 错误消息
*/
@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;

/**
* 全局异常处理器
*
* @author xingcy
* @since 1.0.0
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

/**
* 处理自定义业务异常
*
* @param e 业务异常
* @return ApiResult
*/
@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>());
}

/**
* 处理参数校验异常(Bean Validation)
*
* @param e 参数异常
* @return ApiResult
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResult<List<String>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("参数校验异常:{}", e.getMessage(), e);
return handleBindingResult(e.getBindingResult());
}

/**
* 处理表单绑定异常
*
* @param e 表单绑定异常
* @return ApiResult
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResult<List<String>> handleBindException(BindException e) {
log.error("表单绑定异常:{}", e.getMessage(), e);
return handleBindingResult(e.getBindingResult());
}

/**
* 处理绑定结果
*
* @param bindingResult 绑定结果
* @return ApiResult
*/
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);
}

/**
* 处理约束违反异常
*
* @param e 约束违反异常
* @return ApiResult
*/
@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);
}

/**
* 处理404异常
*
* @param e 异常
* @return ApiResult
*/
@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>());
}

/**
* 处理请求方法不支持异常
*
* @param e 异常
* @return ApiResult
*/
@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>());
}

/**
* 处理所有其他异常
*
* @param e 异常
* @return ApiResult
*/
@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;

/**
* 统一响应处理器
* 将所有接口返回包装成统一格式
*
* @author xingcy
* @since 1.0.0
*/
@Slf4j
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

/**
* 是否支持处理
*
* @param returnType 返回类型
* @param converterType 转换器类型
* @return 是否支持
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 如果已经是ApiResult类型,则不需要处理
return !returnType.getParameterType().isAssignableFrom(ApiResult.class);
}

/**
* 响应前处理
*
* @param body 响应体
* @param returnType 返回类型
* @param selectedContentType 内容类型
* @param selectedConverterType 转换器类型
* @param request 请求
* @param response 响应
* @return 处理后的响应体
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 对null值特殊处理,返回空对象
if (body == null) {
// 返回成功,data为空对象
return ApiResult.success(new HashMap<String, Object>());
}

// 对String特殊处理,因为String类型无法进行类型转换
if (body instanceof String) {
// 如果是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() {
// 抛出业务异常,会被GlobalExceptionHandler捕获并处理
throw new BusinessException(ApiCode.BUSINESS_ERROR, "这是一个业务异常示例");
}

4. 抛出自定义错误码的业务异常

1
2
3
4
5
@GetMapping("/custom-error")
public ApiResult<String> customError() {
// 抛出自定义错误码的业务异常,会被GlobalExceptionHandler捕获并处理
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
}

特性和优势

  1. 统一格式:所有响应都具有相同的结构,便于前端处理
  2. 状态码标准化:使用统一的状态码体系,清晰表达响应状态
  3. 自动包装:自动将各种类型的返回值包装成统一格式,减少重复代码
  4. 全局异常处理:集中处理各类异常,避免异常泄露给客户端
  5. 详细错误信息:提供详细的错误信息,便于调试和问题排查
  6. 空数据一致性:data字段即使为空也返回空对象{},确保前端处理一致性

使用建议

  1. 在业务逻辑中,使用BusinessException抛出业务相关异常
  2. 对于简单返回,直接返回原始类型,由ResponseAdvice自动包装
  3. 对于复杂返回,可以直接使用ApiResult的静态方法构造响应
  4. 记录关键业务日志,便于问题排查
  5. 按需扩展ApiCode枚举,添加业务相关的状态码

总结

统一响应设计和全局异常处理是构建健壮API的重要基础设施。通过本文介绍的方法,可以快速在Spring Boot项目中实现标准化的API响应和异常处理机制,提高API的可用性和可维护性。这种设计不仅有利于前后端协作,也有助于应用的健壮性和可扩展性。