个性化阅读
专注于IT技术分析

Spring Boot REST API错误处理指南

本文概述

在提供有意义的错误消息的同时正确处理API中的错误是一项非常理想的功能, 因为它可以帮助API客户端正确响应问题。默认行为是返回难以理解的堆栈跟踪, 最终对于API客户端无用。将错误信息划分为字段还可以使API客户端对其进行解析, 并向用户提供更好的错误消息。在本文中, 我们将介绍在使用Spring Boot构建REST API时如何进行正确的错误处理。

人们对一条神秘而冗长的错误消息感到困惑

在过去的几年中, 使用Spring构建REST API成为Java开发人员的标准方法。使用Spring Boot会大大帮助, 因为它删除了许多样板代码并启用了各种组件的自动配置。在应用此处介绍的知识之前, 我们将假定你熟悉使用这些技术进行API开发的基础。如果你仍然不确定如何开发基本的REST API, 那么你应该从这篇关于Spring MVC的文章开始, 或者从另一篇关于构建Spring REST服务的文章开始。

使错误响应更加清晰

在整篇文章中, 我们将使用GitHub上托管的应用程序源代码, 该应用程序实现了REST API, 用于检索代表鸟类的对象。它具有本文描述的功能以及错误处理方案的更多示例。以下是在该应用程序中实现的端点的摘要:

</tr>
GET / birds / {birdId} 获取有关鸟的信息, 如果未找到则抛​​出异常。
GET / birds / noexception / {birdId} 此调用还会获取有关鸟的信息, 但不会在找不到鸟的情况下引发异常。
POST /小鸟 制造一只鸟。

Spring框架MVC模块带有一些出色的功能来帮助错误处理。但是, 留给开发人员使用这些功能来处理异常并将有意义的响应返回给API客户端。

让我们看一下默认的Spring Boot答案的示例, 当我们使用以下JSON对象向/ birds端点发出HTTP POST时, 该对象在” mass”字段上具有字符串” aaa”, 该字符串应为整数:

{
 "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4
}

Spring Boot默认答案, 没有适当的错误处理:

{
 "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: [email protected]; line: 4, column: 17]", "path": "/birds"
}

好吧…响应消息有一些不错的字段, 但是过于关注异常是什么。顺便说一下, 这是Spring Boot的DefaultErrorAttributes类。 timestamp字段是一个整数, 甚至不包含时间戳记所使用的度量单位的信息。exception字段仅对Java开发人员有意义, 并且该消息使API使用者在与他们无关的所有实现细节中迷失了。如果还有更多细节可以从错误源中提取出来, 该怎么办?因此, 让我们学习如何正确处理这些异常并将其包装为更好的JSON表示形式, 以使API客户端的工作更加轻松。

由于我们将使用Java 8日期和时间类, 因此我们首先需要为Jackson JSR310转换器添加Maven依赖项。他们负责使用@JsonFormat批注将Java 8日期和时间类转换为JSON表示形式:

<dependency>
   <groupId>com.fasterxml.jackson.datatype</groupId>
   <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

好的, 让我们定义一个表示API错误的类。我们将创建一个名为ApiError的类, 该类具有足够的字段来保存有关REST调用期间发生的错误的相关信息。

class ApiError {

   private HttpStatus status;
   @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
   private LocalDateTime timestamp;
   private String message;
   private String debugMessage;
   private List<ApiSubError> subErrors;

   private ApiError() {
       timestamp = LocalDateTime.now();
   }

   ApiError(HttpStatus status) {
       this();
       this.status = status;
   }

   ApiError(HttpStatus status, Throwable ex) {
       this();
       this.status = status;
       this.message = "Unexpected error";
       this.debugMessage = ex.getLocalizedMessage();
   }

   ApiError(HttpStatus status, String message, Throwable ex) {
       this();
       this.status = status;
       this.message = message;
       this.debugMessage = ex.getLocalizedMessage();
   }
}
  • status属性保存操作调用状态。它可以是4xx表示客户端错误, 也可以是5xx表示服务器错误。常见的情况是http代码400, 它表示BAD_REQUEST, 例如, 当客户端发送格式错误的字段(例如无效的电子邮件地址)时。

  • timestamp属性保存发生错误的日​​期时间实例。

  • message属性保存有关该错误的用户友好消息。

  • debugMessage属性包含更详细地描述错误的系统消息。

  • subErrors属性保存发生的一系列子错误。这用于表示单个调用中的多个错误。一个示例是验证错误, 其中多个字段的验证失败。 ApiSubError类用于封装那些。

abstract class ApiSubError {

}

@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class ApiValidationError extends ApiSubError {
   private String object;
   private String field;
   private Object rejectedValue;
   private String message;

   ApiValidationError(String object, String message) {
       this.object = object;
       this.message = message;
   }
}

因此, ApiValidationError是扩展ApiSubError并表示在REST调用期间遇到的验证问题的类。

在下面, 你将看到我们实施了此处描述的改进后正在生成的JSON响应的一些示例, 目的只是为了了解到本文结尾处的内容。

这是调用端点GET / birds / 2时未找到实体时返回的JSON的示例:

{
 "apierror": {
   "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}"
 }
}

这是发出POST / birds调用时返回的JSON的另一个示例, 该调用的值无效于鸟的质量:

{
 "apierror": {
   "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [
     {
       "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000"
     }
   ]
 }
}

Spring Boot错误处理

让我们探究一些Spring注释, 这些注释将用于处理异常。

