SpringBoot中的全局异常处理
SpringBoot中的全局异常处理
本篇要点
- 介绍SpringBoot默认的异常处理机制。
 - 如何定义错误页面。
 - 如何自定义异常数据。
 - 如何自定义视图解析。
 - 介绍@ControllerAdvice注解处理异常。
 
一、SpringBoot默认的异常处理机制
默认情况下,SpringBoot为以下两种情况提供了不同的响应方式:
- Browser Clients浏览器客户端:通常情况下请求头中的Accept会包含text/html,如果未定义/error的请求处理,就会出现如下html页面:Whitelabel Error Page,关于error页面的定制,接下来会详细介绍。
 

- 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中可以尝试定制错误页面,定制方式主要分静态和动态两种:
- 静态异常页面
 
在classpath:/public/error或classpath:/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页面。
- 使用模板的动态页面
 
放置在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实例。
因此,我们就该知道如何去自定义异常数据属性:
- 实现ErrorAttributes接口。
 - 继承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。
参考阅读
SpringBoot中的全局异常处理的更多相关文章
- SpringBoot优雅的全局异常处理
		
前言 本篇文章主要介绍的是SpringBoot项目进行全局异常的处理. SpringBoot全局异常准备 说明:如果想直接获取工程那么可以直接跳到底部,通过链接下载工程代码. 开发准备 环境要求 JD ...
 - 在.NET Core程序中设置全局异常处理
		
以前我们想设置全局异常处理只需要这样的代码: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledExc ...
 - 在SpringBoot中配置全局捕获异常
		
前言 之前写过一篇博客是使用spring利用HandlerExceptionResolver实现全局异常捕获 里面使用spring的HandlerExceptionResolver接口来实现全局的异常 ...
 - Global Error Handling in ASP.NET Web API 2(webapi2 中的全局异常处理)
		
目前,在Web API中没有简单的方法来记录或处理全局异常(webapi1中).一些未处理的异常可以通过exception filters进行处理,但是有许多情况exception filters无法 ...
 - WEB 项目中的全局异常处理
		
在web 项目中,遇到异常一般有两种处理方式:try.....catch....:throw 通常情况下我们用try.....catch.... 对异常进行捕捉处理,可是在实际项目中随时的进行异常捕捉 ...
 - 【5min+】AspNet Core中的全局异常处理
		
系列介绍 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net ...
 - ASP.NET Core MVC 中设置全局异常处理方式
		
在asp.net core mvc中,如果有未处理的异常发生后,会返回http500错误,对于最终用户来说,显然不是特别友好.那如何对于这些未处理的异常显示统一的错误提示页面呢? 在asp.net c ...
 - 7.2WebApi2中的全局异常处理
		
现在在WebApi中还没有一种简单的方式去记录或者处理全局的异常.一些未处理的异常可以处理通过异常过滤器,但有大量的异常是异常过滤器不能处理的.例如: 从控制器的构造函数引发的异常. 从消息处理程序引 ...
 - Spring Boot2 系列教程(十三)Spring Boot 中的全局异常处理
		
在 Spring Boot 项目中 ,异常统一处理,可以使用 Spring 中 @ControllerAdvice 来统一处理,也可以自己来定义异常处理方案.Spring Boot 中,对异常的处理有 ...
 
随机推荐
- ZooKeeper 【不仅仅是注册中心,你还知道有哪些?】
			
什么是 ZooKeeper Apache ZooKeeper 是一个开源的实现高可用的分布式协调服务器.ZooKeeper是一种集中式服务,用于维护配置信息,域名服务,提供分布式同步和集群管理.所有这 ...
 - 内存操作【memset】【memcpy】
			
void *memset(void *s, int c, unsigned long n); 将指针变量 s 所指向的前 n 字节的内存单元用一个"整数" c 替换,注意 c 是 ...
 - 【数量技术宅 | Python爬虫系列分享】实时监控股市重大公告的Python爬虫
			
实时监控股市重大公告的Python爬虫小技巧 精力有限的我们,如何更加有效率地监控信息? 很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告.如果现有的软件没有办法实现我们的需求,那么就 ...
 - MyEclpse 2015在线安装Gradle插件图解
			
MyEclpse 2015 安装Gradle插件 安装方式:在线安装 一.如何获得Gradle插件在线安装地址 Gradle插件最新在线安装地址可在如下网址中查找: https://github.co ...
 - VS2013中带命令行参数的调试方法---C++
			
今天先记录一下(也是传说中大神喜欢装逼的comment line)c++中向主函数int main(int argc,char** argv )传递4中方法,欢迎添加新方法, 然后可以参考别人写的很好 ...
 - P4568 [JLOI2011]飞行路线 / P2939 [USACO09FEB]Revamping Trails G
			
题目描述 Link Alice 和 Bob 现在要乘飞机旅行,他们选择了一家相对便宜的航空公司.该航空公司一共在 \(n\) 个城市设有业务,设这些城市分别标记为 \(0\) 到 \(n-1\),一共 ...
 - C#实现——十大排序算法之选择排序
			
选择排序法 1.工作原理(算法思路) 给定一个待排序数组,找到数组中最小的那个元素 如果最小元素不是待排序数组的第一个元素,则将其和第一个元素互换 在剩下的元素中,重复1.2过程,直到排序完成. 2. ...
 - vue3.0版本安装
			
如果安装过其他版本的vue的话先卸载 npm uninstall -g vue-cli //卸载指令 卸载不会影响以前项目的启动 然后安装 NPM安装: npm install -g @vue/cli ...
 - Azure Cosmos DB (二) SQL API 操作
			
一,引言 还记得国庆期间,我们学习了一下关于Azure Cosmos DB 的一些基础知识以及Azure Cosmos DB 的几种支持数据库类型.今天就开始分享一些实战操作,如何通过Azure Po ...
 - 这类注解都不知道,还好意思说会Spring Boot ?
			
前言 不知道大家在使用Spring Boot开发的日常中有没有用过@Conditionalxxx注解,比如@ConditionalOnMissingBean.相信看过Spring Boot源码的朋友一 ...