SpringBoot统一错误处理实现指南
ClearSky Drizzle Lv4

概述

本文档详细记录了如何在Spring Boot项目中实现统一的错误处理系统,包括标准化错误码、统一响应格式、全局异常处理和断言工具类。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
src/main/java/com/yourpackage/
├── constant/
│ └── ErrorCode.java # 标准化错误码枚举
├── vo/
│ └── ApiResponse.java # 统一响应结果类
├── exception/
│ ├── BusinessException.java # 业务异常类
│ └── GlobalExceptionHandler.java # 全局异常处理器
├── util/
│ └── AssertUtil.java # 断言工具类
└── controller/
└── TestController.java # 使用示例

实现步骤

1. 添加依赖

pom.xml 中添加验证依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. 创建标准化错误码枚举

文件路径: src/main/java/com/yourpackage/constant/ErrorCode.java

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
package com.yourpackage.constant;

import org.springframework.http.HttpStatus;

/**
* 标准化错误码枚举
* 格式: ABCD-EFGH
* ABCD: 模块标识
* EFGH: 错误序号
*/
public enum ErrorCode {

// 成功状态码
SUCCESS("0000-0000", "操作成功", HttpStatus.OK),

// 通用错误码
SYSTEM_ERROR("1000-0001", "系统繁忙,请稍后重试", HttpStatus.INTERNAL_SERVER_ERROR),
PARAM_ERROR("1000-0002", "参数错误", HttpStatus.BAD_REQUEST),
UNAUTHORIZED("1000-0003", "未授权访问", HttpStatus.UNAUTHORIZED),
FORBIDDEN("1000-0004", "权限不足", HttpStatus.FORBIDDEN),
NOT_FOUND("1000-0005", "资源不存在", HttpStatus.NOT_FOUND),
METHOD_NOT_ALLOWED("1000-0006", "请求方法不支持", HttpStatus.METHOD_NOT_ALLOWED),

// 用户模块错误码
USER_NOT_FOUND("2000-0001", "用户不存在", HttpStatus.NOT_FOUND),
USER_ALREADY_EXISTS("2000-0002", "用户已存在", HttpStatus.CONFLICT),
USER_DISABLED("2000-0003", "用户已被禁用", HttpStatus.FORBIDDEN),
PASSWORD_ERROR("2000-0004", "密码错误", HttpStatus.UNAUTHORIZED),

// 认证授权错误码
TOKEN_EXPIRED("3000-0001", "令牌已过期", HttpStatus.UNAUTHORIZED),
TOKEN_INVALID("3000-0002", "令牌无效", HttpStatus.UNAUTHORIZED),
LOGIN_REQUIRED("3000-0003", "请先登录", HttpStatus.UNAUTHORIZED),

// 问卷模块错误码
SURVEY_NOT_FOUND("4000-0001", "问卷不存在", HttpStatus.NOT_FOUND),
SURVEY_EXPIRED("4000-0002", "问卷已过期", HttpStatus.BAD_REQUEST),
SURVEY_ALREADY_SUBMITTED("4000-0003", "问卷已提交", HttpStatus.CONFLICT),
QUESTION_NOT_FOUND("4000-0004", "问题不存在", HttpStatus.NOT_FOUND),

// 数据模块错误码
DATA_NOT_FOUND("5000-0001", "数据不存在", HttpStatus.NOT_FOUND),
DATA_DUPLICATE("5000-0002", "数据重复", HttpStatus.CONFLICT),
DATA_INVALID("5000-0003", "数据无效", HttpStatus.BAD_REQUEST),

// 文件模块错误码
FILE_UPLOAD_FAILED("6000-0001", "文件上传失败", HttpStatus.INTERNAL_SERVER_ERROR),
FILE_NOT_FOUND("6000-0002", "文件不存在", HttpStatus.NOT_FOUND),
FILE_TYPE_NOT_SUPPORTED("6000-0003", "文件类型不支持", HttpStatus.BAD_REQUEST),
FILE_SIZE_EXCEEDED("6000-0004", "文件大小超出限制", HttpStatus.BAD_REQUEST);

private final String code;
private final String message;
private final HttpStatus httpStatus;

ErrorCode(String code, String message, HttpStatus httpStatus) {
this.code = code;
this.message = message;
this.httpStatus = httpStatus;
}

public String getCode() {
return code;
}

public String getMessage() {
return message;
}

public HttpStatus getHttpStatus() {
return httpStatus;
}

public static ErrorCode fromCode(String code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.getCode().equals(code)) {
return errorCode;
}
}
return SYSTEM_ERROR;
}
}

3. 创建统一响应结果类

