在Spring/Springboot中异步处理异常

256 阅读3分钟

目前,每当出现特殊情况时,客户休息应用程序都会返回一个 ResponseEntity(一个由状态、标头和正文组成的 Http 响应包装器)。例如,在请求详细信息时找不到客户。

@GetMapping("{customerId}")
public ResponseEntity<Customer> findCustomers( 
                @PathVariable("customerId") Long id) {
    return customerRepo
        .findById(id)
        .map(ResponseEntity::ok)
        .orElse(new ResponseEntity("Customer "+id+" not found" 
             ,HttpStatus.NOT_FOUND));
}

没有适当的异常处理策略。实施一个将使代码更易于阅读,并将“常规代码”与发生异常情况时要执行的操作分开。

上面的代码将返回 404 错误和如下图所示的信息。

图片描述

现在让我们看一下在我们的应用程序中管理异常的第一个机制。

@ResponseStatus 的自定义异常

它用应该返回的状态代码()和原因()标记方法或异常类。例如,自定义异常可以声明如下:

@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class CustomerNotFoundException extends RuntimeException {
    public CustomerNotFoundException(Long id) {
        super("Customer was "+id+" not found");
    }
}

现在在控制器中,将抛出自定义异常

@GetMapping("{customerId}")
public ResponseEntity<Customer> findCustomers( 
                @PathVariable("customerId") Long id) {
    return customerRepo
        .findById(id)
        .map(ResponseEntity::ok)
        .orElseThrow(() -> new CustomerNotFoundException(id));    
}

根据 Spring 文档,此注释不适用于 REST API,因为将使用 HttpServletResponse.sendError 方法,并且 Servlet 容器通常会编写 HTML 错误页面。这意味着我们无法控制身体。
另一个缺点是它将异常与 Spring 框架高度耦合。我们可能希望避免侵入异常类(因为它是应用程序核心架构的一部分)并防止它直接依赖于 Spring。

响应状态异常

Spring 5 引入了一个新的 Exception 类,它接受状态代码和可选的原因。这为以多种不同方式管理相同情况/案例提供了一个很好的解决方案。
但是我们仍然没有将全局规则应用于整个应用程序的共同点,而且它可能导致代码重复。

我们的代码看起来像

@GetMapping("{customerId}")
public ResponseEntity<Customer> findCustomers( 
                @PathVariable("customerId") Long id) {
    return customerRepo
        .findById(id)
        .map(ResponseEntity::ok)
        .orElseThrow(() -> new ResponseStatusException( 
            HttpStatus.NOT_FOUND,"Customer "+id+" not found." ));    
}

获取不存在的客户时的输出。

{
    "timestamp": "2023-04-16T14:10:49.752+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/api/v1/customers/100"
}

作为安全措施,默认情况下 Spring 不会在响应中显示错误消息。这是为了防止服务器泄露详细信息。

server.error.include-message=always

现在响应中包含消息。

{
    "timestamp": "2023-04-16T17:09:36.281+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "Customer 1001 not found.",
    "path": "/api/v1/customers/1001"
}

上面的 JSON 可能不符合我们的要求。我们将在下一节中看到如何对任何异常使用自定义 JSON 错误响应。

使用@ExceptionHandler 进行异常处理

它允许在方法中管理异常。允许使用它注释的处理程序方法具有非常灵活的签名。在我们的例子中,该方法将异常类型作为参数并返回一个 ResponseEntity。

它的工作方式是当抛出异常时,处理程序方法将拦截它并返回特定的响应(如果有的话)。更多信息可以在这里找到

首先,我们将创建一个记录来表示我们要发送回客户端的响应。它是一个非常简单的不可变类,包含状态、消息和时间戳三个属性。

public record RestErrorResponse(int status, String message, 
                                LocalDateTime timestamp) {}

接下来,控制器将添加一个新方法来处理异常。

@ExceptionHandler
public ResponseEntity<RestErrorResponse> handleException( 
                            CustomerNotFoundException ex) {
    var response = new RestErrorResponse( 
        HttpStatus.NOT_FOUND.value(), ex.getMessage(), 
        LocalDateTime.now();
    return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}

输出将是

{
    "status": 404,
    "message": "Customer 1001 not found!!",
    "timestamp": "2023-04-16T12:25:10.3432534"
}

这在控制器级别工作得很好,但如果我们需要为我们的应用程序设置全局配置,那将是一个限制。此外,我们可能不希望控制器负责处理异常并将该问题与它们分开。

使用@ControllerAdvice 进行全局配置

@ControllerAdvice 是 Spring AOP 的一部分,它连接到 Spring MVC 项目。它的操作类似于提供预处理请求和后处理响应功能的过滤器/拦截器。它允许集中处理异常并促进代码重用。

首先,必须删除或注释上一节中的异常处理程序方法。其次,创建一个新类并将代码移至其中,如以下代码片段所示:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomerNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    RestErrorResponse handleCustomerNotFoundException( 
                         CustomerNotFoundException ex) {
        return new RestErrorResponse( 
            HttpStatus.NOT_FOUND.value(), 
            ex.getMessage(), 
            LocalDateTime.now());
    }

    // Handle any other exception too.
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    RestErrorResponse handleException(Exception ex) {
        return new RestErrorResponse( 
            HttpStatus.BAD_REQUEST.value(), 
            ex.getMessage(), 
            LocalDateTime.now());
    }
}

@RestControllerAdvice 注释是@ControllerAdvice 和@ResponseBody 的组合,这对于 REST 应用程序非常方便。请注意,返回 httd 代码需要 @ResponseStatus,正文将是我们的 RestErrorResponse 记录。

同样,命中端点http://localhost:8080/api/v1/customers/1001时的输出是预期的。

{
    "status": 404,
    "message": "Customer 1001 not found!",
    "timestamp": "2023-04-16T13:39:26.1711689"
}

概括

  1. @ResponseStatus:不适用于 rest 应用程序,因为服务器将显示一个 HTML 错误页面并且它会导致高度耦合。
  2. ResponseStatusException:它是一种快速且通用的解决方案。但是,它会导致代码重复,并且无法完全控制正文。
  3. @ExceptionHandler:仅适用于声明该方法的控制器。
  4. @ControllerAdvice:以集中方式提供全局配置。生产就绪应用程序的最佳实践。