5. 页面代码(Thymeleaf)
代码完成之后,我们需要编写一个异常信息页面。为了方便演示,我们在resources目录下创建templates目录,并新建文件exception.html。页面代码如下:
<!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 引入相关依赖:
<?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> <!--添加Web依赖 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <!--添加Thymeleaf依赖 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency><!--添加Test依赖 --> <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问题是不可取的,属于越俎代庖的做法。
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请求作为区分。
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";//错误信息页 /** * 情况1:若预期返回类型为text/html,则返回错误信息页(View). */ @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request) { return new ModelAndView(DEFAULT_ERROR_VIEW, "errorInfo", errorInfoBuilder.getErrorInfo(request)); } /** * 情况2:其它预期类型 则返回详细的错误信息(JSON). */ @RequestMapping @ResponseBody public ErrorInfo error(HttpServletRequest request) { return errorInfoBuilder.getErrorInfo(request); } @Override public String getErrorPath() {//获取映射路径 return errorInfoBuilder.getErrorProperties().getPath(); } }
注:是不是非常简单,相信这个工具类可以改变你对ErrorController复杂难用的看法。如果后续想拓展不同种类的错误/异常信息,只需修改ErrorInfoBuilder#getError方法即可,无需修改ErrorController的代码,十分方便。
五. 源码和文档
源码下载:
- https://github.com/yizhiwazi/springboot-socks/tree/master/springboot-error-controller
- https://github.com/yizhiwazi/springboot-socks/tree/master/springboot-error-handler