资讯专栏INFORMATION COLUMN

(六讲)Spring Boot REST API异常处理指南

bbbbbb / 2250人阅读

摘要:在本讲中,通过一个精简的项目,着重介绍一些的异常处理技巧。现在,为了快熟实现自定义异常信息处理类,并让其正常工作,我们可以直接扩展提供的类来定义用户异常信息处理类。将异常报告封装到对象中,并回传给。

能够正确的处理REST API程序抛出的异常以及返回友好的异常信息是一件非常重要的事情,因为它可以帮助API客户端正确的对服务端的问题作出正确的响应。这有助于提高REST API的服务质量。Spring Boot默认返回的异常信息对于API客户端来说是晦涩难懂的,只有开发者才会关注那些堆栈异常报告。在本讲中,将对如何处理好Spring REST API异常信息做一个梳理。

最近一段时间,Spring Boot成为了Java开发圈子的网红,越来越多的开发者选择Spring Boot来构建REST API。使用Spring Boot,能够帮助开发者减少模板代码和配置文件的编写工作量。Spring Boot开箱即用的特性,受到广大开发者的热宠。在本讲中,通过一个精简的Demo项目,着重介绍一些Spring Boot REST API的异常处理技巧。

如果你不想阅读本次内容,只是想快速获得相关的源码,那你可以直接跳转到文章的结尾,找到Gihub仓库链接,通过该链接,你可以轻松的获得本次内容的全部源码。

1. 定义明确的异常信息

当程序发送错误时,不应该将晦涩的堆栈报告信息返回给API客户端,从某种意义将,这是一种不礼貌的和不负责任的行为。现在,我们将模拟这样一个需求,API客户端可以向服务端发送请求以获取一个或者多个用户信息,同时还可以发送请求创建一个新的用户信息。下面是大致的一个API信息:

API 名称 说明
GET /users/{userId} 根据用户ID检索用户信息,如果没有找到,则返回用户未找到异常信息
GET /users 根据传入的ID集合,检索用户信息,若未找到,返回未找到用户异常信息
POST /users 创建一个新的用户

Spring MVC为我们提供了一些很有用的功能,以帮助我们解决系统的异常信息,并将有用的提示信息返回给API客户端。

以 POST /users 创建一个新用户为例,当我们提供正常的用户数据并请求此接口时,REST API将返回如下的提示信息:

{ "id": 2, "username": "wukong", "age": 52, "height": 170 }

现在,将用户年龄修改为200岁,身高修改为500厘米,用户名为rulai ,在此请求此REST API,观察API的返回信息:

{ "restapierror": { "status": "BAD_REQUEST", "timestamp": "2019-05-19 06:04:47", "message": "Validation error", "subErrors": [ { "object": "user", "field": "height", "rejectedValue": 500, "message": "用户身高不能超过250厘米" }, { "object": "user", "field": "age", "rejectedValue": 200, "message": "用户年龄不能超过120岁" } ] } }

如上所示,当API客户端提供不正确的数据时,REST API返回了格式良好的异常提示信息,timestamp由原来的整形时间戳格式化为一个可读的日期+时间,同时还详细列举了详细的错误报告。

2. 包装异常信息

为了能够提供一个可读的JSON格式异常信息给API客户端,我们需要在项目中引入Jackson JSR 310的依赖包,使用其提供的@JsonFormat注解将Java中的日期和时间按照我们给定的日期和时间模板进行格式化。现在,将下面的依赖包加入的Maven pom.xml文件中:

com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.9.8

依赖就绪后,我们需要提供一个异常信息的包装类:RestApiError。它将负责对REST API抛出的异常信息进行封装:

public class RestApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List subErrors; private RestApiError(){ timestamp = LocalDateTime.now(); } RestApiError(HttpStatus status){ this(); this.status = status; } RestApiError(HttpStatus status,Throwable ex){ this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } RestApiError(HttpStatus status,String message,Throwable ex){ this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }

status 属性用于记录响应状态。它沿用了HttpStatus的所有状态吗,如4xx和5xx。

timestamp属性用于记录发送错误的时间

message属性用于记录自定义的异常消息,通常是对API客户端友好的提示信息

debugMessage属性用于记录更为详细的错误报告

subErrors属性用于记录异常附带的子异常信息,如用户实体中字段校验信息等

RestApiSubError类用于记录更为细致的异常信息,通常为实体类中字段校验失败的异常报告:

abstract class RestApiSubError{} @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class RestApiValidationError extends RestApiSubError{ private String object; private String field; private Object rejectedValue; private String message; RestApiValidationError(String object,String message){ this.object = object; this.message = message; } }

RestApiSubError类是一个抽象的空类,具体的扩展将在RestApiValidationError中进行实现。RestApiValidationError类将记录实体类中(如本讲中的User对象)属性校验失败报告。

