还记得我们前面开发的全局异常处理器GlobalExceptionHandler吗,拦截到controller执行前的interceptor抛错或者在执行controller处理方法时抛错,这个全局异常处理器都会发挥作用,因为我们在其头部加了@RestControllerAdvice注解。
但是除此之外的情况,比如开发一个Filter组件,在其执行时抛出异常,这个现在的全局异常处理器管不了。
现在我们就要实现咱们web应用中各个层抛出的异常都能被spring boot整合的web模块所感知,并在我们以rest风格输出json响应时,能把异常信息按照我们想返回的格式来返回,这就是本节我们将一起学习改造全局异常处理方案的目标,开干!
改造全局异常处理器
现在我们将撤销全局异常处理器的拦截能力,把它作为一个普通的工具组件,用来做异常类型和响应对象的转换,也就是我们要将异常包装成带有响应状态和响应体的ResponseEntity对象。这里的改造很简单,我们把原来类头部的@RestControllerAdvice替换为普通的@Component注解,把处理方法上的@ExceptionHandler注解拿掉,取而代之的,我们加一个入口方法:
public ResponseEntity<Response<?>> resolveException(Exception ex) {
if (ex instanceof BusinessException) {
return handleException((BusinessException) ex);
} else {
return handleException(ex);
}
}
private ResponseEntity<Response<?>> handleException(BusinessException ex) {
...
}
private ResponseEntity<Response<?>> handleException(Exception ex) {
...
}
完善RestBodyAdvice
既然我们rest风格的api的json响应最终都会经过@RestControllerAdvice所修饰的RestBodyAdvice类的beforeBodyWrite,那事情就好办了,只要我们在这个方法中获取到异常对象,再由GlobalExceptionHandler类的resolveException(exception)处理得到ResponseEntity<Response<?>>类型的对象,这样下面的路就上轨道了。
确实我们可以得到上层组件,比如interceptor抛出的异常,我们可以在其执行流程上看到有这样的源码:
很显然,异常对象被存起来了,放在了request作用域,这也为我们filter组件抛出异常并被全局处理提供了思路,我们也用相同的key存到request作用域,最终在ResponseBodyAdvice的beforeBodyWrite方法中处理即可。
为此我们先定义下这个key的常量:
package com.xiaojuan.boot.consts;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
public interface RequestConst {
String EXCEPTION = DefaultErrorAttributes.class.getName() + ".ERROR";
}
看下统一响应功能改造后的代码:
package com.xiaojuan.boot.common.web.support;
import ...
@Slf4j
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice<Object> {
@Resource
private GlobalExceptionHandler exceptionHandler;
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports...
@SneakyThrows
@Override
public Object beforeBodyWrite(...) {
// 处理404的响应
if (HttpStatus.NOT_FOUND.value() == WebRequestUtil.getResponseStatus()) {
Map<String, Object> bodyMap = (Map<String, Object>) body;
Object resp = Response.fail("找不到请求路径:" + bodyMap.get("path"));
logResponse(resp);
return resp;
}
Exception exObj = (Exception) WebRequestUtil.getRequest().getAttribute(RequestConst.EXCEPTION);
// 响应异常的情况
if (exObj != null) {
// 处理全局异常的调用
ResponseEntity<Response<?>> resp = exceptionHandler.resolveException(exObj);
response.setStatusCode(resp.getStatusCode());
logResponse(resp.getBody());
return resp.getBody();
}
Object resp;
if (body instanceof String) {
// 字符串需要手动序列化为json
response.getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE);
resp = Response.ok(body);
logResponse(resp);
return objectMapper.writeValueAsString(resp);
}
resp = Response.ok(body);
logResponse(resp);
return resp;
}
...
}
代码说明
这里首先我们处理了
404请求异常的情况,因为spring boot内部会对该请求失败的各种情况做统一的错误格式(响应体)返回,我们可以从body中获取到相关信息,进行包装。其次我们手动解析和处理全局异常,首先从
request作用域获取,然后调相关方法来获取,然后走统一的处理方式。后续的操作就是我们先前的逻辑,即,正常响应的情况。
测试
完成了改造后,我们做一些测试。
启动web服务,用http client进行测试:
各种测试都得到了我们预期的统一响应格式。最后我们再把所有单元测试跑一遍:
ok!改造完毕!