在企业级 Spring Boot 开发中,全局异常处理是系统健壮性的核心保障。 传统 try-catch 硬编码存在代码冗余、业务与异常逻辑耦合、响应格式混乱、堆栈信息泄露等致命问题。Spring Boot 提供的注解式全局异常处理方案,可以完美解决这些问题,实现:
逻辑解耦:业务代码只关注核心逻辑,异常统一集中处理
响应标准化:前后端约定统一的错误格式,降低对接成本
安全隔离:屏蔽底层异常堆栈,向用户返回友好提示
日志规范化:统一记录异常日志,方便问题排查
Spring 全局异常处理依赖两个核心注解,是实现功能的基础:
@RestControllerAdvice
组合注解:@ControllerAdvice + @ResponseBody
作用:全局捕获所有 Controller 层抛出的异常,并直接返回 JSON 格式响应
范围:默认拦截所有包下的控制器,可通过 basePackages 指定扫描范围
@ExceptionHandler
作用:标注在方法上,指定需要捕获的异常类型
优先级:精确匹配异常 > 父类异常(如先捕获BusinessException,再捕获Exception)
企业级规范
禁止硬编码错误码(如 4001、500),企业开发必须使用枚举类统一管理所有错误码,便于维护和前端对接:
package com.example.demo.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 全局错误码枚举
* 约定:
* 200 = 成功
* 4xxx = 客户端异常(参数错误、资源不存在)
* 5xxx = 服务端异常(系统错误、未知异常)
*/
@Getter
@AllArgsConstructor
public enum ErrorCode {
// 成功
SUCCESS(200, "操作成功"),
// 客户端异常
PARAM_ERROR(4000, "参数格式错误"),
RESOURCE_NOT_FOUND(4001, "资源不存在"),
// 服务端异常
SERVER_ERROR(5000, "服务器繁忙,请稍后再试"),
UNKNOWN_ERROR(5001, "未知异常");
private final Integer code;
private final String msg;
}基于枚举封装标准化响应体,同时支持成功 / 失败两种场景,前后端通用:
package com.example.demo.common;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Result<T> {
private Integer code; // 响应码
private String message; // 响应信息
private T data; // 响应数据
// 1. 成功响应(带数据)
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ErrorCode.SUCCESS.getCode());
result.setMessage(ErrorCode.SUCCESS.getMsg());
result.setData(data);
return result;
}
// 2. 成功响应(无数据)
public static <T> Result<T> success() {
return success(null);
}
// 3. 失败响应(自定义错误码+提示语)
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
// 4. 失败响应(使用枚举)
public static <T> Result<T> error(ErrorCode errorCode) {
return error(errorCode.getCode(), errorCode.getMsg());
}
}继承 RuntimeException(非受检异常,无需手动 try-catch),绑定错误码枚举,适配业务场景:
package com.example.demo.exception;
import com.example.demo.common.ErrorCode;
import lombok.Getter;
/**
* 自定义业务异常(所有业务错误都抛出此异常)
*/
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
private final String message;
// 自定义错误码+信息
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
// 使用枚举
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMsg());
this.code = errorCode.getCode();
this.message = errorCode.getMsg();
}
}捕获所有常见异常(业务异常、参数校验、系统异常、请求异常等),统一处理并记录日志,是企业开发标准配置:
package com.example.demo.handler;
import com.example.demo.common.ErrorCode;
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.util.Objects;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.example.demo") // 指定扫描包,缩小范围
public class GlobalExceptionHandler {
// ====================== 1. 自定义业务异常(优先处理) ======================
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.warn("【业务异常】code:{},message:{}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// ====================== 2. 参数校验异常(@Valid 校验失败) ======================
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidException(MethodArgumentNotValidException e) {
// 获取校验失败的提示信息
String message = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
log.warn("【参数校验异常】message:{}", message);
return Result.error(ErrorCode.PARAM_ERROR.getCode(), message);
}
// ====================== 3. 参数类型转换异常 ======================
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public Result<?> handleTypeMismatchException(MethodArgumentTypeMismatchException e) {
String message = "参数类型错误:" + e.getName();
log.warn("【参数类型异常】message:{}", message);
return Result.error(ErrorCode.PARAM_ERROR.getCode(), message);
}
// ====================== 4. 请求方法不支持异常 ======================
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<?> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
String message = "请求方法错误,支持:" + e.getSupportedHttpMethods();
log.warn("【请求方法异常】message:{}", message);
return Result.error(ErrorCode.PARAM_ERROR.getCode(), message);
}
// ====================== 5. 空指针异常 ======================
@ExceptionHandler(NullPointerException.class)
public Result<?> handleNullPointerException(NullPointerException e) {
log.error("【空指针异常】", e);
return Result.error(ErrorCode.SERVER_ERROR);
}
// ====================== 6. 兜底:所有未知异常(最后处理) ======================
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("【系统未知异常】", e);
return Result.error(ErrorCode.SERVER_ERROR);
}
}package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public Result<String> getUser(@PathVariable Long id) {
// 模拟业务:ID=100 抛出资源不存在异常
if (id == 100) {
throw new BusinessException(4001, "用户ID=" + id + "不存在");
}
return Result.success("用户信息:张三");
}
}package com.example.demo.service;
import com.example.demo.exception.BusinessException;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void checkUser(Long id) {
if (id == null) {
throw new BusinessException(4000, "用户ID不能为空");
}
}
}启动项目,测试不同异常场景,验证全局异常处理效果:
精确匹配的异常 > 父类异常 例:先捕获 BusinessException,再捕获 Exception,避免兜底异常覆盖业务异常。
自定义异常必须继承 RuntimeException(非受检异常),无需在方法上声明 throws
禁止继承 Exception(受检异常),会强制代码写 try-catch,违背解耦初衷
@RestControllerAdvice 默认拦截所有包的控制器
生产环境建议指定 basePackages,缩小扫描范围,提升性能
业务异常:log.warn()(警告级别,非严重错误)
系统异常:log.error()(错误级别,必须打印堆栈)
禁止向客户端暴露堆栈信息,仅在控制台打印
统一错误码:所有错误码使用枚举管理,杜绝魔法值
分层抛异常:Service 层抛业务异常,Controller 层不处理异常
全覆盖异常:捕获参数校验、类型转换、请求方法、空指针等所有常见异常
安全提示:客户端只返回友好提示,堆栈信息仅记录在日志
参数校验:配合 @Valid 实现自动参数校验,异常统一处理
范围控制:指定异常处理器扫描包,避免不必要的性能开销
核心注解:@RestControllerAdvice(全局拦截)+ @ExceptionHandler(捕获指定异常)
三层架构:统一响应类 + 错误码枚举 + 自定义异常 + 全局处理器
核心价值:解耦业务与异常逻辑、标准化响应、安全隔离、日志规范化
企业标准:这是 Spring Boot 开发必写的基础配置,所有项目通用