现在,我们来校验GET /users/1 API,检索用户ID为1的用户信息:

{ "id": 1, "username": "ramostear", "age": 28, "height": 170 }

REST API成功的返回了用户信息,接下来我们传入一个系统不存在的用户ID,看看REST API返回什么信息:

GET /users/100

{ "restapierror": { "status": "NOT_FOUND", "timestamp": "2019-05-19 06:31:17", "message": "User was not found for parameters {id=100}" } }

通过上述的JSON信息我们可以看到,检索不存在的用户信息,REST API返回了友好的提示信息。在一开始的时候我们测试提供不合符规范的用户年龄和身高信息,接下来我们在来测试一下提供一个空的用户名,观察REST API返回的信息:

{ "restapierror": { "status": "BAD_REQUEST", "timestamp": "2019-05-19 06:37:46", "message": "Validation error", "subErrors": [ { "object": "user", "field": "username", "rejectedValue": "", "message": "不能为空" } ] } }

3. Spring Boot 处理异常信息的流程

Spring Boot 处理REST API异常信息将会涉及到三个注解:

@RestController : 负责处理REST API具体操作逻辑的注解

@ExceptionHandler : 负责处理@RestController标注的类中抛出的异常的注解

@ControllerAdvice : 能够将@ExceptionHandler标注的方法集中到一个地方进行处理的注解

@ControllerAdivice注解是在Spring 3.2版本中新增的一个注解,它能够将单个由@ExceptionHandler注解标注的方法应用到多个控制器中。使用它的好处是我们可以在一个统一的地方同时处理多个控制器抛出的异常,当控制器有异常抛出时,ControllerAdvice会根据当前抛出的异常类型,自动匹配对应的ExceptionHandler;当没有特定的Exception可用时,将调用默认的异常信息处理类来处理控制器抛出的异常(默认的异常信息处理类)。

下面,我们通过一张流程示例图,更为直观的了解Spring Application处理控制器异常信息的全部过程:

在图中,蓝色箭头表示正常的请求和响应过程,红色箭头表示发生异常的请求和响应过程。

4. 自定义异常信息处理类

Spring Framework自带的异常信息处理类往往不能满足我们实际的业务需求,这就需要我们定义符合具体情况的异常信息处理类,在自定义异常信息处理类中,我们可以封装更为详细的异常报告。

自定义异常信息处理类,我们可以站在“巨人”的肩膀上,快速封装自己的异常信息处理类,而不必要从头开始造“轮子”。现在,为了快熟实现自定义异常信息处理类,并让其正常工作,我们可以直接扩展Spring 提供的ResponseEntityExceptionHandler类来定义用户异常信息处理类。ResponseEntityExceptionHandler已经提供了很多可用的功能,我们只需要扩展该类或者覆盖其提供的方法即可。

打开ResponseEntityExceptionHandler类,我们可以看到如下的源码:

public abstract class ResponseEntityExceptionHandler { //不支持的HTTP请求方法异常信息处理方法 protected ResponseEntity handleHttpRequestMethodNotSupported(...){...} //不支持的HTTP媒体类型异常处理方法 protected ResponseEntity handleHttpMediaTypeNotSupported(...){...} //不接受的HTTP媒体类型异常处方法 protected ResponseEntity handleHttpMediaTypeNotAcceptable(...){...} //请求路径参数缺失异常处方法 protected ResponseEntity handleMissingPathVariable(...){...} //缺少servlet请求参数异常处理方法 protected ResponseEntity handleMissingServletRequestParameter(...){...} //servlet请求绑定异常 protected ResponseEntity handleServletRequestBindingException(...){...} //不支持转换 protected ResponseEntity handleConversionNotSupported(...){...} //类型不匹配 protected ResponseEntity handleTypeMismatch(...){...} //消息无法检索 protected ResponseEntity handleHttpMessageNotReadable(...){...} //HTTP消息不可写 protected ResponseEntity handleHttpMessageNotWritable(...){...} //方法参数无效 protected ResponseEntity handleMethodArgumentNotValid(...){...} //缺少servlet请求部分 protected ResponseEntity handleMissingServletRequestPart(...){...} //绑定异常 protected ResponseEntity handleBindException(...){...} //没有发现处理程序异常 protected ResponseEntity handleNoHandlerFoundException(...){...} //异步请求超时异常 @Nullable protected ResponseEntity handleAsyncRequestTimeoutException(...){...} //内部异常 protected ResponseEntity handleExceptionInternal(...){...} }

我们选择性的覆盖几个常用的异常处理方法,并添加我们自定义异常处理方法:

public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(UserNotFoundException.class) protected ResponseEntity handleUserNotFound(UserNotFoundException ex){ RestApiError apiError = new RestApiError(HttpStatus.NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } @Override protected ResponseEntity handleMissingServletRequestParameter( MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = ex.getParameterName() + " parameter is missing"; return buildResponseEntity(new RestApiError(BAD_REQUEST, error, ex)); } @Override protected ResponseEntity handleHttpMediaTypeNotSupported( HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { StringBuilder builder = new StringBuilder(); builder.append(ex.getContentType()); builder.append(" media type is not supported. Supported media types are "); ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", ")); return buildResponseEntity(new RestApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex)); } ... }

