HTTP状态码:https://www.runoob.com/http/http-status-codes.html
@ControllerAdvice源码分析:https://blog.csdn.net/GAOXINXINGgaoxinxing/article/details/91416412
本章演示代码:https://gitee.com/tysite-web/tysite-service/tree/master/src/main/java/org/tysite/tyservice/common/exception
说明
在日常的服务端接口开发过程中,我们通常会遇到诸如参数不合法、无操作权限、业务限制等需要返回错误信息的业务场景。
阅读过作者《spring validation 参数校验使用详解》章节朋友们,会发现Spring Validation 错误异常的返回形式,并不适合前端给用户呈现错误信息。本章作者将整理不同业务场景的错误提醒格式,以实现报错信息的格式一致性。
一. 依赖添加
在本章中,我们需要对异常信息进行格式化,期间需要使用JSON
格式返回结果。而当前比较主流的JSON处理工具,就是阿里巴巴的fastjson
。
我们通过 maven官方仓库 搜索fastjson
的maven依赖,并找到其相关依赖版本列表,这里我们选择最新版本1.2.68
。
将对应的依赖'com.alibaba:fastjson:1.2.68'
添加到项目build.gradle
文件的依赖配置中。
二. 异常处理功能规划
在《@JsonView 过滤响应正文的对象属性》章节中,我们已经探讨过关于正常响应情况下,响应体数据的处理(采用JSON格式返回);本章将整理请求异常响应情况下,如何规划状态码的使用和响应体报文的格式化。
1、状态码规划
通过阅读HTTP状态码
的相关规范,作者在下表中整理通常情况下,需要后端开发人员
处理的请求状态。
状态码 | 描述 | 应用范围 |
---|---|---|
400 (Bad Request) | 客户端请求的语法错误,服务器无法理解 | 适用于请求参数验证不合规时的异常响应 |
401 (Unauthorized) | 请求要求用户的身份认证 | 适用于无权访问时的异常响应 |
403 (Forbidden) | 服务器理解请求客户端的请求,但是拒绝执行此请求 | 适用于服务端拒绝客户请求时的异常响应,如非法请求 |
500(Internal Server Error) | 服务器内部错误,无法完成请求 | 适用于处理服务端内部错误时的异常响应 |
2. 响应体数据格式化
响应体信息格式化的目的是尽量使所有异常请求的返回信息格式一致,以便于前端同学统一处理。
观察 spring mvc 在请求资源路径错误和序列化失败时的响应体数据格式,如下图所示:
我们对自定义的全局异常响应体格式化遵循该格式规则(做细微调整):
属性 | 描述 |
---|---|
status | 状态码,如400、401、500等 |
error | 错误信息,此属性存放提供给开发人员分析代码的错误信息 (注意此信息不要打印到前端界面 ) |
message | 错误信息,此属性存放提供给用户的错误提示信息 |
path | 请求的API地址 |
timestamp | 请求响应的时间戳 |
三. 全局异常捕获
要实现异常信息的格式化,我们需要在response
返回给调用端之前,捕获异常,并进行相应格式化。这里我们通过@RestControllerAdvice
注解和@ExceptionHandler
注解,实现异常捕获和格式化处理,这里只做简单的使用说明。(需要详细了解这两个注解的朋友,可以看作者文章顶部的转载链接)
在tysite-service
项目中的org.tysite.tyservice.common.exception
包中,添加全局异常处理类 GlobalExceptionHandler.java
@RestControllerAdvice (1)
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class) (2)
@ResponseStatus(HttpStatus.BAD_REQUEST) (3)
public JSON validationBindException(BindException exception, HttpServletRequest request) {
BindingResult result = exception.getBindingResult();
return formatValidationException(result, request);
}
……
}
(1)、启动应用后,被 @ExceptionHandler
、@InitBinder
、@ModelAttribute
注解的方法,都会作用在 @RequestMapping 注解的方法上。
(2)、@ExceptionHandler 处理程序中的特定异常,其value值为待处理的异常的类,如BindException.class
。
(3)、@ResponseStatus 用于设置异常处理返回时,响应的状态码。
注意:HttpStatus
为我们封装了所有可以用到的http状态码信息,我们只需要根据业务需要使用即可。
四. spring validation 异常捕获并格式化
基于spring validation的参数验证规范,针对不同Controller接参方式,其非法数据报错抛出的异常也不相同。
当使用form-data
和Params
接收参数验证异常时,会通过BindException.class
抛出;
当使用@RequestBody
接收参数验证异常时,会通过MethodArgumentNotValidException.class
抛出;
1、BindException 异常捕获
捕获BindException
异常,通过 formatValidationException()
方法重构异常信息,以本文规划格式返回。
由于该异常属于请求参数格式不合规,故而HTTP状态码采用HttpStatus.BAD_REQUEST
。
……
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public JSON validationBindException(BindException exception, HttpServletRequest request) {
BindingResult result = exception.getBindingResult();
return formatValidationException(result, request);
}
……
2、MethodArgumentNotValidException 异常捕获
捕获MethodArgumentNotValidException
异常,通过 formatValidationException()
方法重构异常信息,以本文规划格式返回。
由于该异常属于请求参数格式不合规,故而HTTP状态码采用HttpStatus.BAD_REQUEST
。
……
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public JSON validationBodyException(MethodArgumentNotValidException exception, HttpServletRequest request) {
BindingResult result = exception.getBindingResult();
return formatValidationException(result, request);
}
……
3、spring validation 验证异常通用格式化方法
自定义 formatValidationException(BindingResult result, HttpServletRequest request)
方法实现对 spring validation 验证异常信息格式化,构建error
错误信息和message
提醒信息,并按照规划的JSON格式返回。
……
private JSON formatValidationException(BindingResult result, HttpServletRequest request) {
JSONObject json = new JSONObject();
StringBuffer errBuffer = new StringBuffer();
StringBuffer msgBuffer = new StringBuffer();
if (result.hasErrors()) {
List<ObjectError> errors = result.getAllErrors();
errors.forEach(p -> {
FieldError fieldError = (FieldError) p;
if (errBuffer.length() > 0) {
errBuffer.append("; ");
}
if (msgBuffer.length() > 0) {
msgBuffer.append("; ");
}
errBuffer.append("The value [");
errBuffer.append(fieldError.getRejectedValue());
errBuffer.append("] of { ");
errBuffer.append(fieldError.getObjectName());
errBuffer.append(".");
errBuffer.append(fieldError.getField());
errBuffer.append(" } does not conform to the specification { ");
errBuffer.append(fieldError.getCode());
errBuffer.append(" }");
msgBuffer.append(fieldError.getDefaultMessage());
});
json.put("status", HttpStatus.BAD_REQUEST.value());
json.put("error", errBuffer.toString());
json.put("message", msgBuffer.toString());
json.put("path", request.getServletPath());
json.put("timestamp", new Date());
}
return json;
}
……
演示源码地址:GlobalExceptionHandler
注意:考虑到StringBuffer
的处理性能大概是spring format
的20倍,本方法中的数据拼接采用StringBuffer
方案。
4、异常提示信息演示
提交非法数据,返回格式化后的validation验证异常信息,如下图所示。
五. 自定义业务异常类
在日常开发中,我们通常会遇到用户请求参数合规,但是数据不符合业务规范的场景。
如 需要删除的记录,由于其他业务逻辑限制而禁止删除
等,这时我们就需要通过业务异常提醒前端,该操作为禁止操作。此类异常属于服务端拒绝执行,采用状态码HttpStatus.FORBIDDEN
;异常提醒信息由具体业务逻辑决定,本节将完整业务异常类的自定义演示。
1、定义业务异常类
在tysite-service
项目的org.tysite.tyservice.common.exception
包中,创建业务异常类BusinessExceptions.java
。
……
public class BusinessExceptions extends RuntimeException {
private static final long serialVersionUID = -2008629396532417296L;
public BusinessExceptions(String message) {
super(message);
}
}
……
演示源码地址:BusinessExceptions
注意:由于此类异常为业务执行过程中的服务端拒绝服务,采用运行时异常即可,故选择继承RuntimeException
。
2、异常捕获并格式化
在GlobalExceptionHandler
类中添加BusinessExceptions
的异常捕获及响应体格式化方法
……
@ExceptionHandler(BusinessExceptions.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public JSON businessException(BusinessExceptions exception, HttpServletRequest request) {
JSONObject json = new JSONObject();
StackTraceElement element = exception.getStackTrace()[0];
json.put("status", HttpStatus.FORBIDDEN.value());
json.put("error", "The " + element.getMethodName() +"() method of class [ " + element.getFileName() +
" ] throws a business exception in the " +element.getLineNumber()+ " line { " + exception.getLocalizedMessage() + " }");
json.put("message", exception.getLocalizedMessage());
json.put("path", request.getServletPath());
json.put("timestamp", new Date());
return json;
}
……
注意:BusinessExceptions异常的报错位置,均在堆栈踪迹
的第一条,故提取该回溯记录的类名、方法、错误代码行号及异常信息整理报错内容。
演示源码地址:GlobalExceptionHandler
3、异常提示信息演示
自行编写调用API,并抛出业务异常BusinessExceptions
,用于演示业务异常功能。
执行后,返回如下数据结果:
注意:本文后续将根据业务迭代需要,整理新的异常处理内容