全局异常处理2.0

359 阅读3分钟

还记得我们前面开发的全局异常处理器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抛出的异常,我们可以在其执行流程上看到有这样的源码:

image.png

很显然,异常对象被存起来了,放在了request作用域,这也为我们filter组件抛出异常并被全局处理提供了思路,我们也用相同的key存到request作用域,最终在ResponseBodyAdvicebeforeBodyWrite方法中处理即可。

为此我们先定义下这个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进行测试:

image.png

image.png

image.png

image.png

各种测试都得到了我们预期的统一响应格式。最后我们再把所有单元测试跑一遍:

image.png

ok!改造完毕!