Java笔记··By/蜜汁炒酸奶

Spring Boot REST API错误处理指南

前言

本来是5号来的文章,无奈最近准备换工作,一直拖着没写,今天搜索偶然看见有人已经翻译完了,由于时间原因这次就直接转载下吧,现附上英文原文及相关信息,最后再附上译文原文:

原文Guide to Spring Boot REST API Error Handling 作者:BRUNO LEITE 翻译:雁惊寒

文章正文

API在提供错误消息的同时进行适当的错误处理,是一个非常有用的功能,因为这能让API客户端对问题进行正确地响应。API处理错误的默认行为通常是返回难以理解的堆栈跟踪,而这些对API客户端来说并没有什么用。将错误信息切分成多个字段可以方便API客户端的解析,以此向用户提供更加友好的错误消息。本文将介绍在使用Spring Boot构建REST API的时候如何进行合适的错误处理。 Spring Boot REST API错误处理指南 在过去几年里,使用Spring构建REST API已经成为Java开发人员的标准方法。而使用Spring Boot则有助于API的构建,因为它删除了大量的样板代码,并实现了各种组件的自动化配置。我们假设你对利用这些技术进行API开发的基础知识已经非常了解。如果你对如何开发基本的REST API并不熟悉,那么你应该先阅读这篇关于Spring MVC的文章或另一篇有关构建Spring REST服务的文章。

让错误响应更清晰

在本文中,我们将实现一个通过REST API来检索鸟类(代表一个对象)的应用程序,代码托管在GitHub上。这个示例包含了本文描述的所有功能,以及比较多的错误处理场景。以下是该程序实现的端点URL:

|                                   |                                       |
| --------------------------------- | ------------------------------------- |
| `GET /birds/{birdId}`             | 获取鸟的相关信息,如果没有找到,则抛出异常。                |
| `GET /birds/noexception/{birdId}` | 这个调用也可以获取鸟的相关信息,但是即使没有找到相应的鸟,也不会抛出异常。 |
| `POST /birds`                     | 创建一只鸟。                                |

Spring框架的MVC模块在错误处理方面提供了一些很不错的功能,但是这些功能需要由开发人员主动调用,才能返回对API客户端的有具体意义的响应。 我们来看一下这个Spring Boot默认响应的例子。当我们向/birds发送一个HTTP POST的时候,消息内容是下面这个JSON对象,字段“mass”的值是字符串“aaa”,这个字段本应该填一个整数:

{
 "scientificName": "Common blackbird",
 "specie": "Turdus merula",
 "mass": "aaa",
 "length": 4
}
1
2
3
4
5
6

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"
}
1
2
3
4
5
6
7
8

呃…… 响应消息里有一些很有用的字段,但它里面有关异常的内容太多了。 顺便说一句,这是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>
1
2
3
4

好的,我们来定义一个表示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();
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

[callout class=“info” title=“”]

  • status属性保存了操作调用的状态。 比如,4xx表示客户端错误,5xx意味着服务器错误。 比较常见的情况是:http返回码400表示BAD_REQUEST,例如,客户端发送了格式不正确的字段(如无效的电子邮件地址)。
  • timestamp属性保存了发生错误的日期时间。
  • message属性保存了对用户友好的错误信息。
  • debugMessage属性更详细地描述了错误。
  • subErrors属性保存了发生的子错误的数组。 这用于表示在单个调用中出现的多个错误。比如,校验的时候有多个字段验证失败。用ApiSubError类进行封装。

[/callout]

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;
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ApiValidationError是类ApiSubError的扩展类,表示REST调用时遇到的校验问题。 下面,你将看到几个JSON响应的例子,这些响应根据我们上面的描述做了改进。 以下这个JSON是在调用URLGET /birds/2后找不到实体的时候返回的:

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

下面是调用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"
     }
   ]
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Spring Boot 错误处理

