SpringBoot统一异常管理方案
一、概述
- 本文档提供了一套完整的统一异常处理和响应体封装机制,包含以下核心组件:
- 统一响应体 (Result) - 用于接口响应的统一格式
- 错误码接口 (ErrorCode) - 错误码标准接口
- 认证授权错误码枚举 (AuthErrorCodeEnum) - 登录、认证相关的错误码
- 业务异常类 (BusinessException) - 自定义业务异常基类
- 全局异常处理器 (GlobalExceptionHandler) - 统一处理所有异常
1. 统一响应体 (Result)
- 基本结构:
{
"code": 200, // 状态码
"message": "操作成功", // 响应消息
"data": {}, // 响应数据(可选)
"timestamp": 1234567890 // 时间戳
}
- 使用示例:
// 成功响应(无数据)
return Result.success();
// 成功响应(有数据)
return Result.success(userInfoDTO);
// 成功响应(自定义消息)
return Result.success("查询成功", dataList);
// 失败响应(默认)
return Result.error();
// 失败响应(自定义消息)
return Result.error("操作失败");
// 失败响应(自定义状态码和消息)
return Result.error(400, "参数错误");
// 失败响应(使用错误码枚举)
return Result.error(AuthErrorCodeEnum.TOKEN_EXPIRED);
2. 错误码接口 (ErrorCode)
- 所有错误码枚举都需要实现此自定义异常信息接口:
public interface ErrorCode {
Integer getCode();
String getMessage();
}
3. 业务异常类 (BusinessException)
- 使用方式:
// 方式1: 仅使用消息
throw new BusinessException("用户不存在");
// 方式2: 使用状态码和消息
throw new BusinessException(400, "参数错误");
// 方式3: 使用错误码枚举
throw new BusinessException(AuthErrorCode.TOKEN_EXPIRED);
// 方式4: 使用错误码枚举和自定义消息
throw new BusinessException(AuthErrorCode.LOGIN_FAILED, "用户名或密码错误");
// 方式5: 使用消息和原始异常
throw new BusinessException("系统异常", e);
// 方式6: 使用完整参数
throw new BusinessException(500, "系统异常", e);
4. 全局异常处理器 (GlobalExceptionHandler)
- 支持的异常类型:
| 异常类型 | HTTP状态码 | 说明 |
|---|---|---|
BusinessException |
200 | 业务异常 |
MethodArgumentNotValidException |
400 | 参数校验异常(@Valid) |
BindException |
400 | 参数绑定异常 |
ConstraintViolationException |
400 | 约束校验异常(@Validated) |
MissingServletRequestParameterException |
400 | 缺少请求参数 |
MethodArgumentTypeMismatchException |
400 | 参数类型不匹配 |
HttpMessageNotReadableException |
400 | 请求体不可读 |
HttpRequestMethodNotSupportedException |
405 | 请求方法不支持 |
NoHandlerFoundException |
404 | 404异常 |
MaxUploadSizeExceededException |
400 | 文件上传超限 |
NullPointerException |
500 | 空指针异常 |
IllegalArgumentException |
400 | 非法参数异常 |
IllegalStateException |
500 | 非法状态异常 |
RuntimeException |
500 | 运行时异常 |
Exception |
500 | 其他所有异常 |
5. 工作原理
- 当发生异常时,
@RestControllerAdvice注解的GlobalExceptionHandler会自动捕获 - 根据异常类型,调用对应的
@ExceptionHandler方法 - 方法返回统一的
Result对象 - 所有异常都会记录到日志中,便于排查问题
二、 状态码设计原则与使用规范
1. 为什么使用两套状态码?
-
本项目采用了 HTTP状态码 + 响应体状态码 的双重状态码机制,这不是冗余设计,而是基于以下考虑:
-
职责分离原则:
-
HTTP状态码:表示HTTP协议层面的请求成功与否
-
响应体状态码:表示业务逻辑层面的具体错误类型
-
-
符合RESTful最佳实践:
-
HTTP状态码是HTTP协议标准的一部分
-
中间件、网关、代理可以根据HTTP状态码进行通用处理
-
浏览器、CDN、负载均衡器等基础设施可以基于HTTP状态码优化行为
-
-
便于监控和日志分析:
-
HTTP状态码可以快速判断请求在传输层的成功与否
-
响应体状态码可以精确识别具体的业务错误
-
-
前端开发友好:
-
HTTP状态码用于快速判断请求是否成功(如:401需要重新登录,403需要提示权限不足)
-
响应体状态码用于处理具体的业务逻辑错误
-
2. 两套状态码的职责划分
- HTTP状态码的职责:
| HTTP状态码 | 含义 | 适用场景 |
|---|---|---|
| 200 (OK) | 请求成功处理 | HTTP请求本身成功,无论业务结果如何 |
| 400 (Bad Request) | 客户端请求错误 | 参数校验失败、参数格式错误、参数类型不匹配等 |
| 401 (Unauthorized) | 未认证 | 缺少认证信息或认证信息无效 |
| 403 (Forbidden) | 无权限 | 认证成功但权限不足 |
| 404 (Not Found) | 资源不存在 | 请求的URL不存在 |
| 405 (Method Not Allowed) | 请求方法不支持 | 使用了不正确的HTTP方法 |
| 500 (Internal Server Error) | 服务端内部错误 | 系统异常、空指针异常等未预期的错误 |
核心原则:HTTP状态码表示请求在HTTP层面的处理结果,不是业务层面的结果。
- 响应体状态码的职责:
| 响应体状态码 | 含义 | 说明 |
|---|---|---|
| 200 | 业务成功 | 业务操作成功完成 |
| 400 | 参数错误 | 参数校验失败、参数格式错误等 |
| 404 | 资源不存在 | 请求的业务资源不存在 |
| 405 | 方法不支持 | 请求方法不支持 |
| 1001-1999 | 认证授权错误 | 未登录、令牌过期、权限不足等 |
| 2000-2999 | 用户相关错误 | 用户不存在、用户已存在等 |
| 3000-3999 | 订单相关错误 | 订单不存在、订单状态错误等 |
| 4000-4999 | 支付相关错误 | 支付失败、支付超时等 |
| 5000-9999 | 其他业务错误 | 其他自定义业务场景 |
核心原则:响应体状态码表示业务逻辑层面的具体错误类型,便于前端精确处理。
3. 具体使用规则
- 业务异常 (BusinessException):
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
return Result.error(e.getCode(), e.getMessage());
}
-
为什么使用 HTTP 200?
-
业务异常是预期的、可控的业务逻辑结果
-
HTTP请求本身是成功的(服务器正常接收并处理了请求)
-
例如:用户不存在、余额不足、库存不足等,这些都是正常的业务场景
-
使用 HTTP 200 不会误导中间件和监控系统
-
-
响应体状态码:
-
使用自定义的业务错误码(如 1001、1002 等)
-
前端根据响应体状态码判断具体的业务错误
- 示例:
-
// HTTP Status: 200
{
"code": 1012,
"message": "用户名不存在",
"data": null,
"timestamp": 1706107291000
}
- 参数校验异常:
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
return Result.error(400, errorMessage);
}
-
为什么使用 HTTP 400?
-
参数错误是客户端的错误,不是服务端的错误
-
客户端需要修正请求参数后重新请求
-
HTTP 400 明确告知中间件和代理这是客户端错误
-
便于CDN、负载均衡器等进行错误统计和监控
-
-
响应体状态码:
-
使用 400 表示参数错误
-
与HTTP状态码保持一致,便于理解
- 示例:
-
// HTTP Status: 400
{
"code": 400,
"message": "用户名不能为空; 密码长度必须在6-20位之间",
"data": null,
"timestamp": 1706107291000
}
- 系统异常:
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleException(Exception e, HttpServletRequest request) {
return Result.error(500, "系统内部错误,请联系管理员");
}
-
为什么使用 HTTP 500?
-
系统异常是未预期的、服务端的错误
-
表示服务器内部发生了异常情况
-
需要运维人员介入处理
-
监控系统可以基于 HTTP 500 触发告警
-
-
响应体状态码:
-
使用 500 表示系统错误
-
前端可以统一处理所有 500 错误,显示友好的错误提示
- 示例:
-
// HTTP Status: 500
{
"code": 500,
"message": "系统内部错误,请联系管理员",
"data": null,
"timestamp": 1706107291000
}
4. 设计的合理性和优势
-
优势1:符合RESTful规范:
- HTTP状态码是HTTP协议的一部分,遵循标准可以让系统更容易被理解和集成。例如:
-
网关/中间件:可以根据HTTP状态码进行通用处理
- 401 → 跳转到登录页
- 403 → 返回权限不足提示
- 500 → 触发告警通知
-
CDN/缓存:
- 只缓存 200 响应
- 不缓存 4xx 和 5xx 响应
-
负载均衡器:
- 根据 500 比例判断后端健康状态
- 自动摘除不健康的实例
-
优势2:便于监控和运维:
- 监控系统可以基于HTTP状态码进行统计分析:
# Prometheus 示例
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
annotations:
summary: "服务端错误率过高"
-
优势3:前后端开发友好:
-
前端开发:
// 基于 HTTP 状态码的快速判断 if (response.status === 401) { // 跳转到登录页 redirectToLogin(); } else if (response.status === 403) { // 提示权限不足 showMessage('权限不足'); } else { // 解析响应体中的业务状态码 const { code, message } = response.data; if (code === 1012) { showMessage('用户名不存在'); } } -
后端开发:
// 代码简洁,职责清晰 throw new BusinessException(AuthErrorCodeEnum.USERNAME_NOT_EXIST); // 不需要关心 HTTP 状态码,由全局异常处理器统一处理
-
-
优势4:降低误判风险:
-
如果统一使用 HTTP 200:
// 不推荐的方案 // HTTP Status: 200 (所有请求都是200) { "code": 500, "message": "空指针异常" } -
问题:
-
监控系统无法准确识别服务端错误
-
网关无法基于状态码进行重试或熔断
-
CDN 无法区分成功和失败响应,可能导致错误内容被缓存
-
无法快速定位问题(所有请求都显示200)
-
-
5. 常见疑问解答
Q1: 为什么不统一使用 HTTP 200,只看响应体状态码?
-
A: 不推荐,原因如下:
-
违反RESTful规范:HTTP状态码有明确的语义,应该正确使用
-
监控系统失效:无法准确统计服务端错误率
-
中间件无法工作:网关、代理等无法基于状态码做通用处理
-
缓存策略失效:CDN可能缓存错误响应
-
问题定位困难:所有请求都显示200,无法快速识别失败请求
Q2: BusinessException 为什么返回 HTTP 200?
-
A: 因为业务异常是预期的业务逻辑结果,不是系统错误:
-
用户不存在 → 正常业务场景,不是错误
-
余额不足 → 正常业务场景,不是错误
-
库存不足 → 正常业务场景,不是错误
- 这些情况下,HTTP请求本身是成功的,服务器正常处理了请求,只是业务逻辑返回了一个特定的结果。
-
-
类比:
-
就像银行查询余额,余额为0是一个正常的业务结果,不是系统错误
-
就像购物车结算,库存不足是一个正常的业务结果,不是系统错误
-
Q3: 参数校验为什么要用 HTTP 400?
-
A: 因为参数错误是客户端的错误,需要客户端修正:
-
参数格式错误 → 客户端需要修正参数格式
-
参数类型错误 → 客户端需要修正参数类型
-
必填参数缺失 → 客户端需要补充必填参数
- 使用 HTTP 400 可以明确告知客户端这是客户端的错误,需要客户端修正后重新请求。
-
Q4: 这样不会让前端代码变复杂吗?
-
A: 不会,反而更清晰:
// 推荐的处理方式 axios.get('/api/user/123') .then(response => { // 这里只处理 HTTP 200 的响应 const { code, message, data } = response.data; if (code === 200) { // 业务成功 renderData(data); } else { // 业务失败(如用户不存在、余额不足等) showMessage(message); } }) .catch(error => { // 这里统一处理非 200 的 HTTP 响应 if (error.response.status === 401) { // 跳转到登录页 redirectToLogin(); } else if (error.response.status === 403) { // 提示权限不足 showMessage('权限不足'); } else if (error.response.status === 400) { // 参数错误 showMessage(error.response.data.message); } else if (error.response.status >= 500) { // 系统错误 showMessage('系统错误,请稍后重试'); } }); -
优势:
-
HTTP状态码用于快速判断请求成功与否和错误类型
-
响应体状态码用于处理具体的业务逻辑错误
-
职责清晰,代码可维护性强
-
Q5: 如果我想统一处理所有错误怎么办?
-
A: 可以在 axios 拦截器中统一处理:
// 响应拦截器 axios.interceptors.response.use( response => { // HTTP 200 响应 const { code, message } = response.data; // 业务错误统一处理 if (code !== 200) { showMessage(message); return Promise.reject(new Error(message)); } return response; }, error => { // 非 HTTP 200 响应 const { status, data } = error.response; if (status === 401) { redirectToLogin(); } else if (status === 403) { showMessage('权限不足'); } else if (status >= 500) { showMessage('系统错误,请稍后重试'); } return Promise.reject(error); } );
6. 最佳实践建议
-
严格遵守状态码映射规则:
-
业务异常 → HTTP 200 + 业务错误码
-
参数错误 → HTTP 400 + 400
-
系统异常 → HTTP 500 + 500
-
-
不要随意修改HTTP状态码:
-
不要为了省事统一使用 HTTP 200
-
不要随意将参数错误改为 HTTP 500
-
-
前端要正确处理两套状态码:
-
先判断 HTTP 状态码
-
再判断响应体状态码
-
-
文档要清晰说明:
-
在API文档中明确说明HTTP状态码和响应体状态码的含义
-
提供完整的错误码列表
-
-
监控要区分两套状态码:
-
基于 HTTP 状态码监控服务端健康状态
-
基于响应体状态码监控业务错误
-
7. 错误码分类
-
认证相关 (1001-1099):
-
UNAUTHORIZED(1001)- 未认证,请先登录 -
TOKEN_MISSING(1002)- 认证令牌缺失 -
TOKEN_INVALID(1003)- 认证令牌无效 -
TOKEN_EXPIRED(1004)- 认证令牌已过期 -
LOGIN_FAILED(1006)- 登录失败,用户名或密码错误 -
LOGIN_DISABLED(1007)- 账号已被禁用 -
LOGIN_LOCKED(1008)- 账号已被锁定 -
USERNAME_NOT_EXIST(1012)- 用户名不存在 -
USER_NOT_EXIST(1013)- 用户不存在 -
CAPTCHA_ERROR(1014)- 验证码错误
-
-
授权相关 (1101-1199):
-
FORBIDDEN(1101)- 无权限访问 -
PERMISSION_DENIED(1104)- 权限不足 -
RESOURCE_ACCESS_DENIED(1106)- 资源访问拒绝
-
-
用户相关 (1201-1299):
-
USER_ALREADY_EXIST(1201)- 用户已存在 -
PHONE_DUPLICATE(1203)- 手机号已被使用
-
-
令牌相关 (1301-1399):
-
TOKEN_PARSE_ERROR(1301)- 令牌解析失败 -
TOKEN_SIGNATURE_ERROR(1302)- 令牌签名错误
-
三、核心使用示例
1. Controller层 - 参数校验
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 用户登录
*/
@PostMapping("/login")
public Result<UserInfoDTO> login(@Valid @RequestBody LoginRequest request) {
UserInfoDTO userInfoDTO = userService.login(request);
return Result.success("登录成功", userInfoDTO);
}
/**
* 获取用户信息
*/
@GetMapping("/info/{id}")
public Result<UserInfoDTO> getUserInfo(@PathVariable Long id) {
UserInfoDTO userInfoDTO = userService.getUserInfo(id);
return Result.success(userInfoDTO);
}
}
2. Service层 - 异常抛出
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public UserInfoDTO login(LoginRequest request) {
// 查询用户
User user = userRepository.findByUsername(request.getUsername());
if (user == null) {
throw new BusinessException(AuthErrorCodeEnum.USERNAME_NOT_EXIST);
}
// 校验状态
if (!user.isEnabled()) {
throw new BusinessException(AuthErrorCodeEnum.LOGIN_DISABLED);
}
if (user.isLocked()) {
throw new BusinessException(AuthErrorCodeEnum.LOGIN_LOCKED);
}
log.info("用户登录成功: username={}", user.getUsername());
return UserInfoDTO.builder()
.id(user.getId())
.username(user.getUsername())
.build();
}
@Override
public UserInfoDTO getUserInfo(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException(AuthErrorCodeEnum.USER_NOT_EXIST));
return convertToDTO(user);
}
}
3. DTO参数校验
/**
* 登录请求
*/
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20位之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
4. 嵌套对象校验
@Data
public class UserUpdateRequest {
@NotNull(message = "用户ID不能为空")
private Long id;
@Size(max = 50, message = "昵称长度不能超过50位")
private String nickname;
@Email(message = "邮箱格式不正确")
private String email;
@Valid
@NotNull(message = "地址信息不能为空")
private AddressInfo address;
}
@Data
public class AddressInfo {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@Size(max = 200, message = "详细地址长度不能超过200位")
private String detail;
}
5. 自定义错误码指南
- 错误码分类规范建议:
| 模块 | 错误码范围 | 说明 |
|---|---|---|
| 认证授权 | 1001-1499 | 登录、权限、令牌相关 |
| 用户管理 | 1501-1599 | 用户增删改查相关 |
| 订单管理 | 2001-2099 | 订单业务相关 |
| 商品管理 | 2101-2199 | 商品业务相关 |
| 支付相关 | 3001-3099 | 支付业务相关 |
| 通用业务 | 4001-4999 | 其他业务场景 |
四、跨项目复用指南
1. 必需的Maven依赖
<!--Lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. 核心组件完整代码
- ErrorCode.java
package cn.cloud.common.exception;
/**
* 错误码接口
* 所有错误码枚举类都需要实现此接口
* @author cloud
* {@code @date} 2026-01-21
*/
public interface ErrorCode {
/**
* 获取错误码
*/
Integer getCode();
/**
* 获取错误信息
*/
String getMessage();
}
Result.java
package cn.cloud.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import cn.cloud.common.exception.ErrorCode;
import java.io.Serial;
import java.io.Serializable;
/**
* 统一响应结果封装类
* @param <T> 数据类型
* @author cloud
* {@code @date} 2026-01-21
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 时间戳
*/
private final Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public Result(Integer code, String message) {
this.code = code;
this.message = message;
this.timestamp = System.currentTimeMillis();
}
/**
* 成功响应(无数据)
*/
public static <T> Result<T> success() {
return new Result<>(200, "操作成功");
}
/**
* 成功响应(有数据)
*/
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
/**
* 成功响应(自定义消息)
*/
public static <T> Result<T> success(String message, T data) {
return new Result<>(200, message, data);
}
/**
* 成功响应(自定义消息,无数据)
*/
public static <T> Result<T> success(String message) {
return new Result<>(200, message);
}
/**
* 失败响应
*/
public static <T> Result<T> error() {
return new Result<>(500, "操作失败");
}
/**
* 失败响应(自定义消息)
*/
public static <T> Result<T> error(String message) {
return new Result<>(500, message);
}
/**
* 失败响应(自定义状态码和消息)
*/
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message);
}
/**
* 失败响应(使用错误码枚举)
*/
public static <T> Result<T> error(ErrorCode errorCode) {
return new Result<>(errorCode.getCode(), errorCode.getMessage());
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
", timestamp=" + timestamp +
'}';
}
}
BusinessException.java
package cn.cloud.common.exception;
import lombok.Getter;
import java.io.Serial;
/**
* 业务异常基类
*
* @author cloud
* {@code @date} 2026-01-21
*/
@Getter
public class BusinessException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final Integer code;
/**
* 错误信息
*/
private final String message;
public BusinessException(String message) {
super(message);
this.code = 500;
this.message = message;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
this.message = message;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = 500;
this.message = message;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
}
GlobalExceptionHandler.java
package cn.cloud.common.exception;
import cn.cloud.common.response.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.stream.Collectors;
/**
* 全局异常处理器
* @author cloud
* {@code @date} 2026-01-21
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.error("业务异常: URI={}, Code={}, Message={}", request.getRequestURI(), e.getCode(), e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常(@Valid 注解)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.error("参数校验异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(400, errorMessage);
}
/**
* 处理参数绑定异常
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleBindException(BindException e, HttpServletRequest request) {
String errorMessage = e.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.error("参数绑定异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(400, errorMessage);
}
/**
* 处理参数校验异常(@Validated 注解)
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
log.error("约束校验异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(400, errorMessage);
}
/**
* 处理缺少请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
String errorMessage = String.format("缺少必需的请求参数: %s", e.getParameterName());
log.error("缺少请求参数异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(400, errorMessage);
}
/**
* 处理参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
String errorMessage = String.format("参数类型不匹配: %s", e.getName());
log.error("参数类型不匹配异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(400, errorMessage);
}
/**
* 处理请求体不可读异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
log.error("请求体不可读异常: URI={}, Message={}", request.getRequestURI(), e.getMessage());
return Result.error(400, "请求体格式错误或不可读");
}
/**
* 处理请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Result<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
String errorMessage = String.format("不支持的请求方法: %s", e.getMethod());
log.error("请求方法不支持异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(405, errorMessage);
}
/**
* 处理404异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<?> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
String errorMessage = String.format("请求的资源不存在: %s %s", e.getHttpMethod(), e.getRequestURL());
log.error("404异常: URI={}, Message={}", request.getRequestURI(), errorMessage);
return Result.error(404, "请求的资源不存在");
}
/**
* 处理文件上传大小超出限制异常
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMaxUploadSizeExceededException(HttpServletRequest request) {
log.error("文件上传大小超出限制: URI={}", request.getRequestURI());
return Result.error(400, "上传文件大小超出限制");
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
log.error("空指针异常: URI={}", request.getRequestURI(), e);
return Result.error(500, "系统内部错误: 空指针异常");
}
/**
* 处理非法参数异常
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
log.error("非法参数异常: URI={}, Message={}", request.getRequestURI(), e.getMessage());
return Result.error(400, e.getMessage());
}
/**
* 处理非法状态异常
*/
@ExceptionHandler(IllegalStateException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleIllegalStateException(IllegalStateException e, HttpServletRequest request) {
log.error("非法状态异常: URI={}, Message={}", request.getRequestURI(), e.getMessage());
return Result.error(500, e.getMessage());
}
/**
* 处理运行时异常
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("运行时异常: URI={}, Message={}", request.getRequestURI(), e.getMessage(), e);
return Result.error(500, "系统运行时异常: " + e.getMessage());
}
/**
* 处理其他所有异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleException(Exception e, HttpServletRequest request) {
log.error("系统异常: URI={}", request.getRequestURI(), e);
return Result.error(500, "系统内部错误,请联系管理员");
}
}
AuthErrorCodeEnum.java
package cn.cloud.common.enums;
import cn.cloud.common.exception.ErrorCode;
/**
* 认证授权相关错误码枚举
*
* @author cloud
* {@code @date} 2026-01-21
*/
public enum AuthErrorCodeEnum implements ErrorCode {
// ========== 认证相关 (1001-1099) ==========
UNAUTHORIZED(1001, "未认证,请先登录"),
TOKEN_MISSING(1002, "认证令牌缺失"),
TOKEN_INVALID(1003, "认证令牌无效"),
TOKEN_EXPIRED(1004, "认证令牌已过期"),
TOKEN_REFRESH_FAILED(1005, "令牌刷新失败"),
LOGIN_FAILED(1006, "登录失败,用户名或密码错误"),
LOGIN_DISABLED(1007, "账号已被禁用"),
LOGIN_LOCKED(1008, "账号已被锁定"),
LOGIN_EXPIRED(1009, "账号已过期"),
PASSWORD_ERROR(1010, "密码错误"),
PASSWORD_EXPIRED(1011, "密码已过期,请修改密码"),
USERNAME_NOT_EXIST(1012, "用户名不存在"),
USER_NOT_EXIST(1013, "用户不存在"),
CAPTCHA_ERROR(1014, "验证码错误"),
CAPTCHA_EXPIRED(1015, "验证码已过期"),
CAPTCHA_RATE_LIMIT_EXCEEDED(1016, "验证码请求过于频繁,请稍后再试"),
LOGIN_TIMEOUT(1017, "登录超时"),
TOO_MANY_LOGIN_ATTEMPTS(1018, "登录尝试次数过多,请稍后再试"),
DEVICE_NOT_TRUSTED(1019, "设备未受信任"),
IP_NOT_ALLOWED(1020, "IP地址不在允许范围内"),
SESSION_INVALID(1021, "会话无效"),
SESSION_EXPIRED(1022, "会话已过期"),
// ========== 授权相关 (1101-1199) ==========
FORBIDDEN(1101, "无权限访问"),
ROLE_NOT_EXIST(1102, "角色不存在"),
ROLE_DISABLED(1103, "角色已被禁用"),
PERMISSION_DENIED(1104, "权限不足"),
RESOURCE_NOT_FOUND(1105, "资源不存在"),
RESOURCE_ACCESS_DENIED(1106, "资源访问拒绝"),
OPERATION_NOT_ALLOWED(1107, "不允许执行此操作"),
DATA_ACCESS_DENIED(1108, "数据访问权限不足"),
CROSS_TENANT_ACCESS(1109, "跨租户访问被拒绝"),
// ========== 用户相关 (1201-1299) ==========
USER_ALREADY_EXIST(1201, "用户已存在"),
USER_NAME_DUPLICATE(1202, "用户名已存在"),
PHONE_DUPLICATE(1203, "手机号已被使用"),
EMAIL_DUPLICATE(1204, "邮箱已被使用"),
USER_INFO_INCOMPLETE(1205, "用户信息不完整"),
USER_STATUS_ABNORMAL(1206, "用户状态异常"),
// ========== 令牌相关 (1301-1399) ==========
TOKEN_PARSE_ERROR(1301, "令牌解析失败"),
TOKEN_SIGNATURE_ERROR(1302, "令牌签名错误"),
TOKEN_MALFORMED(1303, "令牌格式错误"),
TOKEN_UNSUPPORTED(1304, "不支持的令牌类型"),
REFRESH_TOKEN_INVALID(1305, "刷新令牌无效"),
REFRESH_TOKEN_EXPIRED(1306, "刷新令牌已过期"),
// ========== 其他认证相关 (1401-1499) ==========
AUTHENTICATION_FAILED(1401, "认证失败"),
AUTHENTICATION_TIMEOUT(1402, "认证超时"),
AUTHENTICATION_INTERRUPTED(1403, "认证被中断"),
AUTHENTICATION_ERROR(1404, "认证发生错误"),
SSO_AUTH_FAILED(1405, "单点登录认证失败"),
SSO_TOKEN_INVALID(1406, "单点登录令牌无效"),
SSO_SERVER_ERROR(1407, "单点登录服务异常"),
SYSTEM_ERROR(1408, "系统错误");
/**
* 错误码
*/
private final Integer code;
/**
* 错误信息
*/
private final String message;
AuthErrorCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
3. 初始化注意事项
- 包名替换:将所有文件中的包名
cn.cloud替换为你项目的包名 - 扫描路径:确保
@SpringBootApplication或@ComponentScan能扫描到这些组件 - 错误码规划:根据业务规划错误码范围,避免与现有错误码冲突
- 日志级别:生产环境建议将异常处理器日志级别设置为
WARN或ERROR
4. 常见问题 (FAQ)
Q1: 如何添加新的错误码枚举?
A: 参照 AuthErrorCodeEnum 创建新枚举类,实现 ErrorCode 接口:
public enum YourBusinessErrorCode implements ErrorCode {
ERROR_CODE(3001, "错误描述"),
ANOTHER_ERROR(3002, "另一个错误");
private final Integer code;
private final String message;
YourBusinessErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
Q2: 如何自定义异常处理逻辑?
A: 在 GlobalExceptionHandler 中添加新的异常处理方法:
@ExceptionHandler(CustomException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleCustomException(CustomException e, HttpServletRequest request) {
log.error("自定义异常: URI={}, Message={}", request.getRequestURI(), e.getMessage());
return Result.error(400, e.getMessage());
}
Q3: 生产环境如何避免敏感信息泄露?
A: 配置日志级别:
# application-prod.yml
logging:
level:
com.yourproject.common.exception: error
或在 GlobalExceptionHandler 中根据环境返回不同的错误消息:
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e, HttpServletRequest request) {
String message = isProd() ? "系统内部错误" : e.getMessage();
log.error("系统异常: URI={}", request.getRequestURI(), e);
return Result.error(500, message);
}
Q4: 嵌套对象的参数校验不生效?
A: 确保在嵌套对象字段上添加 @Valid 注解:
@Data
public class ParentRequest {
@Valid // 必须添加
@NotNull
private ChildInfo child;
}
@Data
public class ChildInfo {
@NotBlank
private String name;
}
五、最佳实践
-
使用错误码枚举:优先使用预定义的错误码枚举,保持错误信息的一致性
-
异常链传递:在捕获异常重新抛出时,保留原始异常信息
-
详细日志:全局异常处理器会自动记录异常日志,包含请求URI等上下文信息
-
参数校验:使用
@Valid或@Validated进行参数校验,避免手动校验 -
统一响应:所有接口返回值使用
Result包装,确保响应格式统一
六、注意事项
-
确保已添加 Lombok 依赖(已在 pom.xml 中配置)
-
确保已添加 Spring Validation 依赖(已在 pom.xml 中配置)
-
如需使用 Spring Security 相关异常处理,请添加 Spring Security 依赖
-
生产环境中,建议配置日志级别,避免敏感信息泄露

浙公网安备 33010602011771号