前言

之前一篇文章介绍了基本的统一异常处理思路: Spring MVC/Boot 统一异常处理最佳实践.

上篇文章也有许多人提出了一些问题:

  • 如何区分 Ajax 请求和普通页面请求, 以分别返回 JSON 错误信息和错误页面.
  • 如何结合 HTTP 状态码进行统一异常处理.

今天这篇文章就主要来讲讲这些, 以及其他的一些拓展点.

区分请求方式

其实 Spring Boot 本身是内置了一个异常处理机制的, 会判断请求头的参数来区分要返回 JSON 数据还是错误页面. 源码为: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController, 他会处理 /error 请求. 核心处理代码如下:

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RequestMapping(produces = {"text/html"}
)
// 如果请求头是 text/html, 则找到错误页面, 并返回
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {// 1. 获取 HTTP 错误状态码HttpStatus status = this.getStatus(request);// 2. 调用 getErrorAttributes 获取响应的 map 结果集.Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));// 3. 设置响应头的状态码response.setStatus(status.value());// 4. 获取错误页面的路径ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {// 调用 getErrorAttributes 获取响应的 map 结果集.Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));// 获取 HTTP 错误状态码HttpStatus status = this.getStatus(request);// 返回给页面 JSON 信息.return new ResponseEntity(body, status);
}

这两个方法的共同点是: 他们都调用了 this.getErrorAttributes(…) 方法来获取响应信息.

然后来看看他默认情况下对于 AJAX 请求和 HTML 请求, 分别的返回结果是怎样的:

对于返回错误页面, 其中还调用了一个非常重要的方法: this.resolveErrorView(...) 方法, 源码我就不带大家看了, 他的作用就是根据 HTTP 状态码来去找错误页面, 如 500 错误会去找 /error/500.html, 403 错误回去找 /error/403.html, 如果找不到则再找 /error/4xx.html 或 /error/5xx.html 页面. 还找不到的话, 则会去找 /error.html 页面, 如果都没有配置, 则会使用 Spring Boot 默认的页面. 即:

看到这里, 应该就清楚了, 我们主要需要做四件事:

  1. 发送异常后, 重定向到 BasicErrorController 来处理 (既然Spring Boot 都已经写好了区分请求的功能, 我们就不必要再写这些判断代码了)
  2. 自定义 HTTP 错误状态码
  3. 他返回的信息格式可能不是我们想要的, 所以必须要改造 getErrorAttributes(...) 方法, 以自定义我们向页面返回的数据. (自定义错误信息)
  4. 创建我们自己的 /error/4xx.html 或 /error/5xx.html 等页面, (自定义错误页面)

BasicErrorController

第一点很简单, BasicErrorController 他处理 /error 请求, 我们只需要将页面重定向到 /error 即可, 在 ControllerAdvice 中是这样的:

复制

1
2
3
4
5
6
7
8
9
@ControllerAdvice
public class WebExceptionHandler {@ExceptionHandlerpublic String methodArgumentNotValid(BindException e) {// do somethingreturn "/error";}
}

自定义 HTTP 错误状态码

我们来看下 this.getStatus(request); 的源码, 看他原来时如何获取错误状态码的:

复制

1
2
3
4
5
6
7
8
9
10
11
12
protected HttpStatus getStatus(HttpServletRequest request) {Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");if (statusCode == null) {return HttpStatus.INTERNAL_SERVER_ERROR;} else {try {return HttpStatus.valueOf(statusCode);} catch (Exception var4) {return HttpStatus.INTERNAL_SERVER_ERROR;}}
}

简单来说就是从 request 域中获取 javax.servlet.error.status_code 的值, 如果为 null 或不合理的值, 都返回 500. 既然如何在第一步, 重定向到 /error 之前将其配置到 request 域中即可, 如:

复制

1
2
3
4
5
6
7
8
9
10
@ControllerAdvice
public class WebExceptionHandler {@ExceptionHandlerpublic String methodArgumentNotValid(BindException e, HttpServletRequest request) {request.setAttribute("javax.servlet.error.status_code", 400);// do somethingreturn "forward:/error";}
}

自定义错误信息

也就是 getErrorAttributes 方法, 默认的代码是这样的:

复制

1
2
3
4
5
6
7
8
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map<String, Object> errorAttributes = new LinkedHashMap();errorAttributes.put("timestamp", new Date());this.addStatus(errorAttributes, webRequest);this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);this.addPath(errorAttributes, webRequest);return errorAttributes;
}

