Spring Boot全局异常捕获指南

文章目录

    • 前言
    • [1. 为什么需要全局异常处理?](#1. 为什么需要全局异常处理?)
    • [2. 全局异常处理的三种实现方式](#2. 全局异常处理的三种实现方式)
      • [2.1 @ControllerAdvice + @ExceptionHandler(推荐方式)](#2.1 @ControllerAdvice + @ExceptionHandler(推荐方式))
      • [2.2 使用@ErrorController自定义错误页面](#2.2 使用@ErrorController自定义错误页面)
      • [2.3 配置ErrorPageRegistrar](#2.3 配置ErrorPageRegistrar)
    • [3. 版本差异与兼容性问题](#3. 版本差异与兼容性问题)
      • [3.1 Spring Boot 1.x vs 2.x vs 3.x](#3.1 Spring Boot 1.x vs 2.x vs 3.x)
      • [3.2 处理版本差异的最佳实践](#3.2 处理版本差异的最佳实践)
    • [4. 高级技巧与最佳实践](#4. 高级技巧与最佳实践)
      • [4.1 异常处理优先级问题](#4.1 异常处理优先级问题)
      • [4.2 区分Web请求和API请求](#4.2 区分Web请求和API请求)
      • [4.3 异常处理与国际化](#4.3 异常处理与国际化)
    • [5. 测试全局异常处理](#5. 测试全局异常处理)
    • [6. 总结](#6. 总结)

前言

在Spring Boot开发中,优雅地处理异常是构建健壮应用的关键。本文将深入探讨全局异常处理的实现方式,并分析不同版本间的差异,助你打造更稳定的应用。

1. 为什么需要全局异常处理?

在Web应用开发中,异常处理往往是一个容易被忽视但却至关重要的环节。如果没有统一的异常处理机制,可能会出现以下问题:

  • 用户体验不一致:不同的异常显示不同的错误页面,甚至直接暴露堆栈信息
  • 代码重复:每个Controller都编写相似的异常处理代码
  • 维护困难:异常处理逻辑分散在各个角落,难以统一管理
  • 安全隐患:可能暴露系统内部细节给攻击者

Spring Boot通过提供全局异常处理机制,让我们能够以声明式的方式统一处理这些异常。

2. 全局异常处理的三种实现方式

2.1 @ControllerAdvice + @ExceptionHandler(推荐方式)

这是最常用且灵活的全局异常处理方式,适用于Spring 3.2及以上版本。

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        logger.error("业务异常: {}", ex.getMessage(), ex);
        ErrorResponse errorResponse = new ErrorResponse("BUSINESS_ERROR", ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
    
    /**
     * 处理数据不存在异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
        logger.error("资源未找到: {}", ex.getMessage(), ex);
        ErrorResponse errorResponse = new ErrorResponse("RESOURCE_NOT_FOUND", ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }
    
    /**
     * 处理所有未捕获的异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
        logger.error("系统异常: {}", ex.getMessage(), ex);
        ErrorResponse errorResponse = new ErrorResponse("INTERNAL_SERVER_ERROR", "系统繁忙,请稍后再试");
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
    /**
     * 处理参数验证异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
        logger.error("参数验证失败: {}", ex.getMessage(), ex);
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.toList());
        
        ErrorResponse errorResponse = new ErrorResponse("VALIDATION_ERROR", "参数验证失败", errors);
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

// 统一的错误响应体
@Data
@AllArgsConstructor
@NoArgsConstructor
class ErrorResponse {
    private String code;
    private String message;
    private List<String> details;
    
    public ErrorResponse(String code, String message) {
        this.code = code;
        this.message = message;
    }
}

2.2 使用@ErrorController自定义错误页面

适用于需要自定义错误页面的场景,Spring Boot提供了BasicErrorController作为默认实现,我们可以继承并重写它。

java 复制代码
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        
        // 自定义错误响应格式
        Map<String, Object> response = new HashMap<>();
        response.put("success", false);
        response.put("errorCode", body.get("status"));
        response.put("errorMessage", body.get("message"));
        response.put("timestamp", new Date());
        
        return new ResponseEntity<>(response, status);
    }
}

2.3 配置ErrorPageRegistrar

适用于需要根据不同HTTP状态码跳转到不同页面的场景。

java 复制代码
@Configuration
public class CustomErrorPageConfiguration implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(
            new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"),
            new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500"),
            new ErrorPage(HttpStatus.FORBIDDEN, "/error/403")
        );
    }
}

@Controller
@RequestMapping("/error")
public class ErrorPageController {
    
    @GetMapping("/404")
    public String notFound() {
        return "error/404";
    }
    
    @GetMapping("/500")
    public String serverError() {
        return "error/500";
    }
    
    @GetMapping("/403")
    public String forbidden() {
        return "error/403";
    }
}

3. 版本差异与兼容性问题

3.1 Spring Boot 1.x vs 2.x vs 3.x

特性 Spring Boot 1.x Spring Boot 2.x Spring Boot 3.x
@ControllerAdvice 支持 支持 支持
ErrorController接口 有变化
默认错误处理 BasicErrorController BasicErrorController 基本保持一致
响应序列化 Jackson 1.x/2.x Jackson 2.x Jackson 2.15+
路径匹配 Ant路径匹配 Ant路径匹配 MVC路径匹配改进

重要变化:

  1. Spring Boot 2.3+ :引入了ErrorProperties的构造方法变化,需要调整自定义ErrorController的实现
  2. Spring Boot 2.5+:改进了/error路径的处理逻辑,更加灵活
  3. Spring Boot 3.0+:Jakarta EE 9+,包名从javax变为jakarta

3.2 处理版本差异的最佳实践

java 复制代码
// 兼容不同版本的ErrorController实现
@Controller
public class CompatibleErrorController {
    
    // 针对Spring Boot 2.3+的构造方法
    @Autowired
    public CompatibleErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        // 实现逻辑
    }
    
    // 针对旧版本的备用构造方法
    @Autowired(required = false)
    public CompatibleErrorController(ErrorAttributes errorAttributes) {
        this(errorAttributes, new ErrorProperties());
    }
}

4. 高级技巧与最佳实践

4.1 异常处理优先级问题

当存在多个异常处理器时,Spring会按照以下优先级匹配:

  1. 最具体的异常类型优先
  2. 同一异常类型,在同一个@ControllerAdvice中按声明顺序
  3. 不同@ControllerAdvice之间,按@Order注解或Ordered接口实现排序
java 复制代码
// 通过@Order控制处理顺序
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class HighPriorityExceptionHandler {
    // 处理高优先级异常
}

@Order(Ordered.LOWEST_PRECEDENCE)
@RestControllerAdvice
public class LowPriorityExceptionHandler {
    // 处理低优先级异常和通用异常
}

4.2 区分Web请求和API请求

有时我们需要对Web页面请求和API请求返回不同的错误响应:

java 复制代码
@RestControllerAdvice
public class ApiExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleApiException(Exception ex, HttpServletRequest request) {
        // 检查请求是否来自API
        if (isApiRequest(request)) {
            // 返回JSON格式错误
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new ErrorResponse("API_ERROR", ex.getMessage()));
        }
        
        // 非API请求,继续后续处理
        throw new RuntimeException(ex);
    }
    
    private boolean isApiRequest(HttpServletRequest request) {
        return request.getRequestURI().startsWith("/api/") ||
               "application/json".equals(request.getHeader("Accept"));
    }
}

4.3 异常处理与国际化

结合Spring的国际化支持,提供多语言错误信息:

java 复制代码
@RestControllerAdvice
public class InternationalizedExceptionHandler {
    
    @Autowired
    private MessageSource messageSource;
    
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex, 
                                                                 Locale locale) {
        String localizedMessage = messageSource.getMessage(
            ex.getErrorCode(), 
            ex.getArgs(), 
            ex.getMessage(), 
            locale
        );
        
        ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), localizedMessage);
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

5. 测试全局异常处理

确保异常处理逻辑正确工作的测试案例:

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
public class GlobalExceptionHandlerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void testResourceNotFoundException() throws Exception {
        mockMvc.perform(get("/api/users/999"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("RESOURCE_NOT_FOUND"))
                .andExpect(jsonPath("$.message").exists());
    }
    
    @Test
    public void testValidationException() throws Exception {
        UserRequest invalidRequest = new UserRequest("", "invalid-email");
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidRequest)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
                .andExpect(jsonPath("$.details").isArray());
    }
}

6. 总结

Spring Boot的全局异常处理机制提供了强大而灵活的方式来统一处理应用中的异常。通过合理使用@ControllerAdvice、自定义ErrorController以及ErrorPageRegistrar,我们可以构建出健壮且用户友好的应用程序。

关键要点:

  1. 优先使用@ControllerAdvice+@ExceptionHandler:这是最灵活和推荐的方式
  2. 注意版本差异:特别是Spring Boot 2.3+和3.0+的 breaking changes
  3. 考虑异常处理优先级:合理安排异常处理器的顺序
  4. 区分API和页面请求:为不同类型的请求提供合适的响应格式
  5. 实现国际化支持:为多语言应用提供本地化的错误信息
  6. 编写测试用例:确保异常处理逻辑的正确性

通过掌握这些技巧,你能够构建出更加健壮、易维护的Spring Boot应用程序,提供更好的用户体验和更安全的错误信息处理。


欢迎在评论区分享你在Spring Boot异常处理中的经验和遇到的问题!

相关推荐
即将进化成人机11 分钟前
Maven架构的依赖管理和项目构建
java·架构·maven
qianmoq26 分钟前
第03章:无限流:generate()和iterate()的神奇用法
java
whitepure28 分钟前
万字详解JVM
java·jvm·后端
我崽不熬夜34 分钟前
Java的条件语句与循环语句:如何高效编写你的程序逻辑?
java·后端·java ee
我崽不熬夜1 小时前
Java中的String、StringBuilder、StringBuffer:究竟该选哪个?
java·后端·java ee
草明1 小时前
docker stats 增加一列容器名称的显示
java·开发语言·docker
期待のcode2 小时前
Maven的概念与Maven项目的创建
java·maven
我崽不熬夜2 小时前
Java中的基本数据类型和包装类:你了解它们的区别吗?
java·后端·java ee
每天学习一丢丢2 小时前
SpringBoot + Vue实现批量导入导出功能的标准方案
vue.js·spring boot·后端
我是廖志伟2 小时前
【jar包启动,每天生成一个日志文件】
java·jar