前言

在前些天,一位朋友在某个群里说@ExceptionHandler+@RestControllerAdvice的方式没法捕获到@WebFilter的异常,于是我搜了下十几个csdn的回答,好家伙,全nm是互相抄的,基本都是将异常信息set到request后再转发到自定义controller,这样转发你不嫌你的代码丑吗?我都看不下去了,好在我之前看过springboot的一些源码,有更好的解决方案,下面就来一起看下。


捕获更外层的异常

验证问题

前置条件,项目代码中已经有捕获指定异常的@ExceptionHandler,完整代码见:https://gitee.com/wenjie2018/UT-APP/tree/global_error_filter/

这个代码比较大,因为我直接拿自己的可用项目测试的,你也可以自己新建一个springboot项目

首先,我们写一个demo,证明@ExceptionHandler+@RestControllerAdvice确实捕获不到异常,将如下代码加到一个SpringBoot项目中:


package run.ut.app;

import run.ut.app.exception.BadRequestException;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter(urlPatterns = "/*", filterName = "sqlFilter")
public class TestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        ServletResponse response = servletResponse;

        String token = request.getHeader("token");

        if (token == null || !token.equals("123")) {
          throw new BadRequestException("非法请求!");
        } else {
            filterChain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
    }
}

之后我们启动项目,随便请求一个开放接口,观察控制台和返回值的内容,首先,下面是返回值的内容👇:
image.png
下面是控制台的日志:
image.png
在我的代码逻辑里面,我是定义了BadRequestException的返回码为400,但按照目前的返回至来看,@ExceptionHandler确实没生效。


覆写BasicErrorController捕获异常

捕获异常的方式也很简单,首先你得知道你抛出的异常去哪了,验证这个问题很简单,你只需要跟着异常debug一下就知道了:
image.png

具体的debug过程就不多说了,最后会来到下面这个地方👇:
image.png

从上面我们可以看到,这里其实就已经把异常对象保存到request对象中了,所以像csdn1csdn2中自己把异常重新set一遍的操作是完全没必要的,侧面证明他们都是抄袭狗,根本没看过代码。

现在我们已经知道springboot会为我们自动保存异常信息到request中,那我们是不是还要像上面的sb抄袭博客一样要自定义一个controller然后把异常转发一遍呢?我既然写这篇博客,那肯定是不用的。


以前阅读过SpringBoot的异常处理逻辑的小伙伴,其实就知道,SpringBoot的BasicErrorController中有个error方法就是集中处理异常返回的,我们只需要覆写成如果是自定义异常就自定义处理器,如果不是就走别的处理器即可,代码如下:

package run.ut.app.exception.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import run.ut.app.exception.UtException;
import run.ut.app.model.support.BaseResponse;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

/**
 * @author chenwenjie.star
 * @date 2021/11/5 1:41 下午
 */
@RestController
@Slf4j
public class GlobalErrorController extends BasicErrorController {

    public GlobalErrorController() {
        this(new DefaultErrorAttributes(), new ErrorProperties());
    }

    public GlobalErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        super(errorAttributes, errorProperties);
    }

    public GlobalErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorProperties, errorViewResolvers);
    }

    @Override
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        log.error("进入error处理器");
        Throwable throwable = (Throwable) request.getAttribute("javax.servlet.error.exception");
        // 可以自定义一个handler取代下面的if else,这里为了demo足够简单先不管这么多
        if (throwable instanceof UtException) {
            UtException utException = (UtException) throwable;
            Integer statusCode = utException.getStatus().value();
            return new ResponseEntity(new BaseResponse<>(statusCode, utException.getMessage(), null), utException.getStatus());
        } else {
            return new ResponseEntity(new BaseResponse<>(500, "出现意料之外的错误", null), HttpStatus.valueOf(500));
        }
    }
}

将上述代码加入到项目后,我们来重新测试下接口的返回👇:
image.png

到此为止,问题解决,大功告成👏🏻👏🏻👏🏻,感兴趣的可以往下看看battle现场。


battle现场

这篇博客的起因是这样的,某一天某个群里某个人问了这样一个问题(过滤器指的是@WebFilter):
image.png

之后他说用全局异常处理器捕获不到(指的是@ExceptionHandler+@RestControllerAdvice的方式),这是当然的,因为@ExceptionHandler+@RestControllerAdvice不是真正意义上的全局处理,而是只会处理进入controller的异常,因为@WebFilter的逻辑是在controller之外的,所以会失效:
image.png

上面聊天里另一个人说的用切面也是对的(之前我写ut就是切面+注解额),但现在提问者是在维护老项目遇到了问题,换成切面的话万一漏了point那可就麻烦大了,所以最好的解决办法还是不改变原有的业务逻辑就能捕获异常。

后来我顺利拿到了demo:
image.png
image.png
image.png

再后来他查了下csdn,给出的方案是在过滤器里用forward把异常再转发给controller
image.png,我觉得这种方式蠢爆了。

他看了网上的博客认为controller外的逻辑springboot都没法控制到;我认为springboot默认用的一样是servlet,肯定有收敛的地方,肯定能全局捕获。

经过了一番代码改造(就如这篇博客前面的部分),到最后风向果然改变了👇:
image.png
image.png

Q.E.D.