Spring Boot REST API 错误处理指南

已发表: 2022-03-11

在提供有意义的错误消息的同时正确处理 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 /birds 创造一只鸟。

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: java.io.PushbackInputStream@cba7ebc; 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 ,我们将能够定义一个中心点来处理异常并将它们包装在一个ApiError对象中,该对象比默认的 Spring Boot 错误处理机制具有更好的组织。

处理异常

表示成功和失败的 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的情况下,错误消息将是“Malformed JSON request”,并且错误将被封装在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: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }

处理自定义异常

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

处理数据库调用的 Spring 应用程序的一个常见场景是使用存储库类调用通过其 ID 查找记录。 但是如果我们查看CrudRepository.findOne()方法,我们会看到如果找不到对象,它会返回null 。 这意味着如果我们的服务只是调用这个方法并直接返回到控制器,即使没有找到资源,我们也会得到一个 HTTP 代码 200(OK)。 事实上,正确的方法是返回 HTTP/1.1 规范中指定的 HTTP 代码 404 (NOT FOUND)。

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

失败的 REST 调用示例

也就是说,让我们在RestExceptionHandler类中为这个新创建的EntityNotFoundException创建一个ExceptionHandler 。 为此,创建一个名为handleEntityNotFound()的方法并使用@ExceptionHandler对其进行注释,并将类对象EntityNotFoundException.class传递给它。 这表明 Spring 每次抛出EntityNotFoundException时,Spring 都应该调用这个方法来处理它。 使用@ExceptionHandler注释方法时,它将接受各种自动注入的参数,如WebRequestLocale和此处所述的其他参数。 我们将只提供异常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 代码中还有一些其他常见异常的示例,例如MethodArgumentTypeMismatchExceptionConstraintViolationException等。

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

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

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