目前,每当出现特殊情况时,客户休息应用程序都会返回一个 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"
}
概括
- @ResponseStatus:不适用于 rest 应用程序,因为服务器将显示一个 HTML 错误页面并且它会导致高度耦合。
- ResponseStatusException:它是一种快速且通用的解决方案。但是,它会导致代码重复,并且无法完全控制正文。
- @ExceptionHandler:仅适用于声明该方法的控制器。
- @ControllerAdvice:以集中方式提供全局配置。生产就绪应用程序的最佳实践。