1631 字
4 分钟
Spring Boot 全局异常处理
2026-03-21

Spring Boot 异常的层层处理与全局异常处理#

在 Spring Boot 项目中,异常处理是非常重要的一部分。一个接口从前端请求开始,通常会依次经过 Controller、Service、Mapper,最后访问数据库。如果数据库操作出错,异常不会只停留在数据库层,而是会沿着调用链反向向上抛出,直到被某一层捕获处理。

以登录接口为例,正常请求调用链如下:

前端请求
Controller
Service
Mapper
数据库

如果数据库操作出错,异常传播方向则是反过来的:

数据库异常
Mapper
Service
Controller
Spring MVC 捕获异常
全局异常处理器处理异常
返回统一错误响应给前端

这就是异常的层层处理机制。简单来说,异常可以不在出错的位置立刻处理,而是可以继续向上抛给调用者处理。谁调用了当前方法,当前方法就可以把异常抛回给谁。

一、异常的层层传播过程#

假设我们有一个登录接口,Controller 层代码如下:

@PostMapping("/login")
public Result<User> login(@RequestBody LoginDTO loginDTO) {
User user = userService.login(loginDTO);
return Result.success(user);
}

Service 层代码如下:

public User login(LoginDTO loginDTO) {
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (!user.getPassword().equals(loginDTO.getPassword())) {
throw new RuntimeException("密码错误");
}
return user;
}

当 Service 中执行到:

throw new RuntimeException("用户不存在");

程序不会继续往下执行,而是开始向上寻找能够处理这个异常的地方。它会先回到 Controller,如果 Controller 没有写 try-catch 处理异常,就继续交给 Spring MVC。Spring MVC 接收到异常后,如果项目中配置了全局异常处理器,就会交给全局异常处理器统一处理;如果没有配置,就会使用 Spring Boot 默认的异常处理机制。

所以异常处理的核心流程可以理解为:

Service 抛出异常
Controller 没有处理
Spring MVC 捕获异常
全局异常处理器统一处理
返回 JSON 给前端

二、Service 层要不要写 try-catch#

Service 层当然可以写 try-catch,但是要分清楚:能不能写和该不该大量写不是一回事。

例如下面这种写法是可以的:

public User login(LoginDTO loginDTO) {
try {
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null) {
throw new RuntimeException("用户不存在");
}
return user;
} catch (Exception e) {
throw new RuntimeException("登录失败:" + e.getMessage());
}
}

这段代码中,Service 捕获了异常,然后又重新抛出。这样做不会把异常吞掉,外层仍然可以知道程序出错了。

但是不推荐在 Service 层为了给前端返回结果而大量写 try-catch。因为 Service 层的主要职责是处理业务逻辑,而不是组织接口返回格式。接口返回格式应该交给 Controller 或全局异常处理器统一处理。

比较不推荐的写法如下:

public User login(LoginDTO loginDTO) {
try {
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null) {
throw new RuntimeException("用户不存在");
}
return user;
} catch (Exception e) {
System.out.println("Service 捕获到了异常:" + e.getMessage());
return null;
}
}

这种写法的问题是异常被 catch 之后没有继续抛出,而是直接返回了 null。Controller 可能还会把 null 当成成功结果返回:

@PostMapping("/login")
public Result<User> login(@RequestBody LoginDTO loginDTO) {
User user = userService.login(loginDTO);
return Result.success(user);
}

前端收到的结果可能是:

{
"code": 200,
"message": "success",
"data": null
}

但是实际上登录已经失败了。这种情况叫做“吞异常”,也就是异常被捕获后没有继续处理,也没有继续抛出,导致外层不知道里面已经出错。

Service 层适合写 try-catch 的情况主要有几种:第一种是把底层异常转换成业务异常,例如把数据库异常、文件异常、第三方接口异常转换成更容易理解的业务提示;第二种是需要做补偿操作,例如上传文件成功后数据库保存失败,需要删除已经上传的文件;第三种是处理文件、流、网络连接等容易出错的资源。

例如上传文件时就可以这样写:

