• SpringBoot中的全局异常处理


    本篇要点

    • 介绍SpringBoot默认的异常处理机制。
    • 如何定义错误页面。
    • 如何自定义异常数据。
    • 如何自定义视图解析。
    • 介绍@ControllerAdvice注解处理异常。

    一、SpringBoot默认的异常处理机制

    默认情况下,SpringBoot为以下两种情况提供了不同的响应方式:

    1. Browser Clients浏览器客户端:通常情况下请求头中的Accept会包含text/html,如果未定义/error的请求处理,就会出现如下html页面:Whitelabel Error Page,关于error页面的定制,接下来会详细介绍。

    1. Machine Clients机器客户端:Ajax请求,返回ResponseEntity实体json字符串信息。
    {
        "timestamp": "2020-10-30T15:01:17.353+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "trace": "java.lang.ArithmeticException: / by zero...",
        "message": "/ by zero",
        "path": "/"
    }
    

    SpringBoot默认提供了程序出错的结果映射路径/error,这个请求的处理逻辑在BasicErrorController中处理,处理逻辑如下:

    // 判断mediaType类型是否为text/html
    @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 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);
    }
    

    二、错误页面的定制

    相信Whitelabel Error Pag页面我们经常会遇到,这样体验不是很好,在SpringBoot中可以尝试定制错误页面,定制方式主要分静态和动态两种:

    1. 静态异常页面

    classpath:/public/errorclasspath:/static/error路径下定义相关页面:文件名应为确切的状态代码,如404.html,或系列掩码,如4xx.html。

    举个例子,如果你想匹配404或5开头的所有状态代码到静态的html文件,你的文件目录应该是下面这个样子:

    src/
     +- main/
         +- java/
         |   + <source code>
         +- resources/
             +- public/
                 +- error/
                 |   +- 404.html
        		 |   +- 5xx.html
                 +- <other public assets>
    

    如果500.html和5xx.html同时生效,那么优先展示500.html页面。

    1. 使用模板的动态页面

    放置在classpath:/templates/error路径下:这里使用thymeleaf模板举例:

    src/
     +- main/
         +- java/
         |   + <source code>
         +- resources/
             +- templates/
                 +- error/
                 |   +- 5xx.html
                 +- <other templates>
    

    页面如下:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
        <head>
            <meta charset="UTF-8">
            <title>5xx</title>
        </head>
        <body>
            <h1>5xx</h1>
            <p th:text="'error -->'+ ${error}"></p>
            <p th:text="'status -->' + ${status}"></p>
            <p th:text="'timestamp -->' + ${timestamp}"></p>
            <p th:text="'message -->' + ${message}"></p>
            <p th:text="'path -->' +${path}"></p>
            <p th:text="'trace -->' + ${trace}"></p>
        </body>
    </html>
    

    如果静态页面和动态页面同时存在且都能匹配,SpringBoot对于错误页面的优先展示规则如下:

    如果发生了500错误:

    动态500 -> 静态500 -> 动态5xx -> 静态5xx

    三、自定义异常数据

    默认的数据主要是以下几个,这些数据定义在org.springframework.boot.web.servlet.error.DefaultErrorAttributes中,数据的定义在getErrorAttributes方法中。

    DefaultErrorAttributes类在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration自动配置类中定义:

    	@Bean
    	@ConditionalOnMissingBean(value = ErrorAttributes.class,
                                  search = SearchStrategy.CURRENT)
    	public DefaultErrorAttributes errorAttributes() {
    		return new DefaultErrorAttributes();
    	}
    

    如果我们没有提供ErrorAttributes的实例,SpringBoot默认提供一个DefaultErrorAttributes实例。

    因此,我们就该知道如何去自定义异常数据属性:

    1. 实现ErrorAttributes接口。
    2. 继承DefaultErrorAttributes,本身已经定义对异常数据的处理,继承更具效率。

    定义方式如下:

    /**
     * 自定义异常数据
     * @author Summerday
     */
    @Slf4j
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
            Map<String, Object> map = super.getErrorAttributes(webRequest, options);
            if(map.get("status").equals(500)){
                log.warn("服务器内部异常");
            }
            return map;
        }
    }
    

    四、自定义异常视图

    自定义视图的加载逻辑存在于BasicErrorController类的errorHtml方法中,用于返回一个ModelAndView对象,这个方法中,首先通过getErrorAttributes获取到异常数据,然后调用resolveErrorView去创建一个ModelAndView对象,只有创建失败的时候,用户才会看到默认的错误提示页面。

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

    DefaultErrorViewResolver类的resolveErrorView方法:

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        // 以异常状态码作为视图名去locations路径中去查找页面
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        // 如果找不到,以4xx,5xx series去查找
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }
    

    那么如何自定义呢?和自定义异常数据相同,如果我们定义了一个ErrorViewResolver的实例,默认的配置就会失效。

    /**
     * 自定义异常视图解析
     * @author Summerday
     */
    
    @Component
    public class MyErrorViewResolver extends DefaultErrorViewResolver {
    
        public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
            super(applicationContext, resourceProperties);
        }
    
        @Override
        public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
            return new ModelAndView("/hyh/resolve",model);
        }
    }
    

    此时,SpringBoot将会去/hyh目录下寻找resolve.html页面。

    五、@ControllerAdvice注解处理异常

    前后端分离的年代,后端往往需要向前端返回统一格式的json信息,以下为封装的AjaxResult对象:

    public class AjaxResult extends HashMap<String, Object> {
        
        //状态码
        public static final String CODE_TAG = "code";
        
        //返回内容
        public static final String MSG_TAG = "msg";
        
        //数据对象
        public static final String DATA_TAG = "data";
    
        private static final long serialVersionUID = 1L;
    
        public AjaxResult() {
        }
    
        public AjaxResult(int code, String msg) {
            super.put(CODE_TAG, code);
            super.put(MSG_TAG, msg);
        }
    
        public AjaxResult(int code, String msg, Object data) {
            super.put(CODE_TAG, code);
            super.put(MSG_TAG, msg);
            if (data != null) {
                super.put(DATA_TAG, data);
            }
        }
    
        public static AjaxResult ok() {
            return AjaxResult.ok("操作成功");
        }
    
        public static AjaxResult ok(Object data) {
            return AjaxResult.ok("操作成功", data);
        }
    
        public static AjaxResult ok(String msg) {
            return AjaxResult.ok(msg, null);
        }
    
        public static AjaxResult ok(String msg, Object data) {
            return new AjaxResult(HttpStatus.OK.value(), msg, data);
        }
    
        public static AjaxResult error() {
            return AjaxResult.error("操作失败");
        }
    
        public static AjaxResult error(String msg) {
            return AjaxResult.error(msg, null);
        }
    
        public static AjaxResult error(String msg, Object data) {
            return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data);
        }
    
        public static AjaxResult error(int code, String msg) {
            return new AjaxResult(code, msg, null);
        }
    }
    

    根据业务的需求不同,我们往往也需要自定义异常类,便于维护:

    /**
     * 自定义异常
     *
     * @author Summerday
     */
    public class CustomException extends RuntimeException {
    
        private static final long serialVersionUID = 1L;
    
        private Integer code;
    
        private final String message;
    
        public CustomException(String message) {
            this.message = message;
        }
    
        public CustomException(String message, Integer code) {
            this.message = message;
            this.code = code;
        }
    
        public CustomException(String message, Throwable e) {
            super(message, e);
            this.message = message;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    
        public Integer getCode() {
            return code;
        }
    }
    

    @ControllerAdvice和@RestControllerAdvice这俩注解的功能之一,就是做到Controller层面的异常处理,而两者的区别,与@Controller和@RestController差不多。

    @ExceptionHandler指定需要处理的异常类,针对自定义异常,如果是ajax请求,返回json信息,如果是普通web请求,返回ModelAndView对象。

    /**
     * 全局异常处理器
     * @author Summerday
     */
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        private static final Logger log = 
            LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    
        @ExceptionHandler(CustomException.class)
        public Object handle(HttpServletRequest request, CustomException e) {
            AjaxResult info = AjaxResult.error(e.getMessage());
            log.error(e.getMessage());
            // 判断是否为ajax请求
            if (isAjaxRequest(request)) {
                return info;
            }
            ModelAndView mv = new ModelAndView();
            mv.setViewName("custom"); // templates/custom.html
            mv.addAllObjects(info);
            mv.addObject("url", request.getRequestURL());
            return mv;
        }
    
        private boolean isAjaxRequest(HttpServletRequest request) {
            return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        }
    }
    

    在Controller层,人为定义抛出异常:

    @RestController
    public class TestController {
    
        @GetMapping("/ajax")
        public AjaxResult ajax() {
            double alpha = 0.9;
            if (Math.random() < alpha) {
                throw new CustomException("自定义异常!");
            }
            return AjaxResult.ok();
        }
    }
    

    最后,通过/templates/custom.html定义的动态模板页面展示数据:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
        <head>
            <meta charset="UTF-8">
            <title>自定义界面</title>
        </head>
        <body>
            <p th:text="'msg -->'+ ${msg}"></p>
            <p th:text="'code -->'+ ${code}"></p>
            <p th:text="'url -->'+ ${url}"></p>
        </body>
    </html>
    

    源码下载

    本文内容均为对优秀博客及官方文档总结而得,原文地址均已在文中参考阅读处标注。最后,文中的代码样例已经全部上传至Gitee:https://gitee.com/tqbx/springboot-samples-learn

    参考阅读

  • 相关阅读:
    MySQL Unable to convert MySQL datetime value to System.DateTime 解决方案
    Zend 无限试用
    SQL 触发器
    C# 多线程示例
    JS 实现打印
    apache开启.htaccess
    MySQL 安装包下载教程
    js系列(10)js的运用(二)
    js系列(9)js的运用(一)
    js系列(8)简介
  • 原文地址:https://www.cnblogs.com/summerday152/p/13906715.html
Copyright © 2020-2023  润新知