基于SpringBoot微服务异常处理最佳实践
本⽂通过对RESTful WebService中异常处理的⼏个关键点如⾃定义错误码、定制错误消息、⾃定义异常、全局异常处理进⾏介绍,分享本⼈对Spring异常处理和对RESTful API设计的思考和实践。
随着前后端分离,前端⼯程化,后端微服务化,越来越多的应⽤都开始倾向于使⽤ RESTful API 为各种各样的客户端提供服务。设计⼀套优雅的 API 服务,需要诸多考量,⽽异常处理往往被忽视,⽽我认为这恰恰是评判⼀套 API 设计好坏的很重要的⼀个衡量因素。经过很多的经验借鉴和思考,最终形成了⼀套我认为还算合理的异常处理⽅式,权作抛砖引⽟。
对于异常处理,最关键的⽆⾮是定义错误码、定制错误消息、⾃定义异常类⼏个环节,关于这些点,以及其中可能遇到的问题,笔者在写作此⽂之前,都是经过了深思熟虑的,也都尽可能在⽂章中或者在代码中指出来了,供读者参考。
错误码
当我们的 API 提供给调⽤⽅使⽤的时候,除了正确的请求、响应接⼝说明以及⽰例,还应该给出⼀个符合统⼀规范的错误消息说明和⼀组错误码说明。可能有⼈习惯使⽤数字作为错误码,但理论上讲,⽆论是接⼝还是数据库,使⽤本⾝⽆意义的数字作为字段值,都属于⾮常糟糕的设计。关于这点,有必要解
释⼀下。设想如下场景,我们设计了⼀张表,其中很多枚举字段,假如都使⽤数字来存储,⽐如某个 Task 的执⾏状态可能有[1-就绪;2-执⾏中;3-故障恢复中;4-已完成]⼏种,那么这种设计只会接下来会⾯临两种情况:
1. 服务接⼝代码中不做翻译,则如果你提供的是 API,则⽤户拿到你的数据之后,不得不对照着你的⽂档,才能明⽩每个值代表什么含
义,如果你的接⼝是给前端使⽤的,那么前端必须按照你的接⼝⽂档去维护⼀个翻译列表。更糟糕的是,如果有⼀天你的状态多了⼀种,⽐如失败重试太多次后中⽌任务[5-已中⽌],如果前端未更新翻译列表,则好的情况是前端做了容错,在页⾯上显⽰5,坏的情况是直接就在页⾯显⽰了 undefined。
2. 服务接⼝代码中做翻译,你可能需要在代码中写⼤量的代码做 IN OUT 两个⽅向上的翻译⼯作。
⽽如果设计之初,就将该状态字段的值设置为了有意义的字符串枚举呢?如[READY;RUNNING;RETRYING;FAILBACK;DONE],则按照上述情况1,⽤户拿到数据后,⼀⽬了然就能知晓当前状态是怎样的。如果是前端使⽤接⼝,即使状态增加[ABORT]时,即使前端未及时更新翻译列表,页⾯上直接显⽰ABORT,也不⾄于使⽤户莫名其妙。
总⽽⾔之,以数字作为枚举值的做法,绝⼤多数情况都可以归为陋习(不排除个别特殊情况)。回到
错误码的讨论上,抛弃旧有的以数字作为错误码的古董观念吧!我们定义错误码枚举包含两个字段: code 字段给出简明扼要的错误提⽰,message 字段描述具体错误,形如:
INVALID_REQUEST("InvalidRequest", "Invalid request, for reason: {0}")
错误消息
对于 HTTP 请求⽽⾔,正确响应⼀般都伴随着2xx状态码,以及⼀个响应消息体。相应地,错误响应也理应有⼀个错误消息,以便 API 使⽤者能够知道错误原因,做出修正。当然,错误响应的响应状态码也是必不可少的,且原则上,应该尽可能地返回恰当的 HTTP 状态码,但这并不是我们本⽂讨论的重点,有兴趣的可以仔细阅读下 RFC ⽂档。⾄于错误消息,则没有⼀个特定的格式。
我们定义错误响应的消息体如下:
{
"requestId": "5f8c89b6-f0d4-48d4-b945-01fbce035c0a",
"status": 400,
"reason": "Bad Request",
"code": "NotFound",
"message": "Resource Book[id=10] not found.",
"message": "Resource Book[id=10] not found.",
"details": "uri=/books/10;client=0:0:0:0:0:0:0:1",
"timestamp": "2018-10-16T23:30:40.431+08:00"
}
注:这⾥只做⼀个⽰例,讲述实践⽅法,具体的错误消息可以根据⾃⼰的需要定制。
具体实现
继上述说明,我们接下来⽤代码说明具体如何实现。该项⽬为 Maven 多模块项⽬,⽂件结构如下
.
├── README.md
├── errorhandle-bookstore
│├── l
│└── src
│├── main
││├── java
│││└── com
│││└── lomagicode
│││└── example
│││└── errorhandle
│││└── errorhandle
│││└── bookstore
│││├── ErrorHandleApplication.java
│││├── config
││││├── ValidationConfig.java
││││└── WebMvcConfig.java
│││├── domain
││││└── Book.java
│││├── error
││││├── BookStoreErrorCode.java
││││└── RestExceptionHandler.java
│││├── service
││││├── BookService.java
││││└── BookServiceImpl.java
│││└── web
│││└── endpoint
unknown怎么处理│││└── BookEndpoint.java
││└── resources
││└── application.properties
│└── test
│└── java
├── errorhandle-commons
│├── l
│└── src
│├── main
││├── java
│││└── com
│││└── lomagicode
│││└── example
│││└── errorhandle
│││└── commons
│││├── constant
││││└── WebConsts.java
│││├── error
││││├── BusinessException.java
││││├── CommonErrorCode.java
││││├── CustomizedBaseExceptionHandler.java
││││├── ErrorCode.java
││││└── ErrorDetails.java
│││├── package-info.java
│││└── web
│││└── interceptor
│││└── RequestIdInterceptor.java
││└── resources
│└── test
│└── java
└── l
对于⼀个⼤中型项⽬⽽⾔,通常我们可以把项⽬看做⼀个产品,⽽产品往往会根据业务分为不同的⼦模块。对于错误码的定义,通常会分为通⽤错误码、以及模块内的业务错误码。模拟⼀个⼤中型产品,我们这⾥将项⽬分为最基本的两个模块,⼀个代表公⽤模块,对于通⽤错误码、通⽤异常、通⽤异常处理以及对错误消息通⽤格式的定义等,我们都会在这个模块中实现。⼀个模拟的书店模块,依赖前⾯的公⽤模块。
为尽量避免通篇⼤量的代码粘贴,我们在这⾥只介绍⼏个重要的⽂件,具体的实现细节,⽂章会在最后附上源码地址。
公共模块
公共模块,或者叫通⽤模块,包括三个重要的⼦包,constant、error、web,我们错误处理实现的主要代码就是在 error 包下。其中:
ErrorCode 是⼀个接⼝,作为错误码枚举类型的⽗接⼝,其中只有两个⽅法
public interface ErrorCode {
String getCode();
String getMessage();
}
CommonErrorCode 中定义通⽤错误码,作为产品各个⼦业务模块的公⽤部分,如
/**
* 错误请求
*/
INVALID_REQUEST("InvalidRequest", "Invalid request, for reason: {0}"),
/**
* 参数验证错误
*/
INVALID_ARGUMENT("InvalidArgument", "Validation failed for argument [{0}], hints: {1}"),
/
**
* 未到资源
*/
NOT_FOUND("NotFound","Resource {0} not found."),
/**
* 未知错误
*/
UNKNOWN_ERROR("UnknownError", "Unknown server internal error.");
BusinessException 是产品业务层⾯错误处理的通⽤异常类,是⼀个⾮受检异常。BusinessException 与系统默认异常最⼤的不同之处就是包含了⼀个 errorCode 属性。在业务中出错的地⽅,抛出⼀个携带有特定错误码的 BusinessException 异常,然后全局处理该异常类型,从其中解析构造出我们想要的错误消息,由 Spring 直接返回出去,即为⼀个符合我们要求的错误消息。
CustomizedBaseExceptionHandler 继承⾃ org.springframework.web.hod.annotation.ResponseEntityExceptionHandler,从名称上也可以看出来,ResponseEntityExceptionHandler 是⼀个⽤于全局处理 RESTful 接⼝异常的处理器。通过在该类型及其⼦类型中添加使⽤ @ExceptionHandler 注解的⽅法,可以处理指定异常类型。
ErrorDetails ⾃定义的错误消息体,可以根据⾃⼰的实际需要随意定制。
业务模块
业务模块中,其他部分可以从略,我们关注这么⼏个类:
BookStoreErrorCode 类是我们定义的与该业务⼦模块息息相关的错误码,举⼏个例⼦
/**
* 虽然<strong>不推荐</strong>,但允许在模块中⾃定义新的错误码,⽽不去使⽤通⽤库中已经定义的 {@link CommonErrorCode#NOT_FOUND} 错误码
*/
NOT_FOUND_BOOK("NotFoundBook", "Book {0} not found."),
/**
* 有如下两种定义错误码的思路:
* 1. 定义宽泛的错误码,传⼊参数,如 Exists ,传⼊ Book[id=1]
* 2. 定义特定的错误码,如 InvalidBookId.Exists ,不⽤传⼊参数
* <p>
* 具体采⽤哪种,可以根据喜好来决定,个⼈更偏向于定义相对宽泛的错误码,上⾯的 {@link #NOT_FOUND_BOOK} ⽰例也类似
*/
EXISTS("Exists", "The specified object {0} already exists."),
INVALID_BOOK_ID_EXISTS("InvalidBookId.Exists", "The specified bookId already exists.");
BookStoreErrorCode(String code, String message) {
}
/**
* Customized error code
*/
private String code;
/**
* Error message details
*/
private String message;
@Override public String getCode() {
return "BookStore." + code;}@Override
public String getMessage() {
return message;
} 很容易发现它与 CommonErrorCode 中定义的通⽤错误码的不同之处,那就是更偏向于具体业务了。此外还需要注意 getCode ⽅法的实现,在⼀个产品中,为与其他⼦业务模块区分,我们在不同的业务模块中,使⽤特定的模块名称作为错误码前缀。另外⼀个主要注意的就是如上代码注释中提及的,在业务模块中错误码定义是采⽤ 宽泛化 还是 特定化 模式,这个因⼈⽽异,在本实践中,我们更倾向于使⽤ 宽泛化 的模式。
RestExceptionHandler 就本⽰例代码中,就只是⼀个 CustomizedBaseExceptionHandler 的空⼦类了。@ControllerAdvice
@RestController
public class RestExceptionHandler extends CustomizedBaseExceptionHandler {
}
如果要为某个特定的异常添加处理逻辑,可以在该处理器类中实现,实现⽅式请参考 ResponseEntityExceptionHandler 和
CustomizedBaseExceptionHandler 。
关于该业务模块,还有最后两点值得⼀提,那就是对于『Not Found』和 『参数校验』的处理⽅式。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论