IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot 全局异常处理 -> 正文阅读

[Java知识库]SpringBoot 全局异常处理

前言

????????异常,一个开发人员再熟悉不过的名词,除数不能为 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

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-15 15:22:28  更:2021-08-15 15:23:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 9:09:48-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码