UserNotFoundException类为我们自定义异常信息类,在执行GET /users/{userIds}或 GET /users请求时,如果数据库中不存在该ID的记录信息,将抛出UserNotFoundException异常信息,且将响应状态码设置为NOT_FOUND。UserNotFoundException源码如下:

public class UserNotFoundException extends Exception { public UserNotFoundException(Class clz,String...searchParams){ super(UserNotFoundException.generateMessage(clz.getSimpleName(),toMap(String.class,String.class,searchParams))); } private static String generateMessage(String entity, Map searchParams){ return StringUtils.capitalize(entity)+ " was not found for parameters "+ searchParams; } private static Map toMap(Class key,Class value,Object...entries){ if(entries.length % 2 == 1){ throw new IllegalArgumentException("Invalid entries"); } return IntStream.range(0,entries.length/2).map(i->i*2) .collect(HashMap::new, (m,i)->m.put(key.cast(entries[i]),value.cast(entries[i+1])),Map::putAll); } }

下图将更为直观的说明自定义异常处理的整个流程:

当UserService发生异常时,异常信息将向上传递到UserController,此时的异常信息被Spring所捕获,并将其跳转到UserNotFoundException处理方法中。UserNotFoundException将异常报告封装到RestApiError对象中,并回传给API Client。通过此方法,API客户端将获得一份逻辑清晰的响应报告。

本次课程的全部源码已经上传到 Github 仓库,你可以点击此链接获取源码:github.com/ramostear/S…

原文地址:www.ramostear.com/articles/sp…

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/6704.html

相关文章

  • Spring 指南(目录)

    摘要:指南无论你正在构建什么,这些指南都旨在让你尽快提高工作效率使用团队推荐的最新项目版本和技术。使用进行消息传递了解如何将用作消息代理。安全架构的主题指南,这些位如何组合以及它们如何与交互。使用的主题指南以及如何为应用程序创建容器镜像。 Spring 指南 无论你正在构建什么,这些指南都旨在让你尽快提高工作效率 — 使用Spring团队推荐的最新Spring项目版本和技术。 入门指南 这些...

    only_do 评论0 收藏0
  • Spring Boot 2.x 系列教程:WebFlux REST API 全局异常处理 Error

    摘要:挺多人咨询的,异常处理用切面注解去实现去全局异常处理。全局异常处理类,代码如下代码解析如下抽象类是用来处理全局错误时进行扩展和实现注解标记的切面排序,值越小拥有越高的优先级,这里设置优先级偏高。 本文内容 为什么要全局异常处理? WebFlux REST 全局异常处理实战 小结 摘录:只有不断培养好习惯,同时不断打破坏习惯,我们的行为举止才能够自始至终都是正确的。 一、为什么要全局...

    BicycleWarrior 评论0 收藏0
  • 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》

    摘要:下一代服务端开发下一代服务端开发第部门快速开始第章快速开始环境准备,,快速上手实现一个第章企业级服务开发从到语言的缺点发展历程的缺点为什么是产生的背景解决了哪些问题为什么是的发展历程容器的配置地狱是什么从到下一代企业级服务开发在移动开发领域 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》 Kotlin + Spring Boot : 下一代 Java...

    springDevBird 评论0 收藏0
  • Spring Boot 参考指南(使用WebClient调用REST服务)

    摘要:为所有实例进行应用程序级的附加定制,你可以声明并在注入点局部的更改。最后,你可以回到原来的并使用,在这种情况下,不应用自动配置或。上一篇使用调用服务下一篇验证发送电子邮件 34. 使用WebClient调用REST服务 如果你的classpath上有Spring WebFlux,那么你还可以选择使用WebClient来调用远程REST服务,与RestTemplate相比,这个客户端具有...

    null1145 评论0 收藏0
  • Spring Boot 最流行的 16 条实践解读!

    摘要:来源是最流行的用于开发微服务的框架。以下依次列出了最佳实践,排名不分先后。这非常有助于避免可怕的地狱。推荐使用构造函数注入这一条实践来自的项目负责人。保持业务逻辑免受代码侵入的一种方法是使用构造函数注入。 showImg(https://mmbiz.qpic.cn/mmbiz_jpg/R3InYSAIZkHQ40ly9Oztiart2lESCyjCH0JwFRp3oErlYobhibM...

    Ethan815 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<