外观
Spring Boot 异常处理
约 1855 字大约 6 分钟
2025-08-16
一、为什么需要专业异常处理
在项目开发中,直接抛出原始异常存在明显问题:
- 前端无法统一解析异常信息
- 敏感信息可能暴露给客户端(如堆栈信息)
- 不同异常处理逻辑分散在各处,维护困难
- 用户体验差(直接看到500错误)
而 Spring Boot 的异常处理机制能够:
- 统一响应格式:前后端约定一致的数据结构
- 隐藏敏感信息:生产环境不暴露堆栈细节
- 集中管理异常:一处定义,全局生效
- 提供友好提示:给用户明确的错误指引
二、核心注解:@ControllerAdvice vs @RestControllerAdvice
1. 两者区别
| 特性 | @ControllerAdvice | @RestControllerAdvice |
|---|---|---|
| 用途 | 传统 MVC 应用 | RESTful API 应用 |
| 返回值 | 需要 @ResponseBody | 默认 JSON 响应 |
| 使用场景 | 前后端不分离 | 前后端分离(推荐) |
| 本质 | MVC 异常处理 | REST 异常处理 |
2. 基本用法
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return Result.error("服务器内部错误");
}
}最佳实践:对于 RESTful API 服务,优先使用
@RestControllerAdvice
三、构建统一的响应结构
1. 响应对象设计
@Data
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("成功");
result.setData(data);
return result;
}
public static Result error(String message) {
return error(500, message);
}
public static Result error(int code, String message) {
Result result = new Result();
result.setCode(code);
result.setMessage(message);
return result;
}
}2. 错误码规范(推荐)
| 错误码 | 含义 | 场景 |
|---|---|---|
| 200 | 成功 | 正常响应 |
| 400 | 参数错误 | 客户端传参错误 |
| 401 | 未授权 | 未登录或token失效 |
| 403 | 禁止访问 | 无权限操作 |
| 404 | 资源不存在 | URL错误或资源被删除 |
| 500 | 服务器错误 | 服务端异常 |
| 1000+ | 业务错误 | 自定义业务异常 |
四、自定义异常体系
1. 定义错误码接口
public interface IErrorCode {
int getCode();
String getMessage();
}2. 实现具体错误码
public enum ErrorCode implements IErrorCode {
// 通用错误
FAILED(500, "操作失败"),
VALIDATE_FAILED(400, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限"),
// 业务错误
USER_NOT_FOUND(1001, "用户不存在"),
PASSWORD_ERROR(1002, "密码错误"),
ACCOUNT_LOCKED(1003, "账号已被锁定");
private int code;
private String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}3. 自定义业务异常
public class BusinessException extends RuntimeException {
private IErrorCode errorCode;
public BusinessException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BusinessException(String message) {
super(message);
}
public BusinessException(Throwable cause) {
super(cause);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}4. 断言工具类(简化异常抛出)
public class Asserts {
public static void isTrue(boolean condition, String message) {
if (!condition) {
throw new BusinessException(message);
}
}
public static void isTrue(boolean condition, IErrorCode errorCode) {
if (!condition) {
throw new BusinessException(errorCode);
}
}
public static void notNull(Object object, IErrorCode errorCode) {
if (object == null) {
throw new BusinessException(errorCode);
}
}
}五、全局异常处理器实战
1. 基础异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
if (e.getErrorCode() != null) {
return Result.error(e.getErrorCode().getCode(), e.getMessage());
}
return Result.error(e.getMessage());
}
/**
* 处理参数验证异常 (JSR-303)
*/
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
public Result handleValidationException(Exception e) {
String message = "参数验证失败";
if (e instanceof BindException) {
message = ((BindException) e).getBindingResult().getFieldError().getDefaultMessage();
} else if (e instanceof MethodArgumentNotValidException) {
message = ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage();
}
log.warn("参数验证异常: {}", message);
return Result.error(400, message);
}
/**
* 处理其他所有异常
*/
@ExceptionHandler(Exception.class)
public Result handleOtherException(Exception e) {
log.error("系统异常: ", e);
return Result.error("服务器内部错误,请联系管理员");
}
}2. 精细化异常处理(高级用法)
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理404异常
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result handle404(NoHandlerFoundException e) {
log.warn("资源不存在: {}", e.getRequestURL());
return Result.error(404, "资源不存在");
}
// 处理405异常
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Result handle405(HttpRequestMethodNotSupportedException e) {
log.warn("请求方法不支持: {} [{}]", e.getMethod(), e.getSupportedHttpMethods());
return Result.error(405, "请求方法不支持");
}
// 处理空指针异常(开发环境可开启,生产环境建议关闭)
@ExceptionHandler(NullPointerException.class)
@ConditionalOnProperty(name = "dev.exception.npe", havingValue = "true")
public Result handleNPE(NullPointerException e) {
log.error("空指针异常: ", e);
return Result.error("空指针异常,请检查参数");
}
}六、常见场景处理
1. RESTful API 参数校验
Controller:
@PostMapping("/login")
public Result login(@Valid @RequestBody LoginRequest request) {
// 业务逻辑
}请求对象:
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
private String password;
}注意:需要添加依赖
spring-boot-starter-validation
2. 业务逻辑校验
@Service
public class UserService {
public User getUserById(Long id) {
User user = userMapper.selectById(id);
Asserts.notNull(user, ErrorCode.USER_NOT_FOUND);
return user;
}
public void changePassword(String oldPassword, String newPassword) {
// 验证旧密码
boolean isMatch = passwordEncoder.matches(oldPassword, currentUser.getPassword());
Asserts.isTrue(isMatch, ErrorCode.PASSWORD_ERROR);
// 验证新密码
Asserts.isTrue(newPassword.length() >= 6, "新密码长度不能少于6位");
// 更新密码
// ...
}
}3. 多环境差异化处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Value("${spring.profiles.active}")
private String activeProfile;
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
if ("dev".equals(activeProfile)) {
// 开发环境返回详细错误信息
return Result.error(500, e.getMessage(), ExceptionUtils.getStackTrace(e));
} else {
// 生产环境隐藏详细错误
if (e instanceof BusinessException) {
return Result.error(500, e.getMessage());
}
return Result.error(500, "服务器内部错误");
}
}
}七、实用技巧
1. 异常分类与日志记录
// 业务异常使用warn级别,减少日志噪音
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.warn("业务异常 [{}]: {}", e.getErrorCode().getCode(), e.getMessage());
return Result.error(e.getErrorCode().getCode(), e.getMessage());
}
// 系统异常使用error级别
@ExceptionHandler(Exception.class)
public Result handleOtherException(Exception e) {
log.error("系统异常: ", e);
return Result.error("服务器内部错误");
}2. 异常信息本地化
// 根据请求头中的语言返回不同语言的错误信息
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(HttpServletRequest request, BusinessException e) {
String lang = request.getHeader("Accept-Language");
String message = e.getMessage();
if ("zh-CN".equals(lang)) {
message = translateToChinese(e.getMessage());
} else if ("en-US".equals(lang)) {
message = translateToEnglish(e.getMessage());
}
return Result.error(e.getErrorCode().getCode(), message);
}3. 异常监控与告警
// 当发生严重异常时触发告警
@ExceptionHandler(Exception.class)
public Result handleOtherException(Exception e) {
// 记录错误日志
log.error("系统异常: ", e);
// 触发告警(可集成企业微信、钉钉等)
alarmService.sendExceptionAlert(e);
return Result.error("服务器内部错误");
}八、最佳实践总结
1. 分层异常处理
| 层级 | 处理异常类型 | 处理方式 |
|---|---|---|
| Controller | 参数验证异常 | 全局捕获,统一返回 |
| Service | 业务异常 | 抛出自定义异常 |
| Repository | 数据访问异常 | 转换为业务异常或静默处理 |
2. 异常处理原则
- 不吞异常:不要捕获异常后不做任何处理
- 不暴露细节:生产环境不要返回堆栈信息
- 分类处理:业务异常与系统异常区别对待
- 日志分级:业务异常用 warn,系统异常用 error
- 统一格式:确保所有接口返回一致的响应结构
3. 配置建议
# application.yml
# 开启Spring Boot默认的错误处理
server:
error:
whitelabel:
enabled: false # 禁用默认错误页面,使用自定义异常处理
# 参数验证配置
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false九、常见问题
1. 如何处理全局未捕获异常?
通过 @ControllerAdvice 已经可以捕获几乎所有异常,包括:
- Controller 层抛出的异常
- Service 层抛出的未处理异常
- 参数绑定异常
- 404/405 等 HTTP 错误
2. 如何自定义 404 页面?
在 resources/templates/error/ 目录下创建 404.html 文件,Spring Boot 会自动识别。
3. 如何处理异步请求异常?
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("异步方法异常: {}#{}",
method.getDeclaringClass().getSimpleName(),
method.getName(), ex);
};
}
}4. 如何处理 Spring Security 认证异常?
@RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public Result handleAccessDeniedException() {
return Result.error(403, "没有访问权限");
}
@ExceptionHandler(AuthenticationException.class)
public Result handleAuthenticationException() {
return Result.error(401, "认证失败");
}
}