SpringBoot 全局异常处理

2022-10-23 11:45:47

前言

        异常,一个开发人员再熟悉不过的名词,除数不能为 0 的异常,IO 异常,数组下标越界异常,操作数据库的 sql 异常,以及让所有程序员都头疼的 NPE

        本文就来谈谈,SpringBoot 和异常的那些事儿

正文

java 异常分类

        java 中主要存在两种类型的异常

  • 检查性异常:是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

如何理解两种异常呢,我们通过下面的例子来理解

首先定义一个异常类 CustomException ,继承 RuntimeException

public class CustomException extends RuntimeException {

    public CustomException() {
        super();
    }

    public CustomException(String message) {
        super(message);
    }

    public CustomException(String message, Throwable cause) {
        super(message, cause);
    }

    public CustomException(Throwable cause) {
        super(cause);
    }
}

该异常就属于运行时异常,检查性异常就是非 RuntimeException 及其子类,拿 IOException 为例

    public static void runtimeExTest() throws CustomException {
        throw new CustomException("test");
    }

    public static void checkExTest() throws IOException {
        throw new IOException("test");
    }

    public static void main(String[] args) {
        runtimeExTest();
        checkExTest();
    }

    public static void errorTest() throws Error {
        throw new Error("test");
    }

 可以看到,checkExTest() 在编译时会报错,提示 Unhandled exception: java.io.IOException

其实还有一类异常,Error,个人感觉使用场景不多,因此一笔带过吧,Error 和 RuntimeException 类似。

SpringBoot 全局异常处理

        SpringBoot 提供了默认的异常处理方式,是会转到对应的错误页面(ErrorPage)去的,但是现如今的前后端分离的开发方式,更多的是使用 RestControllerAdvice (ControllerAdvice)这种方式,小编也是最先接触并了解这种方式的,因此在此先行介绍这种方式。

1 @ControllerAdvice

        见名知意,这是一个 Advice,是对 controller 的增强,直接上用法

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕捉 CustomException ,返回 response 的 status 设置为 HttpStatus.BAD_REQUEST
     *
     * @param e CustomException
     */
    @ExceptionHandler(CustomException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleCustomException(CustomException e) {
        return e.getMessage();
    }

}

        与之 共同使用的有 @ExceptionHandler 和 @ResponseStatus 两个注解

        @ExceptionHandler 用以声明处理的异常类型

        @ResponseStatus 用以声明返回的 http code

  • 实际上述两个注解也可以使用在 RequestMapping 上,这边只是做了全局处理
  • ResponseStatus 个人建议不要修改,http code 多用来判断请求是否成功,业务逻辑可以在请求返回体中添加 code 来实现

 2 BasicErrorController

        本来觉得 @ControllerAdvice 已经很强大了,以后有什么异常直接往里面抛并处理即可。

        最近接了个项目,项目中用到了 SpringSecurity + jwt 的方式来进行登录鉴权。调试接口时,发现请求一直 403 ,但是没有任何返回值。DEBUG 后发现,在 jwt 鉴权的过滤器(Filter)中, jwt 解析时抛出了异常,但是居然没有被 GlobalExceptionHandler  捕捉到。正当我百思不得七姐时,一位热心的网友点醒了我。

ControllerAdvice 是用来处理Controller 抛出的异常的

如此简单明了答案竟让我不知道用什么词语来反驳,只能为自己的愚蠢感到羞愧。


         那么,如何解决呢。

        从返回的信息中看到这么一句话,大致意思就是没有 /error 路径的映射。

         所以一切要从 SpringBoot 的默认异常处理机制说起

        Spring Boot 提供了一套默认的异常处理机制,一旦程序中出现了异常,Spring Boot 会自动识别客户端的类型(浏览器客户端或机器客户端),并根据客户端的不同,以不同的形式展示异常信息。

        对于浏览器客户端,Spring Boot 会响应一个“ whitelabel”错误视图,以 HTML 格式呈现错误信息,如上面的图所示

       而 对于机器客户端而言,Spring Boot 将生成 JSON 响应,来展示异常消息。