我们来探讨一些用于异常处理的Spring注解。 RestController是用于REST操作类的最基本的注解。 ExceptionHandler这个Spring注解提供了一种机制,用来处理在执行程序期间抛出的异常。此注解将作为处理此控制器中抛出的异常的入口点。总而言之,最常见的方法是在@ControllerAdvice类的方法上使用@ExceptionHandler,以便将异常处理应用于全局或控制器的子集。 ControllerAdvice是Spring 3.2中引入的注解,顾名思义,它是多控制器的“建议”。它使得单个ExceptionHandler应用于多个控制器上。这样我们可以在一个地方定义如何处理这样的异常,当ControllerAdvice覆盖的类抛出异常时,这个处理程序就会被调用。受影响的控制器子集可以在@ControllerAdvice上使用以下选择器进行定义:annotations()basePackageClasses()basePackages()。如果没有提供选择器,则ControllerAdvice将应用于全局所有的控制器。 所以,通过使用@ExceptionHandler@ControllerAdvice,我们可以定义一个用于处理异常的中心点,并将异常包装在ApiError对象中,这比Spring Boot默认的错误处理机制更好。

处理异常

Spring Boot REST API错误处理指南 下一步是创建处理异常的类。为了简单起见,我们称之为RestExceptionHandler,它必须继承自Spring Boot的ResponseEntityExceptionHandler。我们也将从ResponseEntityExceptionHandler继承,因为它已经提供了对Spring MVC异常的一些基本处理方法,所以,我们将改进现有的异常处理手段,并同时添加针对新异常的处理。 如果看一下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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果抛出一个HttpMessageNotReadableException,则错误消息将是“Malformed JSON request(格式错误的JSON请求)”,该错误封装在ApiError对象内。下面我们可以看到新的应答:

{
 "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]"
 }
}
1
2
3
4
5
6
7
8

处理自定义异常

现在,我们来看看如何创建一个方法来处理没有在Spring Boot的ResponseEntityExceptionHandler中声明的异常。 Spring程序处理数据库调用的一个常见场景是使用库类通过id去查找记录。但是,如果研究一下CrudRepository.findOne()方法,我们会发现,如果找不到对象,它将返回null。这意味着如果我们的服务只是调用这个方法并直接返回给控制器,那么即使找不到资源,我们也会得到HTTP返回码200(OK)。实际上,正确的方法是返回HTTP/1.1规范中指定的HTTP返回码404(NOT FOUND)。 为了处理这种情况,我们将创建一个名为EntityNotFoundException的自定义异常。它与javax.persistence.EntityNotFoundException不同,因为它提供的一些构造函数可以用来选择以不同的方式处理javax.persistence异常。 Spring Boot REST API错误处理指南 也就是说,我们可以在RestExceptionHandler类中为这个新创建的EntityNotFoundException创建一个ExceptionHandler。为此,创建一个名为handleEntityNotFound()的方法,并使用@ExceptionHandler对其进行注释,将类对象EntityNotFoundException.class传递给它。这表示每次抛出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);
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

太好了!我们在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}"
 }
}
1
2
3
4
5
6
7

结论

对异常处理的控制非常重要,所以我们需要将这些异常正确映射到ApiError对象上,以提供给API客户端一些重要的信息,让它们知道发生了。接下来的步骤就是为抛出的异常创建更多的处理方法(带有@ExceptionHandler的方法)。你可以在GitHub代码仓库中找到更多的示例。 这里另外还有一些资源,可对本文起到补充作用:

了解基础知识

[toggle hide=“yes” title=“为什么API应有一个统一的错误格式?” color=“”] 这样API客户端就可以正确地解析错误对象。一个更复杂的错误可以通过ApiSubError类的实现,并提供关于这个问题的更多细节,这样客户就可以知道要采取哪些操作。 [/toggle] [toggle hide=“yes” title=“Spring如何知道使用哪个ExceptionHandler?” color=“”] Spring MVC中有一个叫ExceptionHandlerExceptionResolver的类。大多数工作都发生在doResolveHandlerMethodException()方法中。 [/toggle] [toggle hide=“yes” title=“哪些信息对API消费者来说很重要?” color=“”] 通常重要的是要说明错误来自哪里。是否有任何输入参数发生错误?提供一些如何修复失败的呼叫的指导也很重要。 [/toggle]

附录

译文来源:Spring Boot REST API错误处理指南-csdn

预览
Loading comments...
0 条评论

暂无数据

example
预览