Spring Boot统一异常处理最佳实践(拓展篇)
作者:赵俊 发布时间:2023-10-29 16:00:04
前言
之前一篇文章介绍了基本的统一异常处理思路: Spring MVC/Boot 统一异常处理最佳实践.
上篇文章也有许多人提出了一些问题:
如何区分 Ajax 请求和普通页面请求, 以分别返回 JSON 错误信息和错误页面.
如何结合 HTTP 状态码进行统一异常处理.
区分请求方式
其实 Spring Boot 本身是内置了一个异常处理机制的, 会判断请求头的参数来区分要返回 JSON 数据还是错误页面. 源码为: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
, 他会处理 /error 请求. 核心处理代码如下:
@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 默认的页面. 即:
看到这里, 应该就清楚了, 我们主要需要做四件事:
发送异常后, 重定向到 BasicErrorController 来处理 (既然Spring Boot 都已经写好了区分请求的功能, 我们就不必要再写这些判断代码了)
自定义 HTTP 错误状态码
他返回的信息格式可能不是我们想要的, 所以必须要改造
getErrorAttributes(...)
方法, 以自定义我们向页面返回的数据. (自定义错误信息)创建我们自己的 /error/4xx.html 或 /error/5xx.html 等页面, (自定义错误页面)
BasicErrorController
第一点很简单, BasicErrorController 他处理 /error 请求, 我们只需要将页面重定向到 /error 即可, 在 ControllerAdvice 中是这样的:
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler
public String methodArgumentNotValid(BindException e) {
// do something
return "/error";
}
}
自定义 HTTP 错误状态码
我们来看下 this.getStatus(request); 的源码, 看他原来时如何获取错误状态码的:
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 域中即可, 如:
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler
public String methodArgumentNotValid(BindException e, HttpServletRequest request) {
request.setAttribute("javax.servlet.error.status_code", 400);
// do something
return "forward:/error";
}
}
自定义错误信息
也就是 getErrorAttributes 方法, 默认的代码是这样的:
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;
}
他获取了时间戳, 错误状态码, 错误信息, 错误路径等信息, 和我们之前看到默认的返回内容是一致的:
{
"timestamp": "2019-01-27T07:08:30.011+0000",
"status": 500,
"error": "Internal Server Error",
"message": "/ by zero",
"path": "/user/index"
}
同样的思路, 我们将错误信息也放到 request 域中, 然后在 getErrorAttributes 中从 request 域中获取:
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler
public String methodArgumentNotValid(BindException e, HttpServletRequest request) {
request.setAttribute("javax.servlet.error.status_code", 400);
request.setAttribute("code", 1);
request.setAttribute("message", "参数校验失败, xxx");
// do something
return "forward:/error";
}
}
再继承 DefaultErrorAttributes 类, 重写 getErrorAttributes 方法:
//@Component
public class MyDefaultErrorAttributes extends DefaultErrorAttributes {
@Override
//重写 getErrorAttributes方法-添加自己的项目数据
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
Map<String, Object> map = new HashMap<>();
// 从 request 域中获取 code
Object code = webRequest.getAttribute("code", RequestAttributes.SCOPE_REQUEST);
// 从 request 域中获取 message
Object message = webRequest.getAttribute("message", RequestAttributes.SCOPE_REQUEST);
map.put("code", code);
map.put("message", message);
return map;
}
}
自定义错误页面
我们遵循 SpringBoot 的规则, 在 /error/ 下建立 400.html, 500.html 等页面细粒度的错误, 并配置一个 /error.html 用来处理细粒度未处理到的其他错误.
/error/400.html
<!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
<!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
<!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() :
$(document).ajaxError(function(event, response){
console.log("错误响应状态码: ",response.status);
console.log("错误响应结果: ",response.responseJSON);
alert("An error occurred!");
});
结语
回顾一下讲到的这些内容:
理解 SpringBoot 默认提供的 BasicErrorController
自定义 HTTP 错误状态码, (通过 request 域的
javax.servlet.error.status_code
参数)自定义错误信息, (将我们自定义的错误信息放到 request 域中, 并重写 DefaultErrorAttributes 的 getErrorAttributes 方法, 从 request 域中获取这些信息).
自定义错误页面, (根据 SpringBoot 查找错误页面的逻辑来自定义错误页面: /error/500.html, /error/400.html, /error.html)
可以自己根据文章一步一步走一遍, 或者看我写好的演示项目先看看效果, 总是动手实践, 而不是收藏文章并封存。
演示项目地址: https://github.com/zhaojun1998/exception-handler-demo
来源:http://www.zhaojun.im/springboot-exception-expand/


猜你喜欢
- 正文将 Java 对象序列化为二进制文件的 Java 序列化技术是 Java 系列技术中一个较为重要的技术点,在大部分情况下,开发人员只需要
- Seata介绍Seata:Simple Extensible Autonomous Transaction Architecture,简易可
- 话不多说,跟着小编一起来看下吧using System;using System.Collections.Generic;using Sys
- 动态参数拼接的查询语句–传入参数类型为自定义数据类型<select id="queryMessageList" p
- 该注解用于将 Controller 的方法返回的对象,通过 HttpMessageConverter 接口转换为指定格式的数据如:json,
- 实例如下:package bys.utils;import java.util.Date;/** * Created by toutou o
- JAVA中实现pdf转图片可以通过第三方提供的架包,这里介绍几种常用的,可以根据自身需求选择使用。一、icepdf。有收费版和开源版,几种方
- 什么是Dozer?Dozer是一种Java Bean到Java Bean的映射器,递归地将数据从一个对象复制到另一个对象,它是一个强大的,通
- 多线程内容大致分两部分,其一是异步操作,可通过专用,线程池,Task,Parallel,PLINQ等,而这里又涉及工作线程与IO线程;其二是
- 依赖如下:<dependency> <groupId>org.springframework.boot&
- 本文实例讲述了C#把数组中的某个元素取出来放到第一个位置的实现方法。分享给大家供大家参考。具体分析如下:如何取出数组中符合某种条件的元素,然
- 好问题。答案就是这篇文章的题目所建议的,这是一种合理的设计。在这种情况下,newInstance()方法是一种“静态工厂方法",让
- SpringBootWeb开发回顾一下:springboot帮助我们配置了什么,能不能进行修改,能修改哪些,能否扩展?xxxAutoConf
- tomcat中文乱码问题这几天测试的兄弟发现了项目中存在乱码问题 经过排查发现是tomcat中的问题 于是在server.xml中添加了如下
- 熬夜写完,尚有不足,但仍在努力学习与总结中,而您的点赞与关注,是对我最大的鼓励!在一些本地化项目开发当中,存在这样一种需求,即开发完成的项目
- 在C/C++跨平台开发中,我们知道在Windows上可以通过VS,进行单步断点调试,这非常方便。但是我们如果编译好的动态库so,想要跟踪下其
- 为开发团队选择一款优秀的MVC框架是件难事儿,在众多可行的方案中决择需要很高的经验和水平。你的一个决定会影响团队未来的几年。要考虑方面太多:
- 上周工作中遇到一个奇怪的问题,解决之后想想还是写出来和大家分享一下。故障描述:在A程序中使用Process.Start方法调用一个B.exe
- 本文实例讲述了Java设计模式之模板方法模式。分享给大家供大家参考,具体如下:我们在生活中,很多事情都包含特定的一些步骤。如去银行办理业务,
- 平时开发中经常遇到的很小的问题,这里记录一下。一般在AndroidManifest.xml中添加了android:windowSoftInp