文件路径: src/main/java/com/yourpackage/vo/ApiResponse.java

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
package com.yourpackage.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.yourpackage.constant.ErrorCode;

import java.time.LocalDateTime;

/**
* 统一API响应结果类
* @param <T> 响应数据类型
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private String code;
private String message;
private T data;
private LocalDateTime timestamp;

public ApiResponse() {
this.timestamp = LocalDateTime.now();
}

public ApiResponse(String code, String message, T data) {
this();
this.code = code;
this.message = message;
this.data = data;
}

public static <T> ApiResponse<T> success() {
return new ApiResponse<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), null);
}

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), data);
}

public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(ErrorCode.SUCCESS.getCode(), message, data);
}

public static <T> ApiResponse<T> error(ErrorCode errorCode) {
return new ApiResponse<>(errorCode.getCode(), errorCode.getMessage(), null);
}

public static <T> ApiResponse<T> error(ErrorCode errorCode, String message) {
return new ApiResponse<>(errorCode.getCode(), message, null);
}

public static <T> ApiResponse<T> error(String code, String message) {
return new ApiResponse<>(code, message, null);
}

public static <T> ApiResponse<T> error(ErrorCode errorCode, T data) {
return new ApiResponse<>(errorCode.getCode(), errorCode.getMessage(), data);
}

public static <T> ApiResponseBuilder<T> builder() {
return new ApiResponseBuilder<>();
}

public static class ApiResponseBuilder<T> {
private String code;
private String message;
private T data;

public ApiResponseBuilder<T> code(String code) {
this.code = code;
return this;
}

public ApiResponseBuilder<T> message(String message) {
this.message = message;
return this;
}

public ApiResponseBuilder<T> data(T data) {
this.data = data;
return this;
}

public ApiResponse<T> build() {
return new ApiResponse<>(code, message, data);
}
}

// Getters and Setters
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}

4. 创建业务异常类

文件路径: src/main/java/com/yourpackage/exception/BusinessException.java

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
package com.yourpackage.exception;

import com.yourpackage.constant.ErrorCode;

/**
* 业务异常类
*/
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Object[] args;

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.args = new Object[0];
}

public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.args = new Object[0];
}

public BusinessException(ErrorCode errorCode, Object... args) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.args = args;
}

public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.args = new Object[0];
}

public ErrorCode getErrorCode() {
return errorCode;
}

public Object[] getArgs() {
return args;
}

public String getFormattedMessage() {
if (args != null && args.length > 0) {
return String.format(errorCode.getMessage(), args);
}
return errorCode.getMessage();
}
}

5. 创建全局异常处理器

文件路径: src/main/java/com/yourpackage/exception/GlobalExceptionHandler.java

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
package com.yourpackage.exception;

import com.yourpackage.constant.ErrorCode;
import com.yourpackage.vo.ApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.converter.HttpMessageNotReadableException;
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.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
logger.warn("业务异常: {}", e.getFormattedMessage(), e);
return ApiResponse.error(e.getErrorCode(), e.getFormattedMessage());
}

/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String errorMessage = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
logger.warn("参数校验异常: {}", errorMessage);
return ApiResponse.error(ErrorCode.PARAM_ERROR, errorMessage);
}

/**
* 处理绑定异常
*/
@ExceptionHandler(BindException.class)
public ApiResponse<Void> handleBindException(BindException e) {
String errorMessage = e.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
logger.warn("绑定异常: {}", errorMessage);
return ApiResponse.error(ErrorCode.PARAM_ERROR, errorMessage);
}

/**
* 处理约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
String errorMessage = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
logger.warn("约束违反异常: {}", errorMessage);
return ApiResponse.error(ErrorCode.PARAM_ERROR, errorMessage);
}

/**
* 处理缺少请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ApiResponse<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
String errorMessage = "缺少参数: " + e.getParameterName();
logger.warn("缺少请求参数: {}", errorMessage);
return ApiResponse.error(ErrorCode.PARAM_ERROR, errorMessage);
}

/**
* 处理请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiResponse<Void> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
logger.warn("请求方法不支持: {}", e.getMessage());
return ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED, e.getMessage());
}

/**
* 处理404错误
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ApiResponse<Void> handleNoHandlerFoundException(NoHandlerFoundException e) {
logger.warn("404错误: {}", e.getMessage());
return ApiResponse.error(ErrorCode.NOT_FOUND, "接口不存在");
}

/**
* 处理参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ApiResponse<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
String errorMessage = "参数类型错误: " + e.getName();
logger.warn("参数类型不匹配: {}", errorMessage);
return ApiResponse.error(ErrorCode.PARAM_ERROR, errorMessage);
}

/**
* 处理HTTP消息不可读异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ApiResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
logger.warn("HTTP消息不可读: {}", e.getMessage());
return ApiResponse.error(ErrorCode.PARAM_ERROR, "请求参数格式错误");
}

/**
* 处理未捕获的异常
*/
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
logger.error("未捕获的异常", e);
return ApiResponse.error(ErrorCode.SYSTEM_ERROR);
}
}

