如何正确的处理API的返回信息,让返回的错误信息提供更多的含义是一个非常值得做的功能。
默认一般返回的都是难以理解的堆栈信息,然而这些信息也许对于API的客户端来说有可能并没有多大用途,并没有多大意义。
如果我们把错误的信息分成多个字段,这样api客户端就可以解析这些信息,然后给用户反馈更好的错误message。
在本文中,我们就来介绍在我们使用spring boot来构建REST API时如何更好的更恰当的处理错误信息。
使用Spring来构建REST API现在基本上已经变成了java开发者事实上的标准。
如果你使用Spring Boot的话,就更方便了,因为它帮你搞了很多的样板代码,而且通过auto-configuration可以集成各种组件。
我们将假设你在应用此处所述的知识之前已经比较熟悉使用这些技术的API开发的基础知识。
如果你仍然不确定如何开发基本的REST API,那么你应该先去了解下有关Spring MVC的文章,或者关于构建Spring REST服务的文章。
让Error响应更清晰
在本文中,我们将使用托管在GitHub(源码spring-boot-exception-handling在文末的阅读原文里,链接:https://github.com/importsource/spring-boot-exception-handling) 上的spring-boot-exception-handling应用程序上的源代码来通过REST API来查询“鸟”这个对象。 代码里有本文中描述的功能和更多的错误处理方案的示例。 以下是该应用程序中实现的几个endpoint:
Spring框架的MVC模块提供了一些很好的功能来帮助处理错误。 但是, 它把处理异常的事情扔给了开发人员,需要开发人员自己来处理异常,然后向API客户端返回返回有意义的响应。
我们来看一下Spring Boot的默认做法。 当我们把下面的一个对象通过 HTTP POST 发送到 /bird 端点时,我们故意给“mass”字段传递一个字符串“aaa”,其实这个字段类型是一个整数:
然后我 们来看一下 Spring Boot的默认应答。没有任何额外的错误处理:
嗯。。。这个响应信息确实提供了一些不错的字段,但是它更侧重的时候抛出一个底层的变成异常。顺便说一句,这是Spring Boot中的DefaultErrorAttributes类。 时间戳字段是一个整数,甚至不携带时间戳所在的度量单位的信息。异常字段的话,可能只有Java开发人员看到这个比较开心,这些信息让API消费者也陷入了思索API服务端究竟发生了什么内部的编程错误。 如果我们从这些编程异常的内容中抽象出更多的细节是不是更好一点?那么就让我们学习下如何正确地处理这些异常,并将它们转成更好的JSON表示形式,使我们的API客户端理解起来更加的轻松。
因为我们接下来要使用到Java 8日期和时间类,我们首先就加个Jackson JSR310转换器的maven依赖。 这样我们就可以使用@JsonFormat这个注解来将Java 8的日期和时间类转换为JSON来表示:
好的,来定义一个表示API错误的类。 我们将创建一个名为ApiError的类,其具有足够的字段来保存REST调用期间发生的错误的相关信息。
status 属性:保存操作调用状态。 比如4xx客户端错误或5xx服务端错误。 一个常见的情况是比如http代码400,表示BAD_REQUEST,这种情况是当客户端例如发送了格式不正确的字段,比如一个无效的电子邮件地址。
timestamp 属性:保存发生错误的日期时间。
message 属性:保存有关错误的对用户友好的消息。
debugMessage 属性:是更详细的描述错误的系统消息。
subErrors 属性:包含发生的一系列子错误。 这用于在单个调用中出现多个错误。 比如多个字段验证失败的验证错误。ApiSubError类就是用来封装这一系列子错误的。
ApiValidationError扩展了ApiSubError。它表示在REST调用期间遇到的验证问题的类。
下面,你将看到在我们实现了这里所做的改进之后生成的JSON响应的例子,仅仅是为了了解本文接下来的内容。
下面的例子就是当一个实体没有找到后返回的样子(端点:GET /birds/2):
下面是当我们发送一个POST /birds JSON串后,里边包含了非法的值赋值给了鸟的mass字段,然后返回了如下错误信息:
接下来我们介绍一下将要用于处理异常的Spring注解。
RestController 是处理REST操作放置在类上的基础注解。
ExceptionHandler
ExceptionHandler 是一个Spring注解,它提供了一种机制来处理在处理程序执行过程中抛出的异常(比如Controller操作)。 这个注解(如果用于Controller类的话)将用作处理仅在此Controller中抛出的验证yi'cyi异常。 总而言之,最常用的方法是在@ControllerAdvice类的方法上使用@ExceptionHandler,以便将异常处理应用到所有的Controller或指定的Controller子集上。
ControllerAdvice
ControllerAdvice 是Spring 3.2中引入的一个注解,顾名思义,是“Advice”多个Controller。 你可以启用一个ExceptionHandler,然后让多个Controller都遵循它所定义的异常处理。 这样的话,我们就只需要在一个地方定义好如何处理某一个异常,并且当这个ControllerAdvice涵盖的类抛出该异常时,这个处理程序就将会被调用。 如果你只是希望某一些Controller受影响,那么你可以通过在@ControllerAdvice上加这几个选择器属性来限制:annotations(),basePackageClasses()和basePackages())。 如果没有添加这些选择器,则ControllerAdvice将应用于所有Controller。
所以通过使用@ExceptionHandler和@ControllerAdvice,我们可以定义一个中心点来处理异常,并将它们包装在一个比Default错误处理机制组织错误信息更好的ApiError对象中。
下一步是创建处理异常的类。 简单点说,我们称之为RestExceptionHandler,它继承自Spring Boot的ResponseEntityExceptionHandler。 然后我们扩展ResponseEntityExceptionHandler,因为它已经提供了Spring MVC异常的一些基本处理,接下来我们就针对一些新的异常添加一些新的handler,算是对现有的handler的一个扩展和改进。
如果你看看ResponseEntityExceptionHandler的源代码,你会看到很多方法叫做handle ******(),如handleHttpMessageNotReadable()或handleHttpMessageNotWritable()。 我们先来看看,我们是如何通过扩展handleHttpMessageNotReadable()来处理HttpMessageNotReadableException异常们的。 其实只需要在RestExceptionHandler类中覆盖方法handleHttpMessageNotReadable()如下:
之前已说过,如果HttpMessageNotReadableException被抛出,错误的message将是“ Malformed JSON request( 格式错误的JSON请求)”,并且该错误将被封装在ApiError对象内。
下面就是我们扩展后的REST调用的响应json:
接下来介绍如何创建一个方法来处理在Spring Boot的ResponseEntityExceptionHandler中没有被声明处理的异常。
一个用于处理数据库调用的Spring应用程序的常见场景就是使用存储库类通过其ID来查找一条或多条记录。 但是,我们发现CrudRepository.findOne()方法中,如果找不到数据,那么就返回null。 这意味着如果我们的服务只是调用该方法并直接返回给Controller,即使没有找到资源,我们也会得到HTTP代码 200(OK)。 事实上,正确的方法应该是返回HTTP / 1.1规范中指定的HTTP code 404(NOT FOUND)。
为了处理这种情况,我们可以创建一个名为EntityNotFoundException的自定义异常。 这是一个自定义创建的异常,与javax.persistence.EntityNotFoundException不同 ,因为它提供了一些缓解对象创建的构造函数,并且可以选择以 不同方式处理javax.persistence异常。
然后,我们在RestExceptionHandler类中为这个新创建的EntityNotFoundException创建一个ExceptionHandler。 其实就是创建一个名为handleEntityNotFound()的方法,并使用@ExceptionHandler对其进行注解,将类对象EntityNotFoundException.class传递给它。 这表明Spring每次抛出EntityNotFoundException时,Spring应该调用此方法来处理它。 当使用@ExceptionHandler注解方法时,它将接受多种自动注入的参数,如WebRequest,Locale和其他。 下面我们就把异常EntityNotFoundException本身作为下面这个handleEntityNotFound方法的参数。
好! 在handleEntityNotFound()方法中,我们将HTTP状态代码设置为NOT_FOUND并使用了新的异常消息。 以下是 GET /birds/2 终端的响应:
控制异常处理非常重要,所以我们可以将这些异常映射到ApiError对象,然后向API客户端提供了更有意义的信息,让客户端知道发生了什么。 然后就是如何为应用程序代码中抛出的异常创建更多的手工方法(带有@ExceptionHandler的方法)。 还有一些其他常见异常的例子,例如MethodArgumentTypeMismatchException,ConstraintViolationException等等。
本文源码可以点击阅读原文查看。