{
  "timestamp": "2021-08-14T02:32:20.075+00:00",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/level1/1"
}

        Spring Boot 通过配置类 ErrorMvcAutoConfiguration 对异常处理提供了自动配置,该配置类向容器中注入了以下 4 个组件。

  • ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到“/error”上。
  • BasicErrorController:处理默认的“/error”请求。
  • DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。
  • DefaultErrorAttributes:用于页面上共享异常信息。

        SpringBoot 的自动配置流程,可以移步一些网上的教程(比如尚硅谷的 SpringBoot 教程,源码级的讲解,非常适合对 SpringBoot 使用过一段时间,但是没有深入了解其原理的同学),小编这边只介绍如何转发,及如何处理。

        1)转发过程可以关注一个 StandardHostValue 的类

        // 寻找 ErrorPage
        ErrorPage errorPage = context.findErrorPage(statusCode);
        if (errorPage == null) {
            // Look for a default error page
            errorPage = context.findErrorPage(0);
        }
        if (errorPage != null && response.isErrorReportRequired()) {
            response.setAppCommitted(false);
            request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
                              Integer.valueOf(statusCode));

            String message = response.getMessage();
            if (message == null) {
                message = "";
            }
            request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
            request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
                    errorPage.getLocation());
            request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
                    DispatcherType.ERROR);


            Wrapper wrapper = request.getWrapper();
            if (wrapper != null) {
                request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
                                  wrapper.getName());
            }
            request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
                                 request.getRequestURI());
            // 该方法会转发到 ErrorPage 
            if (custom(request, response, errorPage)) {
                response.setErrorReported();
                try {
                    response.finishResponse();
                } catch (ClientAbortException e) {
                    // Ignore
                } catch (IOException e) {
                    container.getLogger().warn("Exception Processing " + errorPage, e);
                }
            }
        }

          默认的 ErrorPage

         然后是 custom(Request request, Response response, ErrorPage errorPage)方法

            if (response.isCommitted()) {
                // Response is committed - including the error page is the
                // best we can do
                rd.include(request.getRequest(), response.getResponse());
            } else {
                // Reset the response (keeping the real error code and message)
                response.resetBuffer(true);
                response.setContentLength(-1);
                // forward 服务端转发
                rd.forward(request.getRequest(), response.getResponse());

                // If we forward, the response is suspended again
                response.setSuspended(false);
            }

        2) 请求处理过程关注 BasicErrorController

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    // 2.3.0 版本后不再通过此处获取,而是通过 server.error.path 配置文件获取
	@Override
	public String getErrorPath() {
		return null;
	}

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}


}
  • public String getErrorPath()

        获取错误 page ,默认为 /error

        2.3.0 版本后不再通过此处获取,而是通过 server.error.path 配置文件获取

  • public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)

        浏览器客户端返回的页面

        一开始以为默认页面会是某个静态的 html ,但是找了半天没找到。后来在 ErrorMvcAutoConfiguration 配置类中找到了这段代码

  • public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)

        机器客户端返回的 json 数据

         知道了原理,那就可以继续了,自己重新实现 BasicErrorController,代码如下

public class CustomErrorController extends BasicErrorController {

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

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

    @Override
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> errorAttributes = getErrorAttributes(request);
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(errorAttributes, status);
    }

    @Override
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    private Map<String, Object> getErrorAttributes(HttpServletRequest request) {
        // 获取异常参数
        ErrorAttributeOptions options = ErrorAttributeOptions.of(
                // 异常 message
                ErrorAttributeOptions.Include.MESSAGE,
                // 异常类型
                ErrorAttributeOptions.Include.EXCEPTION,
                // 异常堆栈,比较长
                // ErrorAttributeOptions.Include.STACK_TRACE,
                // 绑定的错误 error
                ErrorAttributeOptions.Include.BINDING_ERRORS
        );
        return getErrorAttributes(request, options);
    }


}

        记得注册为 Bean

    @Bean
    public CustomErrorController basicErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
                                                      ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
        return new CustomErrorController(errorAttributes, serverProperties.getError(),
                errorViewResolversProvider.getIfAvailable());
    }

        结果演示:

        浏览器客户端:

        机器客户端(postman或者 swagger):

最后,附上 git 地址security-demo

  • 作者:再见丶孙悟空
  • 原文链接:https://blog.csdn.net/qq_38150250/article/details/119696649
    更新时间:2022-10-23 11:45:47