SpringBoot实现统一异常处理
SpringBoot实现统一异常处理
一、什么是统一异常处理?
统一异常处理是一种集中式异常管理机制,通过定义全局异常处理器,捕获项目中所有未被局部处理的异常,并以统一格式返回响应。它将分散在各处的异常处理逻辑集中管理,避免代码冗余,保证异常响应格式一致。
二、为什么需要统一异常处理?
未使用统一异常处理时,项目通常存在以下问题:
- 代码冗余:每个接口都需
try-catch,重复代码多。 - 响应格式混乱:不同异常返回格式不一(字符串、Map、500 错误页等),前端解析困难。
- 安全性问题:未处理的异常可能暴露敏感信息(如数据库表名、类路径)。
- 调试困难:异常信息分散,难以快速定位问题。
统一异常处理的优势:
- 代码简洁:消除重复
try-catch,专注业务逻辑。 - 格式统一:所有响应采用标准化结构(状态码、消息、时间戳等),便于前后端对接。
- 信息安全:屏蔽敏感信息,返回用户友好提示。
- 便于维护:异常处理逻辑集中,修改只需改一处。
三、为什么引入 Lombok 和 @Slf4j?
- Lombok:通过注解简化 Java 代码(如
@Data自动生成 getter/setter,@NoArgsConstructor生成无参构造等),减少模板代码。 - @Slf4j:Lombok 提供的注解,自动为类生成
SLF4J日志对象(log),无需手动声明private static final Logger log = ...,简化日志使用。
使用@Slf4j后,可直接通过log.info()、log.error()等方法记录日志,避免重复的日志对象定义。
四、环境准备
- JDK:1.8 及以上
- Spring Boot:2.7.x(本文以此为例,3.x 兼容)
- 开发工具:IDEA(需安装 Lombok 插件,否则注解可能报错)
- 核心依赖:
spring-boot-starter-web(Web 功能)、lombok(简化代码)
五、实现过程