他获取了时间戳, 错误状态码, 错误信息, 错误路径等信息, 和我们之前看到默认的返回内容是一致的:

复制

1
2
3
4
5
6
7
{"timestamp": "2019-01-27T07:08:30.011+0000","status": 500,"error": "Internal Server Error","message": "/ by zero","path": "/user/index"
}

同样的思路, 我们将错误信息也放到 request 域中, 然后在 getErrorAttributes 中从 request 域中获取:

复制

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class WebExceptionHandler {@ExceptionHandlerpublic String methodArgumentNotValid(BindException e, HttpServletRequest request) {request.setAttribute("javax.servlet.error.status_code", 400);request.setAttribute("code", 1);request.setAttribute("message", "参数校验失败, xxx");// do somethingreturn "forward:/error";}
}

再继承 DefaultErrorAttributes 类, 重写 getErrorAttributes 方法:

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//@Component
public class MyDefaultErrorAttributes extends DefaultErrorAttributes {@Override//重写 getErrorAttributes方法-添加自己的项目数据public Map<String, Object> getErrorAttributes(WebRequest webRequest,boolean includeStackTrace) {Map<String, Object> map = new HashMap<>();// 从 request 域中获取 codeObject code = webRequest.getAttribute("code", RequestAttributes.SCOPE_REQUEST);// 从 request 域中获取 messageObject message = webRequest.getAttribute("message", RequestAttributes.SCOPE_REQUEST);map.put("code", code);map.put("message", message);return map;}
}

自定义错误页面

我们遵循 SpringBoot 的规则, 在 /error/ 下建立 400.html500.html 等页面细粒度的错误, 并配置一个 /error.html 用来处理细粒度未处理到的其他错误.

/error/400.html

复制

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>400</title>
</head>
<body><h1>400</h1><h1 th:text="${code}"></h1><h1 th:text="${message}"></h1>
</body>
</html>

/error/500.html

复制

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>500</title>
</head>
<body><h1>500</h1><h1 th:text="${code}"></h1><h1 th:text="${message}"></h1>
</body>
</html>

/error.html

复制

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>系统出现了错误</title>
</head>
<body><h1>ERROR PAGE</h1><h1 th:text="${code}"></h1><h1 th:text="${message}"></h1>
</body>
</html>

测试效果

到此位置, 大功告成, 然后来创造一个异常来测试一下效果:

前端 error 处理

现在使用了 HTTP 状态码, 所以 Ajax 请求出现错误后, 需要在每个 Ajax 请求方法中都写 error: function() {} 方法, 甚至麻烦. 好在 jQuery 为我们提供了全局处理 Ajax 的 error 结果的方法 ajaxError() :

复制

1
2
3
4
5
$(document).ajaxError(function(event, response){console.log("错误响应状态码: ",response.status);console.log("错误响应结果: ",response.responseJSON);alert("An error occurred!");
});

结语

回顾一下讲到的这些内容:

  1. 理解 SpringBoot 默认提供的 BasicErrorController
  2. 自定义 HTTP 错误状态码, (通过 request 域的 javax.servlet.error.status_code 参数)
  3. 自定义错误信息, (将我们自定义的错误信息放到 request 域中, 并重写 DefaultErrorAttributes 的 getErrorAttributes 方法, 从 request 域中获取这些信息).
  4. 自定义错误页面, (根据 SpringBoot 查找错误页面的逻辑来自定义错误页面: /error/500.html/error/400.html/error.html)

可以自己根据文章一步一步走一遍, 或者看我写好的演示项目先看看效果, 总是动手实践, 而不是收藏文章并封存。

SpringBoot 统一异常处理最佳实践 -- 拓展篇相关推荐

  1. springboot统一异常处理类及注解参数为数组的写法

    springboot统一异常处理类及注解参数为数组的写法 参考文章: (1)springboot统一异常处理类及注解参数为数组的写法 (2)https://www.cnblogs.com/zhucww ...

  2. SpringCloud 应用在 Kubernetes 上的最佳实践 — 部署篇(工具部署)

    作者 | 孤弋  阿里云高级技术专家,负责 EDAS 的开发和用户体验优化工作. 导读:上一篇文章<SpringCloud 应用在 Kubernetes 上的最佳实践 - 部署篇(开发部署)&g ...

  3. java try catch陷阱_Java异常处理最佳实践及陷阱防范

    原标题:Java异常处理最佳实践及陷阱防范 出自<深夜里的程序猿> 作者:wangzenghuang 前言 不管在我们的工作还是生活中,总会出现各种"错误",各种突发的 ...

  4. springboot统一异常处理及返回数据的处理

    springboot统一异常处理及返回数据的处理 参考文章: (1)springboot统一异常处理及返回数据的处理 (2)https://www.cnblogs.com/renshengruozhi ...

  5. 蒲公英平台用法的最佳实践(Android篇)

    蒲公英平台(http://www.pgyer.com)用法的最佳实践(Android篇) 蒲公英主要提供了app托管分发和SDK功能(异常上报,摇一摇反馈,新版本检测),当然还有各种各样的客户端(ma ...

  6. Java异常处理最佳实践

    在 Java 中处理异常并不是一个简单的事情.不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等.这也是绝大多数开发团队都会制定一些规 ...

  7. 分库分表技术演进最佳实践-修订篇

    https://www.itcodemonkey.com/article/10048.html 每个优秀的程序员和架构师都应该掌握分库分表,这是我的观点. 移动互联网时代,海量的用户每天产生海量的数量 ...

  8. SpringCloud 应用在 Kubernetes 上的最佳实践 — 部署篇(开发部署)

    作者 | 孤弋  阿里云高级技术专家,负责 EDAS 的开发和用户体验优化工作. 导读:在上一篇文章<SpringCloud 应用在 Kubernetes 上的云上实践 - 开发篇>中讲到 ...

  9. SpringCloud 应用在 Kubernetes 上的最佳实践 —— 开发篇

    作者 | 孤弋  阿里云高级技术专家,负责 EDAS 的开发和用户体验优化工作. 前言 近年来,云原生.Kubernetes.微服务.SpringCloud 这些名词在技术圈内不绝于耳,数据显示,使用 ...

  10. SpringCloud应用在Kubernetes上的最佳实践—开发篇

    作者 | 孤弋  阿里云高级技术专家,负责 EDAS 的开发和用户体验优化工作. 前言 近年来,云原生.Kubernetes.微服务.SpringCloud 这些名词在技术圈内不绝于耳,数据显示,使用 ...

最新文章

  1. 【背景建模】基于时空特征(续2)
  2. 智能车竞赛技术报告 | 智能视觉组 - 大连海事大学 - 菜鸡啄米
  3. 自学web前端的方法都有哪些?新手怎么学HTML5
  4. shell for循环命令行_精心汇总的24道shell脚本面试题
  5. Python apply函数
  6. 时代风口下,东软集团软件业务怎么转型?
  7. Raspberry Pi, UPNP(二), Scala
  8. python如何读取csv文件列表页_每25行读取一个csv文件,并使用python传递到列表
  9. linux lvm 系统快照,系统运维|在 LVM中 录制逻辑卷快照并恢复(第三部分)
  10. 新版二开cp盲盒小纸条月老小程序源码
  11. JAVA开发面试常问问题总结4
  12. 总结一下网站注入与防范的方法
  13. 虚拟机与ubuntu(一):VirtureBox虚拟机安装和ubuntu14搭建
  14. 记一次调试YOLOv5+DeepSort车辆跟踪项目的经过
  15. 中国历代更改重复地名及其现实意义
  16. 局域网常见问题解决方案之你可能没有权限使用网络资源无法访问网上邻居
  17. SpringBoot+tomcat发布之war包发布
  18. oracle可恢复的等待,【案例】Oracle RAC恢复报错RMAN-06004 RMAN-20003解决办法笔记
  19. vue+animation实现翻页动画
  20. 在鹅厂如何带薪刷视频?

热门文章

  1. java控制反转_控制反转(IOC)模式
  2. php中的数值型字符串相加 相比较( ==)
  3. oracle 分区表详解
  4. [C++]动态规划系列之币值最大化
  5. C语言中函数中传入一个数组,并且返回一个数组
  6. Microsoft SQL Server Query Processor Internals and Architecture
  7. java连接远程服务器之manyged-ssh2 (windows和linux)
  8. SqlParameter类中的两对好基友:SqlDbType与DbType、SqlValue与Value
  9. liunx 下mysql 的安装
  10. zebra(斑马)PDA扫码uniapp程序小demo