public void upload(File file) {
String url = null;
try {
url = ossClient.upload(file);
productMapper.insert(url);
} catch (Exception e) {
if (url != null) {
ossClient.delete(url);
}
throw new BusinessException(500, "上传失败");
}
}

三、为什么需要全局异常处理#

如果不使用全局异常处理,也可以在每个 Controller 方法中写 try-catch:

@PostMapping("/login")
public Result<User> login(@RequestBody LoginDTO loginDTO) {
try {
User user = userService.login(loginDTO);
return Result.success(user);
} catch (Exception e) {
return Result.error(500, e.getMessage());
}
}

这种写法可以实现异常处理,但是问题很明显:每个接口都要写类似的 try-catch,代码会大量重复,Controller 也会变得很臃肿。而且不同接口如果返回格式不统一,前端处理起来也会比较麻烦。

所以在实际 Spring Boot 项目中,更推荐使用全局异常处理。Controller 只负责接收请求、调用 Service、返回正常结果;Service 只负责业务逻辑;异常统一交给 GlobalExceptionHandler 处理。

四、Spring Boot 全局异常处理实现#

全局异常处理一般使用两个注解:

@RestControllerAdvice
@ExceptionHandler

其中,@RestControllerAdvice 表示这是一个全局 Controller 增强类,可以拦截 Controller 层抛出的异常;@ExceptionHandler 表示某个方法专门处理某一种类型的异常。

在写全局异常处理之前,一般会先定义统一返回结果类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success() {
return new Result<>(200, "success", null);
}
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null);
}
}

然后定义一个业务异常类:

@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(String message) {
super(message);
this.code = 500;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
}

业务异常一般用于用户不存在、密码错误、权限不足、库存不足、参数不合法、数据不存在等场景。相比直接抛 RuntimeException,自定义 BusinessException 的好处是可以携带业务状态码和业务提示信息。

最后编写全局异常处理器:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult()
.getFieldErrors()
.get(0)
.getDefaultMessage();
log.error("参数校验异常:{}", message);
return Result.error(400, message);
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常:", e);
return Result.error(500, "系统异常,请稍后再试");
}
}

这里可以看到,全局异常处理器中可以写多个异常处理方法。Spring 会优先寻找最匹配的异常处理方法。比如 BusinessException 会优先交给 handleBusinessException 处理,参数校验异常会交给 handleMethodArgumentNotValidException 处理,其他没有单独处理的异常最终会交给 Exception.class 兜底处理。

五、实际项目中的推荐写法#

在实际项目中,比较推荐的分工是:Controller 层不写大量 try-catch,只负责接收请求和返回结果;Service 层负责业务逻辑,遇到业务错误直接抛出 BusinessException;Mapper 层只负责数据库操作,一般不主动处理异常;GlobalExceptionHandler 负责统一捕获异常、记录日志,并返回统一 JSON 格式给前端。

以登录接口为例,Service 中可以这样写:

public User login(LoginDTO loginDTO) {
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null) {
throw new BusinessException(400, "用户不存在");
}
if (!user.getPassword().equals(loginDTO.getPassword())) {
throw new BusinessException(400, "密码错误");
}
return user;
}

Controller 层保持简洁:

@PostMapping("/login")
public Result<User> login(@RequestBody LoginDTO loginDTO) {
User user = userService.login(loginDTO);
return Result.success(user);
}

如果用户不存在,异常会从 Service 向上抛出,Controller 不处理,最终交给全局异常处理器处理。前端收到的结果就是统一格式:

{
"code": 400,
"message": "用户不存在",
"data": null
}

总结来说,Spring Boot 中异常处理的推荐方式是:业务错误在 Service 层抛出,Controller 层不大量写 try-catch,全局异常处理器统一捕获异常并返回结果。这样可以让代码更加简洁,职责更加清晰,前端收到的错误格式也更加统一,后期维护起来会更加方便。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Spring Boot 全局异常处理
https://csa.fur1na.top/posts/spring-boot-全局异常处理/
作者
fur1na
发布于
2026-03-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00