RestController是处理REST操作的类的基本注释。

ExceptionHandler是一个Spring注释, 它提供一种机制来处理在执行处理程序(控制器操作)期间引发的异常。如果在控制器类的方法上使用此注释, 它将仅用作处理在此控制器内引发的异常的入口。总之, 最常见的方法是在@ControllerAdvice类的方法上使用@ExceptionHandler, 以便将异常处理应用于全局或应用于控制器的子集。

ControllerAdvice是Spring 3.2中引入的注释, 顾名思义, 它是多个控制器的” Advice”。它用于使单个ExceptionHandler可以应用于多个控制器。这样, 我们可以在一个地方定义如何处理此类异常, 并且当从此ControllerAdvice覆盖的类引发异常时, 将调用此处理程序。可以通过在@ControllerAdvice上使用以下选择器来定义受影响的控制器的子集:annotations(), basePackageClasses()和basePackages()。如果未提供选择器, 则ControllerAdvice将全局应用于所有控制器。

因此, 通过使用@ExceptionHandler和@ControllerAdvice, 我们将能够定义一个中心点来处理异常, 并将其包装在比默认Spring Boot错误处理机制更好的组织中的ApiError对象中。

处理异常

表示成功和失败的REST客户端调用所发生的情况

下一步是创建将处理异常的类。为简单起见, 我们将其称为RestExceptionHandler, 它必须从Spring Boot的ResponseEntityExceptionHandler扩展。我们将扩展ResponseEntityExceptionHandler, 因为它已经提供了Spring MVC异常的一些基本处理, 因此我们将在改进现有异常的同时添加新异常的处理程序。

覆盖在ResponseEntityExceptionHandler中处理的异常

如果你查看ResponseEntityExceptionHandler的源代码, 则会看到很多称为handle ******()的方法, 例如handleHttpMessageNotReadable()或handleHttpMessageNotWritable()。首先, 让我们看看如何扩展handleHttpMessageNotReadable()以处理HttpMessageNotReadableException异常。我们只需要在RestExceptionHandler类中重写方法handleHttpMessageNotReadable():

@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

   @Override
   protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
       String error = "Malformed JSON request";
       return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex));
   }

   private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
       return new ResponseEntity<>(apiError, apiError.getStatus());
   }

   //other exception handlers below

}

我们已经声明, 如果抛出HttpMessageNotReadableException, 则错误消息将为”格式错误的JSON请求”, 并且错误将封装在ApiError对象内。在下面, 我们可以看到REST调用的答案, 此新方法已被覆盖:

{
 "apierror": {
   "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: [email protected]; line: 4, column: 17]"
 }
}

处理自定义异常

现在, 我们将看到如何创建一个方法来处理Spring Boot的ResponseEntityExceptionHandler中尚未声明的异常。

一个处理数据库调用的Spring应用程序的常见方案是调用一个使用存储库类按其ID查找记录的记录。但是, 如果我们查看CrudRepository.findOne()方法, 将会发现如果找不到对象, 则该方法返回null。这意味着, 如果我们的服务仅调用此方法并直接返回到控制器, 即使找不到资源, 我们也会获得HTTP代码200(确定)。实际上, 正确的方法是返回HTTP / 1.1规范中指定的HTTP代码404(未找到)。

为了处理这种情况, 我们将创建一个名为EntityNotFoundException的自定义异常。这是一个自定义创建的异常, 与javax.persistence.EntityNotFoundException不同, 因为它提供了一些简化对象创建的构造函数, 并且可以选择以不同的方式处理javax.persistence异常。

REST调用失败的示例

也就是说, 让我们在RestExceptionHandler类中为此新创建的EntityNotFoundException创建一个ExceptionHandler。为此, 创建一个名为handleEntityNotFound()的方法, 并使用@ExceptionHandler对其进行注释, 并将类对象EntityNotFoundException.class传递给该方法。这表明Spring每次抛出EntityNotFoundException时, Spring应该调用此方法来处理它。使用@ExceptionHandler注释方法时, 它将接受各种自动注入的参数, 如WebRequest, Locale和此处所述的其他参数。我们只提供EntityNotFoundException异常本身作为此handleEntityNotFound方法的参数。

@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
  
   //other exception handlers
  
   @ExceptionHandler(EntityNotFoundException.class)
   protected ResponseEntity<Object> handleEntityNotFound(
           EntityNotFoundException ex) {
       ApiError apiError = new ApiError(NOT_FOUND);
       apiError.setMessage(ex.getMessage());
       return buildResponseEntity(apiError);
   }
}

大!在handleEntityNotFound()方法中, 我们将HTTP状态代码设置为NOT_FOUND并使用新的异常消息。这是现在GET / birds / 2端点的响应:

{
 "apierror": {
   "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}"
 }
}

总结

控制异常处理非常重要, 因此我们可以将这些异常正确映射到ApiError对象, 并提供重要的信息, 使API客户端可以知道发生了什么。从这里开始的下一步将是为在应用程序代码内引发的异常创建更多的处理程序方法(带有@ExceptionHandler的方法)。 GitHub代码中还有其他一些常见异常的示例, 例如MethodArgumentTypeMismatchException, ConstraintViolationException和其他示例。

以下是一些有助于撰写本文的其他资源:

  • Baeldung-使用Spring进行REST的错误处理

  • Spring博客-Spring MVC中的异常处理

赞(0) 打赏
未经允许不得转载:srcmini » Spring Boot REST API错误处理指南
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

微信扫一扫打赏