Spring Boot中的统一异常处理:ErrorController与@ControllerAdvice

Spring Boot中的统一异常处理:ErrorController与@ControllerAdvice

你好,Spring Boot的“急救医生”

大家好!今天我们要聊的是Spring Boot中非常重要的两个概念:ErrorController@ControllerAdvice。它们就像是Spring Boot应用中的“急救医生”,专门负责处理那些意外的错误,确保我们的应用程序在遇到问题时能够优雅地响应,而不是直接崩溃或者返回一个难看的500错误页面。

1. 为什么需要统一异常处理?

想象一下,你正在开发一个电商网站,用户可以浏览商品、加入购物车、下单支付。突然,某个地方抛出了一个NullPointerException,导致整个页面显示了一个丑陋的500错误。用户看到这个错误后,可能会感到困惑甚至失去信心,进而放弃购买。这显然不是一个好的用户体验。

为了避免这种情况,我们需要一种机制来捕获所有的异常,并且根据不同的异常类型返回合适的响应。这就是统一异常处理的作用。通过统一异常处理,我们可以:

  • 捕获所有未处理的异常
  • 根据异常类型返回不同的HTTP状态码
  • 返回友好的错误信息给用户
  • 记录日志以便后续排查问题

2. ErrorController:全局错误处理的第一道防线

ErrorController是Spring Boot提供的一个接口,用于处理全局的错误。它是Spring Boot默认的错误处理机制之一。当你访问一个不存在的URL时,Spring Boot会自动调用ErrorController来处理404错误;当发生内部服务器错误时,它也会调用ErrorController来处理500错误。

默认实现:BasicErrorController

Spring Boot自带了一个默认的ErrorController实现,叫做BasicErrorController。它会根据请求的内容类型(如JSON或HTML)返回相应的错误响应。例如,如果你发送的是一个JSON请求,BasicErrorController会返回一个JSON格式的错误信息;如果是HTML请求,则会返回一个简单的错误页面。

public class BasicErrorController implements ErrorController {
    @RequestMapping(produces = "application/json")
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }

    @RequestMapping
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        return new ModelAndView("error", model);
    }
}

自定义ErrorController

虽然BasicErrorController已经足够强大,但有时候我们可能需要更细粒度的控制。比如,我们想要自定义错误页面的样式,或者在某些情况下返回特定的HTTP状态码。这时,我们可以通过实现ErrorController接口来自定义错误处理逻辑。

@Controller
public class CustomErrorController implements ErrorController {

    private static final String PATH = "/error";

    @RequestMapping(value = PATH)
    public String errorPage(HttpServletRequest request) {
        // 获取HTTP状态码
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");

        // 根据状态码返回不同的页面
        switch (statusCode) {
            case 404:
                return "error/404";
            case 500:
                return "error/500";
            default:
                return "error/default";
        }
    }

    @Override
    public String getErrorPath() {
        return PATH;
    }
}

在这个例子中,我们根据HTTP状态码返回了不同的错误页面。这样,用户在遇到404或500错误时,会看到更加友好的提示信息,而不是一个空白的500页面。

3. @ControllerAdvice:局部异常处理的“救火队员”

@ControllerAdvice是一个注解,用于定义全局的异常处理器。它可以捕获所有控制器中的异常,并根据异常类型进行处理。相比于ErrorController@ControllerAdvice更加灵活,因为它可以针对具体的异常类型提供不同的处理逻辑。

基本用法

假设我们在一个RESTful API中,有一个控制器方法用于获取用户信息。如果用户不存在,我们应该返回一个404错误;如果数据库连接失败,我们应该返回一个500错误。我们可以使用@ControllerAdvice来捕获这些异常,并返回相应的HTTP状态码和错误信息。

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // 模拟用户不存在的情况
        if (id == 1) {
            throw new UserNotFoundException("User with ID 1 not found");
        }
        return new User(id, "John Doe");
    }
}

接下来,我们定义一个全局异常处理器,使用@ControllerAdvice注解来捕获UserNotFoundException和其他异常。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
        ErrorResponse error = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred");
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

在这个例子中,GlobalExceptionHandler类使用了@ControllerAdvice注解,表示它是一个全局异常处理器。我们定义了两个异常处理方法:

  • handleUserNotFoundException:专门处理UserNotFoundException,返回404状态码和自定义的错误信息。
  • handleGeneralException:处理所有其他类型的异常,返回500状态码和通用的错误信息。

ErrorResponse类

为了让错误信息更加结构化,我们可以定义一个ErrorResponse类,用于封装错误代码和错误消息。

public class ErrorResponse {
    private int code;
    private String message;

    public ErrorResponse(int code, String message) {
        this.code = code;
        this.message = message;
    }

    // Getters and Setters
}

更多高级用法

@ControllerAdvice不仅可以捕获异常,还可以用于全局的数据绑定验证、模型属性添加等操作。例如,我们可以在@ControllerAdvice中定义一个方法,用于处理所有控制器中的参数验证错误。

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
    List<String> errors = ex.getBindingResult()
                            .getFieldErrors()
                            .stream()
                            .map(error -> error.getField() + ": " + error.getDefaultMessage())
                            .collect(Collectors.toList());

    ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), String.join(", ", errors));
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

这个方法会捕获所有由@Valid@Validated注解引发的验证错误,并返回一个包含所有字段验证错误的JSON响应。

4. ErrorController vs @ControllerAdvice:谁更适合你?

现在我们已经了解了ErrorController@ControllerAdvice的基本用法,那么它们之间有什么区别呢?什么时候应该使用哪一个呢?让我们通过一个表格来对比它们的功能和适用场景。

特性 ErrorController @ControllerAdvice
作用范围 全局错误处理,主要处理HTTP状态码 局部异常处理,主要用于捕获控制器中的异常
触发条件 当Spring Boot检测到错误时自动调用 当控制器方法抛出异常时调用
返回内容 可以返回HTML页面或JSON响应 主要返回JSON响应,适合API开发
灵活性 适用于全局错误页面的定制 适用于细粒度的异常处理和验证错误处理
适用场景 404、500等常见HTTP错误的统一处理 自定义业务逻辑异常的处理,如用户不存在

从表格中可以看出,ErrorController更适合处理全局的HTTP错误,而@ControllerAdvice则更适合处理业务逻辑中的异常。在实际开发中,我们通常会结合使用两者,以达到最佳的效果。

5. 总结

今天我们一起探讨了Spring Boot中的两种异常处理机制:ErrorController@ControllerAdviceErrorController可以帮助我们处理全局的HTTP错误,而@ControllerAdvice则提供了更加灵活的异常处理方式,适用于业务逻辑中的各种异常情况。

通过合理使用这两种工具,我们可以让我们的应用程序在遇到错误时更加优雅地响应,提升用户体验的同时,也方便我们进行后续的错误排查和修复。

希望这篇文章对你有所帮助!如果你有任何问题,欢迎在评论区留言,我会尽力为你解答。 😊


参考资料:

  • Spring Boot官方文档
  • Spring Framework官方文档
  • Stack Overflow上的相关讨论

(本文不包含外部链接,所有引用的技术文档均为Spring官方文档或社区讨论中的常见内容。)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注