步骤 1:创建 Spring Boot 项目并添加依赖
通过 Spring Initializr 创建项目,修改pox.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- SpringBoot父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/>
</parent>
<groupId>com.yqd</groupId>
<artifactId>SpringBoot-Exception-Demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBoot-Exception-Demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Web:提供REST接口支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok:简化代码,提供@Data、@Slf4j等注解 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<optional>true</optional>
</dependency>
<!-- 测试依赖(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
IDEA 配置:需安装 Lombok 插件(File → Settings → Plugins,搜索Lombok并安装,重启 IDEA),否则注解可能无法识别。
步骤 2:定义统一响应结果类(Result)
创建标准化响应实体,确保成功 / 失败响应格式一致。
package com.yqd.common;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 统一响应结果类
* @param <T> 响应数据类型
*/
@Data // Lombok注解:自动生成getter/setter/toString等
public class Result<T> {
// 状态码:200成功,非200失败(如400参数错误、500系统错误)
private Integer code;
// 响应消息(成功/失败描述)
private String message;
// 响应数据(成功时返回,失败时为null)
private T data;
// 响应时间戳
private LocalDateTime timestamp;
// 私有构造:通过静态方法创建实例,避免直接new
private Result() {
this.timestamp = LocalDateTime.now(); // 自动填充当前时间
}
/**
* 成功响应(带数据)
*/
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 <T> Result<T> success() {
return success(null);
}
/**
* 失败响应(自定义状态码和消息)
*/
public static <T> Result<T> fail(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(null);
return result;
}
}
说明:
@Data注解替代了手动编写getter、setter、toString等方法,简化代码。- 静态方法
success()和fail()用于快速创建响应实例,避免重复代码。
步骤 3:定义自定义异常类
区分业务异常(如 “用户不存在”)和系统异常(如空指针),便于针对性处理。
package com.yqd.exception;
import lombok.Getter;
/**
* 业务异常:用于处理可预见的业务错误(如参数错误、权限不足等)
*/
@Getter // Lombok注解:自动生成getter方法(获取code和message)
public class BusinessException extends RuntimeException {
// 异常状态码(如400参数错误、403权限不足、404资源不存在)
private final Integer code;
/**
* 构造方法:自定义状态码和消息
*/
public BusinessException(Integer code, String message) {
super(message); // 调用父类构造,保存异常消息
this.code = code;
}
/**
* 简化构造:默认状态码400(Bad Request)
*/
public BusinessException(String message) {
this(400, message);
}
}
说明:
@Getter注解自动生成code字段的getter方法,无需手动编写。- 继承
RuntimeException(非受检异常),避免业务代码中强制try-catch。
步骤 4:实现全局异常处理器(结合 @Slf4j)
使用@RestControllerAdvice定义全局异常处理器,通过@ExceptionHandler捕获不同类型的异常,并使用@Slf4j简化日志记录。
package com.yqd.exception;
import com.yqd.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
/**
* 全局异常处理器:捕获所有Controller层抛出的异常
*/
@RestControllerAdvice // 组合@ControllerAdvice和@ResponseBody,返回JSON响应
@Slf4j // Lombok注解:自动生成log日志对象(SLF4J)
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常(BusinessException)
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
// 使用@Slf4j生成的log对象记录日志(warn级别:业务异常为可预见错误)
log.warn("业务异常 - 路径:{},消息:{}", request.getRequestURI(), e.getMessage());
// 返回统一失败响应(使用异常中定义的code和message)
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理参数绑定异常(如@RequestParam必填参数缺失)
*/
@ExceptionHandler(ServletRequestBindingException.class)
public Result<Void> handleServletRequestBindingException(ServletRequestBindingException e, HttpServletRequest request) {
log.warn("参数绑定异常 - 路径:{},消息:{}", request.getRequestURI(), e.getMessage());
return Result.fail(400, "参数错误:" + e.getMessage().split(":")[0]); // 简化错误消息
}
/**
* 处理参数类型不匹配异常(如URL参数应为数字却传字符串)
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
log.warn("参数类型异常 - 路径:{},参数:{},期望类型:{}",
request.getRequestURI(),
e.getName(),
e.getRequiredType().getSimpleName()); // 打印参数名和期望类型
return Result.fail(400, "参数类型错误:" + e.getName() + "应为" + e.getRequiredType().getSimpleName());
}
/**
* 处理所有未捕获的异常(兜底处理)
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
// 系统异常记录error级别日志,并打印堆栈信息(便于排查问题)
log.error("系统异常 - 路径:{}", request.getRequestURI(), e); // 注意:e作为最后一个参数会打印堆栈
// 生产环境返回通用消息,避免暴露敏感信息(如类名、数据库信息)
return Result.fail(500, "服务器内部错误,请联系管理员");
}
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
// 获取第一个校验失败的消息
String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
log.warn("参数校验异常 - 路径:{},消息:{}", request.getRequestURI(), errorMsg);
return Result.fail(400, "参数校验失败:" + errorMsg);
}
}
核心注解与逻辑说明:
@RestControllerAdvice:- 作用于所有标注
@RestController的类,拦截其抛出的异常。 - 结合
@ResponseBody,确保异常处理方法的返回值自动转为 JSON。
- 作用于所有标注
@Slf4j:- 自动生成
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GlobalExceptionHandler.class); - 直接使用
log.warn()、log.error()等方法记录日志,无需手动声明日志对象。
- 自动生成
@ExceptionHandler:- 注解参数指定处理的异常类型(如
BusinessException.class)。 - 方法参数可注入
HttpServletRequest,用于获取请求路径等信息。
- 注解参数指定处理的异常类型(如
步骤 5:编写测试接口验证效果
创建 Controller 模拟不同异常场景,测试统一异常处理是否生效。
package com.yqd.controller;
import com.yqd.common.Result;
import com.yqd.dto.UserDTO;
import com.yqd.exception.BusinessException;
import com.yqd.exception.UserNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/test")
@Slf4j // 用于记录接口访问日志(示例)
public class ExceptionTestController {
/**
* 正常响应示例
*/
@GetMapping("/success")
public Result<String> success() {
log.info("访问成功接口"); // 使用@Slf4j的log对象
return Result.success("这是成功返回的数据");
}
/**
* 抛出自定义业务异常(用户不存在)
*/
@GetMapping("/user/{id}")
public Result<String> getUser(@PathVariable Long id) {
log.info("查询用户ID:{}", id);
if (id <= 0) {
// 模拟业务校验失败,抛出自定义异常
throw new BusinessException(400, "用户ID必须为正数");
}
if (id == 999) {
throw new UserNotFoundException(id); // 抛出自定义细分异常
}
return Result.success("用户信息:" + id);
}
/**
* 模拟参数缺失异常(@RequestParam必填参数未传)
*/
@GetMapping("/param-required")
public Result<Void> paramRequired(
@RequestParam(required = true) String username // required=true表示必填
) {
return Result.success();
}
/**
* 模拟参数类型不匹配(URL参数应为数字,实际传字符串)
*/
@GetMapping("/param-type/{num}")
public Result<Void> paramType(@PathVariable Integer num) {
return Result.success();
}
/**
* 模拟系统异常(空指针)
*/
@GetMapping("/system-error")
public Result<Void> systemError() {
String str = null;
str.length(); // 触发NullPointerException
return Result.success();
}
@PostMapping("/user")
public Result<UserDTO> addUser(@Valid @RequestBody UserDTO user) {
return Result.success(user);
}
}
步骤 6:启动类
package com.yqd;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootExceptionDemo {
public static void main(String[] args) {
SpringApplication.run(SpringBootExceptionDemo.class, args);
}
}
步骤 7:启动项目并测试
启动 Spring Boot 应用,通过浏览器或 Postman 访问以下接口,验证效果:
-
正常响应:
http://localhost:8080/test/success响应:{ "code": 200, "message": "操作成功", "data": "这是成功返回的数据", "timestamp": "2024-10-24T16:30:00" }日志输出:
INFO com.yqd.controller.ExceptionTestController : 访问成功接口 -
业务异常(用户 ID 为负数):
http://localhost:8080/test/user/-1响应:{ "code": 400, "message": "用户ID必须为正数", "data": null, "timestamp": "2024-10-24T16:31:00" }日志输出:
WARN com.yqd.exception.GlobalExceptionHandler : 业务异常 - 路径:/test/user/-1,消息:用户ID必须为正数 -
参数缺失异常:
http://localhost:8080/test/param-required(不传递username)响应:{ "code": 400, "message": "参数错误:Required String parameter 'username' is not present", "data": null, "timestamp": "2024-10-24T16:32:00" }日志输出:
WARN com.yqd.exception.GlobalExceptionHandler : 参数绑定异常 - 路径:/test/param-required,消息:Required String parameter 'username' is not present -
参数类型不匹配:
http://localhost:8080/test/param-type/abc(abc不是数字)响应:{ "code": 400, "message": "参数类型错误:num应为Integer", "data": null, "timestamp": "2024-10-24T16:33:00" }日志输出:
WARN com.yqd.exception.GlobalExceptionHandler : 参数类型异常 - 路径:/test/param-type/abc,参数:num,期望类型:Integer -
系统异常(空指针):
http://localhost:8080/test/system-error响应:{ "code": 500, "message": "服务器内部错误,请联系管理员", "data": null, "timestamp": "2024-10-24T16:34:00" }日志输出(包含堆栈):
ERROR com.yqd.exception.GlobalExceptionHandler : 系统异常 - 路径:/test/system-error同时打印NullPointerException的完整堆栈信息,便于排查问题。
六、进阶优化
1. 细化异常类型(针对特定业务)
根据业务场景定义更具体的异常类,如UserNotFoundException、OrderExpiredException,使异常处理更精准。
示例:UserNotFoundException.java
package com.yqd.exception;
import lombok.Getter;
@Getter
public class UserNotFoundException extends BusinessException {
// 继承业务异常,默认状态码404
public UserNotFoundException(Long userId) {
super(404, "用户不存在:" + userId);
}
}
在 Controller 中使用:
@GetMapping("/user/{id}")
public Result<String> getUser(@PathVariable Long id) {
if (id == 999) {
throw new UserNotFoundException(id); // 抛出自定义细分异常
}
return Result.success("用户信息:" + id);
}
无需修改全局异常处理器(BusinessException的处理器会自动捕获其子类异常)。
2. 集成参数校验框架(validation)
结合spring-boot-starter-validation处理请求参数校验(如字段长度、格式),并在全局异常处理器中捕获校验异常。
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
创建实体类并添加校验注解:
package com.yqd.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空") // 非空校验
@Size(min = 3, max = 20, message = "用户名长度必须为3-20个字符") // 长度校验
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
private String password;
}
在 Controller 中使用@Valid触发校验:
@PostMapping("/user")
public Result<UserDTO> addUser(@Valid @RequestBody UserDTO user) {
return Result.success(user);
}
在全局异常处理器中添加校验异常处理:
import org.springframework.validation.BindException;
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
// 获取第一个校验失败的消息
String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
log.warn("参数校验异常 - 路径:{},消息:{}", request.getRequestURI(), errorMsg);
return Result.fail(400, "参数校验失败:" + errorMsg);
}
测试请求:发送 POST 请求到/test/user,传入{"username":"ab","password":"123"},响应:
{
"code": 400,
"message": "参数校验失败:用户名长度必须为3-20个字符",
"data": null,
"timestamp": "2024-10-24T16:40:00"
}
3. 区分环境返回异常信息
开发环境需要详细异常信息(如堆栈)便于调试,生产环境则返回脱敏后的通用消息。可通过@Profile或配置文件实现。
示例:通过配置文件区分环境application-dev.yml(开发环境):
server:
port: 8080
custom:
exception:
show-detail: true # 开发环境显示详细异常信息
application-prod.yml(生产环境):
server:
port: 8080
custom:
exception:
show-detail: false # 生产环境隐藏详细信息
修改全局异常处理器:
import org.springframework.beans.factory.annotation.Value;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Value("${custom.exception.show-detail:false}")
private boolean showDetail; // 是否显示详细异常信息
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
log.error("系统异常 - 路径:{}", request.getRequestURI(), e);
// 根据环境决定返回消息
String message = showDetail ? e.getMessage() : "服务器内部错误,请联系管理员";
return Result.fail(500, message);
}
}
启动时指定环境(如--spring.profiles.active=dev),开发环境会返回异常详情,生产环境返回通用消息。
七、总结
本教程通过以下步骤实现了 Spring Boot 统一异常处理:
- 引入 Lombok,使用
@Data简化实体类,@Slf4j简化日志记录。 - 定义统一响应类
Result,保证所有响应格式一致。 - 创建自定义业务异常
BusinessException,区分业务错误和系统错误。 - 实现全局异常处理器
GlobalExceptionHandler,通过@RestControllerAdvice和@ExceptionHandler集中捕获异常,并使用@Slf4j记录日志。 - 细化异常处理逻辑,支持参数校验、类型转换等常见异常,并可根据环境返回不同详细程度的信息。

浙公网安备 33010602011771号