通用全局异常处理逻辑
一、通用异常处理逻辑
程序员的异常处理逻辑要十分的单一:无论在Controller层、Service层还是什么其他位置,程序员只负责一件事:那就是捕获异常,并将异常转换为自定义异常。使用用户友好的信息去填充CustomException的message,并将CustomException抛出去。
@Service
public class ExceptionService {
//服务层,模拟系统异常
public void systemBizError() {
try {
Class.forName("com.mysql.jdbc.xxxx.Driver");
} catch (ClassNotFoundException e) {
throw new CustomException(
CustomExceptionType.SYSTEM_ERROR,
"在XXX业务,myBiz()方法内,出现ClassNotFoundException,请将该信息告知管理员");
}
}
//服务层,模拟用户输入数据导致的校验异常
public void userBizError(int input) {
if(input < 0){
//模拟业务校验失败逻辑
throw new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"您输入的数据不符合业务逻辑,请确认后重新输入!");
}
//…… 其他的业务
}
}
二、全局异常处理器
通过团队内的编码规范的要求,我们已经知道了:不允许程序员截留处理Exception,必须把异常转换为自定义异常CustomException全都抛出去。那么程序员把异常跑出去之后由谁来处理?那就是ControllerAdvice。
ControllerAdvice注解的作用就是监听所有的Controller,一旦Controller抛出CustomException,就会在@ExceptionHandler(CustomException.class)注解的方法里面对该异常进行处理。处理方法很简单就是使用AjaxResponse.error(e)
包装为通用的接口数据结构返回给前端。
@ControllerAdvice
public class WebExceptionHandler {
//处理程序员主动转换的自定义异常
@ExceptionHandler(CustomException.class)
@ResponseBody
public AjaxResponse customerException(CustomException e) {
if(e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()){
//400异常不需要持久化,将异常信息以友好的方式告知用户就可以
//TODO 将500异常信息持久化处理,方便运维人员处理
}
return AjaxResponse.error(e);
}
//处理程序员在程序中未能捕获(遗漏的)异常
@ExceptionHandler(Exception.class)
@ResponseBody
public AjaxResponse exception(Exception e) {
//TODO 将异常信息持久化处理,方便运维人员处理
return AjaxResponse.error(new CustomException(
CustomExceptionType.OTHER_ERROR));
}
}
三、测试一下
随便找一个API,注入ExceptionService访问测试一下
当id=1的数据响应结果
当id!=1的数据响应结果
四、业务状态与HTTP协议状态一致
不知道大家有没有注意到一个问题(看上图)?这个问题就是我们的AjaxResponse的code是400,但是真正的HTTP协议状态码是200?
通说的说,目前
- AjaxResponse的code是400代表的是业务状态,也就是说用户的请求业务失败了
- 但是HTTP请求是成功的,也就是说数据是正常返回的。
在很多的公司开发RESTful服务时,要求HTTP状态码能够体现业务的最终执行状态,所以说:我们有必要让业务状态与HTTP协议Response状态码一致。
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//return returnType.hasMethodAnnotation(ResponseBody.class);
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
//如果响应结果是JSON数据类型
if(selectedContentType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON)){
//为HTTP响应结果设置状态码,状态码就是AjaxResponse的code,二者达到统一
response.setStatusCode(
HttpStatus.valueOf(((AjaxResponse) body).getCode())
);
return body;
}
return body;
}
}
实现ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。也就是说,ResponseBodyAdvice 的处理过程在全局异常处理的后面。
效果如下:
五、进一步优化
我们已经知道了,ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。将上文的GlobalResponseAdvice 中beforeBodyWrite方法代码优化如下。
- 如果Controller或全局异常处理响应的结果body是AjaxResponse,就直接return给前端。
- 如果Controller或全局异常处理响应的结果body不是AjaxResponse,就将body封装为AjaxResponse之后再return给前端。
所以,我们之前的代码是这样写的,比如:某个controller方法返回值
return AjaxResponse.success(objList);
现在就可以这样写了,因为在GlobalResponseAdvice 里面会统一再封装为AjaxResponse。
return objList;
最终代码如下:
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter methodParameter,
MediaType mediaType,
Class aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
//如果响应结果是JSON数据类型
if(mediaType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON)){
if(body instanceof AjaxResponse){
AjaxResponse ajaxResponse = (AjaxResponse)body;
if(ajaxResponse.getCode() != 999){
//999 不是标准的HTTP状态码,特殊处理
serverHttpResponse.setStatusCode(HttpStatus.valueOf(
ajaxResponse.getCode()
));
}
return body;
}else{
serverHttpResponse.setStatusCode(HttpStatus.OK);
return AjaxResponse.success(body);
}
}
return body;
}
}