一. 默认错误处理
SpringBoot 默认为我们提供了BasicErrorController 来处理全局错误/异常,并在Servlet容器中注册error为全局错误页。所以在浏览器端访问,发生错误时,我们能及时看到错误/异常信息和HTTP状态等反馈。工作原理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); }
|
例如下面这两个错误,对于日常开发而言,再熟悉不过了。
二. 统一异常处理
默认的英文空白页,显然不能够满足我们复杂多变的需求,因此我们可以通过专门的类来收集和管理这些异常信息,这样做不仅可以减少控制层的代码量,还有利于线上故障排查和紧急短信通知等。
具体步骤
为了让小伙伴少走一些弯路,楼主根据官方源码和具体实践,提炼这些核心工具类:
- ErrorInfo 错误信息
- ErrorInfoBuilder 错误信息的构建工具
注:在CSDN和大牛博客中,不乏关于Web应用的统一异常处理的教程,但更多的是基础学习使用,并不能投入实际项目使用,为了让大家少走一些弯路和快速投入生产,楼主根据官方源码和项目实践,提炼出了核心工具类(ErrorInfoBuilder ),将构建异常信息的逻辑从异常处理器/控制器中抽离出来,让大家通过短短几行代码就能获取丰富的异常信息,更专注于业务开发!!
1. 统一异常处理器(GlobalErrorHandler)
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 27 28 29 30 31
| package com.hehe.error;
@ControllerAdvice public class GlobalErrorHandler {
private final static String DEFAULT_ERROR_VIEW = "error";
@Autowired private ErrorInfoBuilder errorInfoBuilder;
@ExceptionHandler(Exception.class) @ResponseBody public Object exceptionHandler(HttpServletRequest request, Throwable error) {
if (isAjaxRequest(request)) { return errorInfoBuilder.getErrorInfo(request,error); } return new ModelAndView(DEFAULT_ERROR_VIEW, "errorInfo", errorInfoBuilder.getErrorInfo(request, error)); }
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); }
}
|
2. 统一异常信息(ErrorInfo)
虽然官方提供了ErrorAttributes来存储错误信息,但其返回的是存储结构是Map,为了更好的服务统一异常,这里我们统一采用标准的ErrroInfo来记载错误信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.hehe.error;
public class ErrorInfo {
private String time; private String url; private String error; String stackTrace; private int statusCode; private String reasonPhrase;
...
}
|
3. 统一异常信息的构建工具(ErrorInfoBuilder)
ErrorInfoBuilder 作为核心工具类,其意义不言而喻,重点API:
注:正确使用ErrorInfoBuilder,可以让处理器减少80%的代码。总而言之,ErrorInfoBuilder是个好东西,值得大家细细琢磨。
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| package com.hehe.error;
@Order(Ordered.HIGHEST_PRECEDENCE) @Component public class ErrorInfoBuilder implements HandlerExceptionResolver, Ordered {
private final static String ERROR_NAME = "hehe.error";
private ErrorProperties errorProperties;
public ErrorProperties getErrorProperties() { return errorProperties; }
public void setErrorProperties(ErrorProperties errorProperties) { this.errorProperties = errorProperties; }
public ErrorInfoBuilder(ServerProperties serverProperties) { this.errorProperties = serverProperties.getError(); }
public ErrorInfo getErrorInfo(HttpServletRequest request) {
return getErrorInfo(request, getError(request)); }
public ErrorInfo getErrorInfo(HttpServletRequest request, Throwable error) { ErrorInfo errorInfo = new ErrorInfo(); errorInfo.setTime(LocalDateTime.now().toString()); errorInfo.setUrl(request.getRequestURL().toString()); errorInfo.setError(error.toString()); errorInfo.setStatusCode(getHttpStatus(request).value()); errorInfo.setReasonPhrase(getHttpStatus(request).getReasonPhrase()); errorInfo.setStackTrace(getStackTraceInfo(error, isIncludeStackTrace(request))); return errorInfo; }
public Throwable getError(HttpServletRequest request) { Throwable error = (Throwable) request.getAttribute(ERROR_NAME); if (error == null) { error = (Throwable) request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE); } if (error != null) { while (error instanceof ServletException && error.getCause() != null) { error = error.getCause(); } } else { String message = (String) request.getAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE); if (StringUtils.isEmpty(message)) { HttpStatus status = getHttpStatus(request); message = "Unknown Exception But " + status.value() + " " + status.getReasonPhrase(); } error = new Exception(message); } return error; }
public HttpStatus getHttpStatus(HttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE); try { return statusCode != null ? HttpStatus.valueOf(statusCode) : HttpStatus.INTERNAL_SERVER_ERROR; } catch (Exception ex) { return HttpStatus.INTERNAL_SERVER_ERROR; } }
public String getStackTraceInfo(Throwable error, boolean flag) { if (!flag) { return "omitted"; } StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); stackTrace.flush(); return stackTrace.toString(); }
public boolean isIncludeStackTrace(HttpServletRequest request) {
IncludeStacktrace includeStacktrace = errorProperties.getIncludeStacktrace();
if (includeStacktrace == IncludeStacktrace.ALWAYS) { return true; } if (includeStacktrace == IncludeStacktrace.ON_TRACE_PARAM) { String parameter = request.getParameter("trace"); return parameter != null && !"false".equals(parameter.toLowerCase()); } return false; }
@Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { request.setAttribute(ERROR_NAME, ex); return null; }
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
|
注:ErrorBuilder之所以使用@Order注解和实现HandlerExceptionResolver接口是为了获取错误/异常,通常情况下@ExceptionHandler并不需要这么做,因为在映射方法注入Throwable就可以获得错误/异常,这是主要是为了ErrorController根据Request对象快速获取错误/异常。
4. 控制层代码(Controller)
上述,错误/异常处理器、错误信息、错误信息构建工具全部完成,我们编写控制层代码来测试相关效果。
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 27 28 29 30 31 32 33 34 35 36 37 38
| package com.hehe;
@SpringBootApplication @RestController public class ErrorHandlerApplication {
private void randomException() throws Exception { Exception[] exceptions = { new NullPointerException(), new ArrayIndexOutOfBoundsException(), new NumberFormatException(), new SQLException()}; double probability = 0.75; if (Math.random() < probability) { throw exceptions[(int) (Math.random() * exceptions.length)]; } else { }
}
@GetMapping("/") public List index() throws Exception { randomException(); return Arrays.asList("正常用户数据1!", "正常用户数据2! 请按F5刷新!!"); }
public static void main(String[] args) { SpringApplication.run(ErrorHandlerApplication.class, args); } }
|
5. 页面代码(Thymeleaf)
代码完成之后,我们需要编写一个异常信息页面。为了方便演示,我们在resources目录下创建templates目录,并新建文件exception.html。页面代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>GlobalError</title> </head> <h1>统一祖国 振兴中华</h1> <h2>服务异常,请稍后再试。</h2> <div th:object="${errorInfo}"> <h3 th:text="*{'发生时间:'+time}"></h3> <h3 th:text="*{'访问地址:'+url}"></h3> <h3 th:text="*{'问题类型:'+error}"></h3> <h3 th:text="*{'通信状态:'+statusCode+','+reasonPhrase}"></h3> <h3 th:text="*{'堆栈信息:'+stackTrace}"></h3> </div> </body> </html>
|
注:SpringBoot默认支持很多种模板引擎(如Thymeleaf、FreeMarker),并提供了相应的自动配置,做到开箱即用。默认的页面加载路径是 src/main/resources/templates ,如果放到其它目录需在配置文件指定。(举例:spring.thymeleaf.prefix=classpath:/views/ )
6. 引入依赖(POM文件)
以前操作之前,不要忘了在pom.xml 引入相关依赖:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hehe</groupId> <artifactId>springboot-error-handler</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-error-handler</name> <description>SpringBoot 统一异常处理</description>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.M4</version> <relativePath/> </parent>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<repositories> <repository> <id>spring-snapshots</id> <url>http://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <url>http://repo.spring.io/milestone</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <url>http://repo.spring.io/snapshot</url> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <url>http://repo.spring.io/milestone</url> </pluginRepository> </pluginRepositories>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
|
7. 开始测试
上述步骤完成之后,打开启动类GlobalExceptionApplication,启动项目然后进行测试。本案例-项目结构图如下:
**测试效果:**在浏览器输入 http://localhost:8080 多次按F5刷新,然后查看页面效果。
三. 使用@ExceptionHandler的不足之处
关于实现Web应用统一异常处理的两种方法比较:
特性 | @ExceptionHandler | ErrorController |
---|
获取异常 | 通过方法参数注入 | 通过ErrorInfoBuilder获取 |
返回类型 | 若请求的类型为Ajax则返回JSON,否则返回页面. | 若请求的媒介类型为HTML 则返回页面 ,否则返回JSON. |
缺点 | 无法处理404类异常 | 很强大,可处理全部错误/异常 |
1. 使用@ExceptionHandler 为什么无法处理404错误/异常?
- 答:因为SpringMVC优先处理(Try Catch)掉了资源映射不存在的404类错误/异常,虽然在响应信息注入了404的HttpStatus通信信息,但木有了异常,肯定不会进入@ExceptionHandler 的处理逻辑。
2. 使用@ExceptionHandler + 抛出异常 是否可取?
- 答:由上图可知@ExceptionHanlder的最大不足之处是无法直接捕获404背后的异常,网上流传通过取消资源目录映射来解决无404问题是不可取的,属于越俎代庖的做法。
1 2
| spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false
|
3. 为什么推荐ErrorController 替代 @ExceptionHandler ?
- 使用ErrorController可以处理 全部错误/异常 。
- 使用ErrorController+ErrorInfoBuilder 在单个方法里面可以针对不同的Exception来添加详细的错误信息,具体做法:拓展ErrorInfoBuilder的getErrorInfo方法来添加错误信息(例如:ex instanceof NullPointerException Set xxx)。
注意:实际上,目前SpringBoot官方就是通过ErrorController来做的统一错误/异常处理,但遗憾的是,关于这方面的官方文档并没有给出详细示例,仅仅是一笔带过,大概官方认为@ExceptionHandler 够用??而网上也甚少人具体提及ErrorController和ErrorAttribute 背后一整套的实现逻辑,也正是如此,促使楼主决心写下这篇文章,希望给大家带来帮助,少走一些弯路!!
四. 使用ErrorController替代@ExceptionHandler
4. 如何快速使用 ErrorController ?
回答:经过楼主的精心设计,ErrorInfoBuilder 可以无缝对接ErrorController (即上述两种错误/异常处理均共用此工具类),你只需要做的是:将本案例的ErrorInfo和ErrorInfoBuilder 拷贝进项目,简单编写ErrorController 跳转页面和返回JSON即可。具体如下:
- @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
说明:produces属性作为匹配规则:表示Request请求的Accept头部需含有text/html。
用途:text/html 主要用于响应普通的页面请求,与AJAX请求作为区分。
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 27 28 29 30 31 32 33
| package com.hehe.error;
@Controller @RequestMapping("${server.error.path:/error}") public class GlobalErrorController implements ErrorController {
@Autowired private ErrorInfoBuilder errorInfoBuilder;
private final static String DEFAULT_ERROR_VIEW = "error";
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request) { return new ModelAndView(DEFAULT_ERROR_VIEW, "errorInfo", errorInfoBuilder.getErrorInfo(request)); }
@RequestMapping @ResponseBody public ErrorInfo error(HttpServletRequest request) { return errorInfoBuilder.getErrorInfo(request); }
@Override public String getErrorPath() { return errorInfoBuilder.getErrorProperties().getPath(); } }
|
注:是不是非常简单,相信这个工具类可以改变你对ErrorController复杂难用的看法。如果后续想拓展不同种类的错误/异常信息,只需修改ErrorInfoBuilder#getError方法即可,无需修改ErrorController的代码,十分方便。