6. 创建断言工具类

文件路径: src/main/java/com/yourpackage/util/AssertUtil.java

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
package com.yourpackage.util;

import com.yourpackage.constant.ErrorCode;
import com.yourpackage.exception.BusinessException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Map;

/**
* 断言工具类
*/
public class AssertUtil {

public static void isTrue(boolean expression, ErrorCode errorCode) {
if (!expression) {
throw new BusinessException(errorCode);
}
}

public static void isTrue(boolean expression, ErrorCode errorCode, String message) {
if (!expression) {
throw new BusinessException(errorCode, message);
}
}

public static void isFalse(boolean expression, ErrorCode errorCode) {
if (expression) {
throw new BusinessException(errorCode);
}
}

public static void isNull(Object object, ErrorCode errorCode) {
if (object != null) {
throw new BusinessException(errorCode);
}
}

public static void notNull(Object object, ErrorCode errorCode) {
if (object == null) {
throw new BusinessException(errorCode);
}
}

public static void notBlank(String str, ErrorCode errorCode) {
if (!StringUtils.hasText(str)) {
throw new BusinessException(errorCode);
}
}

public static void notEmpty(Collection<?> collection, ErrorCode errorCode) {
if (CollectionUtils.isEmpty(collection)) {
throw new BusinessException(errorCode);
}
}

public static void notEmpty(Map<?, ?> map, ErrorCode errorCode) {
if (CollectionUtils.isEmpty(map)) {
throw new BusinessException(errorCode);
}
}

public static void notEmpty(Object[] array, ErrorCode errorCode) {
if (array == null || array.length == 0) {
throw new BusinessException(errorCode);
}
}
}

7. 使用示例

文件路径: src/main/java/com/yourpackage/controller/TestController.java

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
package com.yourpackage.controller;

import com.yourpackage.constant.ErrorCode;
import com.yourpackage.exception.BusinessException;
import com.yourpackage.util.AssertUtil;
import com.yourpackage.vo.ApiResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

@RestController
@RequestMapping("/test")
@Validated
public class TestController {

// 使用断言工具类
@GetMapping("/assert")
public ApiResponse<String> testAssert(@RequestParam String param) {
AssertUtil.notBlank(param, ErrorCode.PARAM_ERROR, "参数不能为空");
return ApiResponse.success("参数有效: " + param);
}

// 抛出业务异常
@GetMapping("/business-exception")
public ApiResponse<String> testBusinessException() {
throw new BusinessException(ErrorCode.USER_NOT_FOUND, "用户ID: 123");
}

// 返回统一响应
@GetMapping("/response")
public ApiResponse<UserVO> testResponse() {
UserVO user = new UserVO(1L, "张三", "zhangsan@example.com");
return ApiResponse.success(user);
}

// 参数校验异常
@PostMapping("/validation")
public ApiResponse<String> testValidation(@Valid @RequestBody UserDTO userDTO) {
return ApiResponse.success("参数校验通过");
}

// 内部类示例
public static class UserVO {
private Long id;
private String username;
private String email;
// 构造函数、getter、setter省略
}

public static class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;

@NotBlank(message = "邮箱不能为空")
private String email;
// getter、setter省略
}
}

使用规范

1. 错误码命名规范

  • 模块标识 + 错误序号
  • 4位模块标识 + 4位错误序号
  • 示例:USER-0001(用户模块第1个错误)

2. 异常处理规范

  • 业务异常使用 BusinessException
  • 参数校验使用断言工具类
  • 系统异常由全局异常处理器统一处理

3. 响应格式规范

  • 所有接口返回 ApiResponse<T>
  • 成功使用 ApiResponse.success(data)
  • 失败使用 ApiResponse.error(errorCode)

4. 日志记录规范

  • 业务异常记录WARN级别日志
  • 系统异常记录ERROR级别日志
  • 参数异常记录WARN级别日志

测试验证

启动项目后,可以通过以下接口测试:

  • GET /test/assert?param=hello - 测试断言工具
  • GET /test/business-exception - 测试业务异常
  • GET /test/response - 测试统一响应
  • POST /test/validation - 测试参数校验
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
This site is deployed on
Unique Visitor Page View