在 SpringBoot 项目开发中,异常处理是容易被忽视但至关重要的环节。多数开发者习惯在业务代码中大量使用try-catch捕获异常,导致代码冗余、异常流转混乱、错误信息不统一,且难以快速定位问题。优秀的异常处理机制能实现 "业务与异常解耦、错误信息标准化、问题快速追溯",提升系统健壮性与可维护性。
本文从异常分层、全局异常处理器实现、自定义异常设计、日志规范等维度,讲解 SpringBoot 项目异常处理的完整方案,帮你告别 "到处 try-catch",打造规范、可扩展的异常处理体系。
一、核心认知:异常处理的价值与核心原则
1. 核心价值
- 代码解耦:业务逻辑与异常处理分离,减少重复编码(无需每个接口都写 try-catch);
- 响应统一:错误响应格式标准化,前端无需适配多种错误返回,降低协作成本;
- 问题追溯:异常日志规范记录,包含上下文信息(请求参数、堆栈信息),便于快速排查;
- 用户友好:返回清晰的错误提示,避免暴露敏感信息(如数据库密码、堆栈详情);
- 系统稳定:捕获未预料到的异常,避免服务崩溃,提供降级策略。
2. 核心设计原则
- 分层捕获:Controller 层捕获接口请求相关异常,Service 层捕获业务逻辑异常,DAO 层捕获数据访问异常,全局处理器兜底未捕获异常;
- 自定义异常优先:业务异常(如 "订单不存在""权限不足")用自定义异常表示,避免滥用系统异常;
- 日志分级:ERROR 级记录异常堆栈与上下文,WARN 级记录可容忍异常(如参数格式错误),INFO 级记录正常流转;
- 敏感信息屏蔽:错误响应中不返回堆栈信息、数据库地址等敏感内容,仅返回用户可理解的提示;
- 异常不落地:捕获异常后要么处理(修复、降级),要么抛出(交给上层处理),禁止捕获后不做任何操作。
3. 异常分层(SpringBoot 项目)
按业务分层划分异常范围,明确各层异常职责:
- 接口层异常:请求参数错误、HTTP 方法不支持、Token 失效、接口不存在等;
- 业务层异常:订单状态异常、库存不足、权限校验失败等(自定义异常为主);
- 数据层异常:数据库连接失败、SQL 语法错误、主键冲突等;
- 系统层异常:空指针、数组越界、内存溢出等(未预料到的系统异常,全局兜底)。
二、实战:构建规范的异常处理体系
1. 第一步:定义自定义异常(业务异常标准化)
自定义异常用于描述业务场景下的异常情况,包含错误码、错误提示,便于全局处理器统一处理。
(1)基础自定义异常类
java
运行
import lombok.Getter;
import org.springframework.http.HttpStatus;
/**
* 基础业务异常类,所有业务异常都继承此类
*/
@Getter
public class BusinessException extends RuntimeException {
// 业务错误码(如40001:参数错误,50001:业务逻辑异常)
private final int code;
// HTTP状态码(适配响应状态)
private final HttpStatus httpStatus;
// 构造方法(默认HTTP 400状态码)
public BusinessException(int code, String message) {
super(message);
this.code = code;
this.httpStatus = HttpStatus.BAD_REQUEST;
}
// 构造方法(自定义HTTP状态码)
public BusinessException(int code, String message, HttpStatus httpStatus) {
super(message);
this.code = code;
this.httpStatus = httpStatus;
}
}
(2)业务异常子类(按需扩展)
按业务场景拆分异常,提升可读性与可维护性:
java
运行
import org.springframework.http.HttpStatus;
/**
* 资源不存在异常(如用户、订单不存在)
*/
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String message) {
super(40400, message, HttpStatus.NOT_FOUND);
}
}
/**
* 权限不足异常
*/
public class ForbiddenException extends BusinessException {
public ForbiddenException(String message) {
super(40300, message, HttpStatus.FORBIDDEN);
}
}
/**
* 库存不足异常
*/
public class StockInsufficientException extends BusinessException {
public StockInsufficientException(String message) {
super(40002, message);
}
}
2. 第二步:实现全局异常处理器(核心组件)
通过 Spring 的@ControllerAdvice注解实现全局异常捕获,统一处理所有异常,返回标准化响应。
(1)全局异常处理器类
java
运行
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器,捕获所有Controller层异常
*/
@Slf4j
@ControllerAdvice // 全局捕获Controller层异常
@ResponseBody // 返回JSON响应
public class GlobalExceptionHandler {
// 1. 处理自定义业务异常(优先级最高,精准匹配)
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
// 记录ERROR级日志(包含请求路径、错误信息、堆栈)
log.error("业务异常 - 请求路径:{},错误码:{},错误信息:{}",
request.getRequestURI(), e.getCode(), e.getMessage(), e);
// 返回标准化错误响应
return Result.fail(e.getCode(), e.getMessage(), null);
}
// 2. 处理参数校验异常(@Valid校验失败)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
// 收集字段校验错误信息(字段名:错误提示)
for (FieldError fieldError : bindingResult.getFieldErrors()) {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
// 记录WARN级日志(参数错误属于客户端问题,无需堆栈)
log.warn("参数校验异常 - 请求路径:{},错误信息:{}",
request.getRequestURI(), errorMap);
// 返回参数错误响应(包含具体字段错误)
return Result.fail(40001, "参数格式不正确", errorMap);
}
// 3. 处理参数类型不匹配异常(如路径参数应为数字却传字符串)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
String message = String.format("参数【%s】类型不匹配,期望类型:%s",
e.getName(), e.getRequiredType().getSimpleName());
log.warn("参数类型异常 - 请求路径:{},错误信息:{}", request.getRequestURI(), message);
return Result.fail(40002, message, null);
}
// 4. 处理系统异常(兜底所有未捕获异常)
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleSystemException(Exception e, HttpServletRequest request) {
// 记录ERROR级日志(包含完整堆栈,便于排查)
log.error("系统异常 - 请求路径:{},错误信息:{}",
request.getRequestURI(), e.getMessage(), e);
// 生产环境返回友好提示,避免暴露堆栈
String errorMsg = "服务端异常,请稍后重试";
// 开发环境返回详细信息(便于调试)
if ("dev".equals(System.getenv("SPRING_PROFILES_ACTIVE"))) {
errorMsg = e.getMessage();
}
return Result.fail(50001, errorMsg, null);
}
// 可扩展:处理404、405等HTTP异常
@ExceptionHandler(org.springframework.web.servlet.NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<?> handle404Exception(org.springframework.web.servlet.NoHandlerFoundException e) {
log.warn("接口不存在 - 请求路径:{},HTTP方法:{}", e.getRequestURL(), e.getHttpMethod());
return Result.fail(40400, "接口不存在", null);
}
}
(2)标准化响应类(Result)
统一接口响应格式,包含成功 / 失败状态、业务码、提示信息、数据体:
java
运行
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Result<T> {
// 响应状态(success/fail)
private String status;
// 业务码(20000:成功,其他:失败)
private int code;
// 提示信息
private String message;
// 响应数据
private T data;
// 响应时间
private LocalDateTime timestamp;
// 私有构造(禁止外部直接创建)
private Result() {
this.timestamp = LocalDateTime.now();
}
// 成功响应(无数据)
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.setStatus("success");
result.setCode(20000);
result.setMessage("操作成功");
result.setData(null);
return result;
}
// 成功响应(带数据)
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setStatus("success");
result.setCode(20000);
result.setMessage("操作成功");
result.setData(data);
return result;
}
// 失败响应(带错误码、提示、数据)
public static <T> Result<T> fail(int code, String message, T data) {
Result<T> result = new Result<>();
result.setStatus("fail");
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
// 失败响应(带错误码、提示,无数据)
public static <T> Result<T> fail(int code, String message) {
return fail(code, message, null);
}
}
3. 第三步:业务代码中使用异常
无需手动捕获异常,直接抛出自定义异常,由全局处理器统一处理:
java
运行
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
// 构造注入(推荐,替代@Resource)
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{orderId}")
public Result<OrderDTO> getOrderById(@PathVariable Long orderId) {
// 业务层抛出的异常由全局处理器捕获
OrderDTO order = orderService.getOrderById(orderId);
return Result.success(order);
}
@PostMapping
public Result<OrderDTO> createOrder(@Valid @RequestBody CreateOrderRequest request) {
// 参数校验失败会抛出MethodArgumentNotValidException,全局处理器处理
OrderDTO order = orderService.createOrder(request);
return Result.success(order);
}
}
// Service层示例
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public OrderDTO getOrderById(Long orderId) {
// 模拟查询订单,不存在则抛出自定义异常
OrderDO orderDO = orderMapper.selectById(orderId);
if (orderDO == null) {
// 抛出资源不存在异常,全局处理器捕获后返回404状态
throw new ResourceNotFoundException("订单不存在,订单ID:" + orderId);
}
// 库存不足场景
if (orderDO.getStock() < 1) {
throw new StockInsufficientException("订单库存不足,无法下单");
}
// 权限校验场景
if (!hasPermission(orderDO.getUserId())) {
throw new ForbiddenException("无权限查看该订单");
}
return convertToDTO(orderDO);
}
// 其他业务方法...
}
4. 第四步:异常日志规范(关键)
日志是异常排查的核心依据,需包含 "时间、请求上下文、错误信息、堆栈",避免日志无效化:
(1)日志记录原则
- 业务异常:记录 ERROR 级日志,包含请求路径、参数、错误码、错误信息,无需完整堆栈(业务异常可预期);
- 系统异常:记录 ERROR 级日志,包含完整堆栈、请求上下文(路径、参数、用户 ID),便于定位根源;
- 客户端异常(参数错误、404):记录 WARN 级日志,无需堆栈,减少日志量;
- 日志模板:统一格式,便于 ELK 等工具收集分析(如
[时间] [级别] [线程名] [类名] - 描述信息)。
(2)日志工具使用(SLF4J + Logback)
SpringBoot 默认集成 SLF4J + Logback,直接注入使用:
java
运行
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class OrderService {
// 注入日志对象(类名作为日志标识)
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void createOrder(CreateOrderRequest request) {
try {
// 业务逻辑
log.info("创建订单 - 请求参数:{}", request); // 记录请求参数
} catch (BusinessException e) {
// 业务异常,记录ERROR级日志
log.error("创建订单失败 - 错误码:{},错误信息:{},请求参数:{}",
e.getCode(), e.getMessage(), request, e);
throw e; // 重新抛出,交给全局处理器
}
}
}
三、进阶:异常处理增强特性
1. 开发 / 生产环境差异化响应
开发环境返回详细错误信息(堆栈、参数)便于调试,生产环境返回友好提示,避免敏感信息泄露:
java
运行
// 全局异常处理器中判断环境
if ("dev".equals(System.getenv("SPRING_PROFILES_ACTIVE"))) {
// 开发环境:返回堆栈信息
Map<String, String> errorData = new HashMap<>();
errorData.put("stackTrace", Arrays.toString(e.getStackTrace()));
return Result.fail(50001, e.getMessage(), errorData);
} else {
// 生产环境:返回友好提示
return Result.fail(50001, "服务端异常,请稍后重试", null);
}
2. 异常降级策略
针对非核心业务异常,可实现降级处理(如缓存兜底、默认值返回),避免服务整体受影响:
java
运行
@Service
public class ProductService {
private final ProductMapper productMapper;
private final RedisUtils redisUtils;
public ProductDTO getProductById(Long productId) {
try {
return productMapper.selectById(productId);
} catch (Exception e) {
log.error("查询商品失败,触发降级 - 商品ID:{}", productId, e);
// 降级策略:从Redis缓存获取兜底数据
ProductDTO cacheProduct = (ProductDTO) redisUtils.getString("product:cache:" + productId);
if (cacheProduct != null) {
return cacheProduct;
}
// 无缓存时返回默认数据
return new ProductDTO(productId, "默认商品", 0.0);
}
}
}
3. 自定义异常枚举(优化错误码管理)
用枚举统一管理错误码与提示,避免硬编码,提升可维护性:
java
运行
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public enum ErrorCodeEnum {
// 客户端错误
PARAM_ERROR(40001, "参数格式不正确", HttpStatus.BAD_REQUEST),
RESOURCE_NOT_FOUND(40400, "资源不存在", HttpStatus.NOT_FOUND),
FORBIDDEN(40300, "无权限访问", HttpStatus.FORBIDDEN),
// 业务错误
STOCK_INSUFFICIENT(40002, "库存不足", HttpStatus.BAD_REQUEST),
ORDER_STATUS_ERROR(40003, "订单状态异常", HttpStatus.BAD_REQUEST),
// 系统错误
SYSTEM_ERROR(50001, "服务端异常,请稍后重试", HttpStatus.INTERNAL_SERVER_ERROR);
private final int code;
private final String message;
private final HttpStatus httpStatus;
ErrorCodeEnum(int code, String message, HttpStatus httpStatus) {
this.code = code;
this.message = message;
this.httpStatus = httpStatus;
}
}
// 自定义异常使用枚举
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException() {
super(ErrorCodeEnum.RESOURCE_NOT_FOUND.getCode(),
ErrorCodeEnum.RESOURCE_NOT_FOUND.getMessage(),
ErrorCodeEnum.RESOURCE_NOT_FOUND.getHttpStatus());
}
// 支持自定义提示信息
public ResourceNotFoundException(String message) {
super(ErrorCodeEnum.RESOURCE_NOT_FOUND.getCode(), message,
ErrorCodeEnum.RESOURCE_NOT_FOUND.getHttpStatus());
}
}
四、避坑指南
1. 坑点 1:捕获异常后不抛出也不处理
- 表现:
try-catch捕获异常后仅打印日志,不重新抛出也不做降级,导致业务流程中断且无法感知; - 解决方案:捕获异常后要么重新抛出(交给全局处理器),要么实现降级逻辑,禁止 "吞掉" 异常。
2. 坑点 2:全局异常处理器未生效
- 表现:抛出异常后未被全局处理器捕获,返回默认错误页面或 Tomcat 错误响应;
- 原因:1. 全局处理器类未被 Spring 扫描(未加
@ControllerAdvice或不在启动类扫描路径下);2. 异常被业务代码中try-catch捕获并处理; - 解决方案:确认
@ControllerAdvice注解生效,排查业务代码是否有冗余try-catch。
3. 坑点 3:暴露敏感信息到响应中
- 表现:生产环境错误响应中返回堆栈信息、数据库地址、用户密码等敏感内容;
- 解决方案:生产环境统一返回友好提示,通过日志记录敏感信息与堆栈,禁止响应体包含敏感内容。
4. 坑点 4:滥用系统异常替代自定义异常
- 表现:业务异常用
NullPointerException、IllegalArgumentException表示,难以区分业务错误与系统错误; - 解决方案:所有业务场景异常都用自定义异常(继承
BusinessException),系统异常仅用于未预料到的底层错误。
5. 坑点 5:日志记录不完整
- 表现:仅记录错误信息,无请求参数、用户 ID、请求路径等上下文,无法定位问题;
- 解决方案:日志记录需包含 "请求上下文 + 错误码 + 错误信息 + 堆栈(系统异常)",确保可追溯。
五、终极总结:异常处理的核心是 "规范与可控"
优秀的异常处理体系,本质是让 "异常流转可控、错误信息规范、问题快速追溯"。核心逻辑是:用自定义异常封装业务错误,用全局处理器统一兜底,用规范日志支撑排查,用降级策略保障稳定。
落地时需记住:
- 业务与异常解耦,告别冗余
try-catch; - 错误码与提示统一,前后端协作无阻碍;
- 日志分级记录,兼顾排查效率与日志量;
- 环境差异化响应,平衡调试需求与安全。
遵循这些实践,能大幅提升项目的可维护性与健壮性,从容应对生产环境的各类异常场景。