一、为什么需要全局异常处理?
在Web应用开发中,异常处理是保证系统健壮性的关键环节。传统的try-catch方式虽然直接,但存在诸多问题:
- 代码冗余:每个Controller方法都需要重复的异常捕获逻辑
- 维护困难:异常处理逻辑分散,修改成本高
- 响应不统一:不同方法返回的异常格式各异,前端难以统一处理
- 业务代码污染:异常处理与业务逻辑耦合,可读性差
SpringBoot通过全局异常处理机制,让我们能够集中管理异常,统一返回格式,提升开发效率和系统稳定性。
二、SpringBoot全局异常处理核心组件
1. @ControllerAdvice 注解
这是全局异常处理的基石。通过@ControllerAdvice标记的类,可以拦截所有Controller抛出的异常。
@ControllerAdvice
public class GlobalExceptionHandler {
// 异常处理方法
}
@ControllerAdvice支持更细粒度的控制:
// 只处理指定包下的Controller
@ControllerAdvice(basePackages = "com.example.controller")
// 只处理指定注解标记的Controller
@ControllerAdvice(annotations = RestController.class)
// 只处理指定类
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
2. @ExceptionHandler 注解
用于标记具体的异常处理方法,可以针对特定异常类型进行捕获:
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception ex) {
// 处理逻辑
}
3. @ResponseStatus 注解
可以配合@ExceptionHandler使用,指定HTTP响应状态码:
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleResourceNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("资源不存在", ex.getMessage());
}
三、实战:构建完整的全局异常处理体系
1. 定义统一响应体
首先创建标准的错误响应格式:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
private Long timestamp = System.currentTimeMillis();
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data, System.currentTimeMillis());
}
public static ApiResponse<?> error(Integer code, String message) {
return new ApiResponse<>(code, message, null, System.currentTimeMillis());
}
}
2. 自定义业务异常
创建业务异常基类,便于统一处理:
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(String message) {
super(message);
this.code = 500;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
// getter方法
}
3. 实现全局异常处理器
核心的异常处理类:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException ex) {
log.error("业务异常: {}", ex.getMessage(), ex);
return ApiResponse.error(ex.getCode(), ex.getMessage());
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("; "));
log.error("参数校验异常: {}", message);
return ApiResponse.error(400, message);
}
/**
* 处理数据绑定异常
*/
@ExceptionHandler(BindException.class)
public ApiResponse<?> handleBindException(BindException ex) {
String message = ex.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("; "));
log.error("数据绑定异常: {}", message);
return ApiResponse.error(400, message);
}
/**
* 处理其他运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public ApiResponse<?> handleRuntimeException(RuntimeException ex) {
log.error("运行时异常: {}", ex.getMessage(), ex);
return ApiResponse.error(500, "系统繁忙,请稍后重试");
}
/**
* 处理其他所有异常
*/
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleException(Exception ex) {
log.error("系统异常: {}", ex.getMessage(), ex);
return ApiResponse.error(500, "系统异常");
}
}
4. 使用示例
在Controller中抛出异常:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
throw new BusinessException(404, "用户不存在");
}
return ApiResponse.success(user);
}
@PostMapping
public ApiResponse<?> createUser(@Valid @RequestBody UserDTO userDTO) {
// 参数校验由@Valid自动完成,失败会抛出MethodArgumentNotValidException
userService.create(userDTO);
return ApiResponse.success(null);
}
}
四、进阶技巧与最佳实践
1. 异常日志分级处理
不同级别的异常采用不同的日志级别:
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException ex) {
if (ex.getCode() >= 500) {
log.error("业务异常: {}", ex.getMessage(), ex);
} else {
log.warn("业务异常: {}", ex.getMessage());
}
return ApiResponse.error(ex.getCode(), ex.getMessage());
}
2. 国际化支持
结合Spring的MessageSource实现异常消息国际化:
@Autowired
private MessageSource messageSource;
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException ex, HttpServletRequest request) {
String message = messageSource.getMessage(ex.getMessage(), null,
LocaleContextHolder.getLocale());
return ApiResponse.error(ex.getCode(), message);
}
3. 自定义异常处理顺序
当多个异常处理器都能处理同一异常时,可以通过@Order注解或实现Ordered接口控制优先级:
@Order(Ordered.HIGHEST_PRECEDENCE)
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException ex) {
// 高优先级处理
}
4. 处理特定HTTP状态码异常
Spring MVC会抛出特定状态码的异常,如HttpRequestMethodNotSupportedException(405),可以单独处理:
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiResponse<?> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
return ApiResponse.error(405, "请求方法不支持");
}
五、常见问题与解决方案
1. 异常处理器不生效?
- 检查
@ControllerAdvice或@RestControllerAdvice是否被Spring扫描到 - 确认异常类型是否匹配
@ExceptionHandler指定的类型 - 检查是否有更高优先级的异常处理器已经处理了该异常
2. 如何测试全局异常处理?
使用MockMvc进行单元测试:
@SpringBootTest
@AutoConfigureMockMvc
class GlobalExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testBusinessException() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(404))
.andExpect(jsonPath("$.message").value("用户不存在"));
}
}
3. 如何处理Filter中的异常?
Filter中的异常无法被@ControllerAdvice捕获,可以通过Filter的doFilter方法捕获,然后重定向到错误处理接口:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
chain.doFilter(request, response);
} catch (Exception ex) {
// 重定向到错误处理接口
((HttpServletResponse) response).sendRedirect("/error");
}
}
或者使用OncePerRequestFilter的shouldNotFilterErrorDispatch方法。
六、总结
SpringBoot的全局异常处理机制通过@ControllerAdvice和@ExceptionHandler提供了优雅的解决方案。通过本文的实践,你可以:
- ✅ 统一异常响应格式,提升前后端协作效率
- ✅ 集中管理异常处理逻辑,降低维护成本
- ✅ 实现异常日志分级,便于问题排查
- ✅ 支持国际化,满足多语言需求
- ✅ 处理各种类型的异常,包括参数校验、业务异常等
建议在实际项目中根据业务需求进行定制化扩展,如添加请求ID追踪、异常监控上报等功能,构建更加健壮的后端服务。
代码示例说明:以上代码基于SpringBoot 2.x/3.x版本,需要引入Lombok、Spring Web等依赖。实际使用时请根据项目结构进行调整。