Spring Boot 全局异常处理
说到异常处理,我们都知道使用 try-catch
可以捕捉异常,可以 throws
抛出异常。那么在 Spring Boot 中我们如何处理异常,如何是的处理更加优雅,如何全局处理异常。是本章讨论解决的问题。
首先让我们简单了解或重新学习下 Java 的异常机制。
1 Java 异常机制概述
Spring Boot 的所有异常处理都基于 java 的。
1.1 Java 异常类图
- Java 内部的异常类
Throwable
包括了Exception
和Error
两大类,所有的异常类都是Object
对象。 Error
是不可捕捉的异常,通俗的理解就是由于 java 内部 jvm 引起的不可预见的异常,比如 java 虚拟机运行错误,当内存资源错误,将会出现OutOfMemoryError
。此时 java 虚拟机会选择终止线程。Excetpion
异常是程序本身引起的,它又分为运行时异常RuntimeException
,和非运行时(编译时)IOException
等异常。- 运行时异常
RuntimeException
例如:除数为零,将引发ArrayIndexOutOfBoundException
异常。 - 非运行异常都是可查可捕捉的。Java 编译器会告诉程序他错了,错在哪里,正确的建议什么。我们可以通过 throws 配合
try-catch
来处理。
1.2 Exception 运行时异常和编译异常
- 运行时异常 即
RuntimeException
类型下的异常 - 编译异常 即
Exception
类型下除了RuntimeException
类型的异常,例如IOException
1.3 可查异常与不可查异常
- 可查异常 即
Exception
类型下除了RuntimeException
类型的异常,都是可查的,具有可查性。 - 不可查异常 错误类
Error
和RuntimeException
类型的异常都是不可查的,具有不可查性。
2 Java 异常处理机制
2.1 异常处理机制的分类
在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
-
抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
-
捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
针对不同的异常类型,Java 对处理的要求不一样
- Error 错误,由于不可捕捉,不可查询,Java 允许不做任何处理。
- 对于运行时异常 RuntimeException 不可查询异常,Java 允许程序忽略运行时异常,Java 系统会自动记录并处理。
- 对于所有可查异常都可捕捉,Java 自己。
2.2 捕获异常 try、catch、finally
注意 finally 不论程序如何执行都会执行到
try{
//可能出现异常的业务代码
}catch(Exception1 e1){
//异常处理1
}catch(Exception2 e2){
//异常处理2
}catch(Exceptionn en){
//异常处理n...
}
finally{
//无论是否是否异常都会执行的地方
}
2.2.1 try、catch 流程规则
try
、catch
语句,try只有一个,catch
可以有多个,也就是当有多个异常的时候,不需要编写多个 try-catch
模块,只要写一个 try
多个 catch
就可以。
try{
//可能出现异常的业务代码
}catch(Exception1 e1){
//异常处理1
}catch(Exception2 e2){
//异常处理2
}catch(Exceptionn en){
//异常处理n...
}
2.2.2 try、catch 、finally
try
、catch
、finally
语句中,finally
并不是必须的,但在有的场景确是非常实用的。
try{
//可能出现异常的业务代码
}catch(Exception1 e1){
//异常处理1
}catch(Exception2 e2){
//异常处理2
}catch(Exceptionn en){
//异常处理n...
}
finally{
//无论是否是否异常都会执行的地方
}
2.2.3 try、catch、finally 执行顺序
执行顺序通常分两种,有异常发生执行程序、无异常执行程序。
例如当我们有示例
try{
语句1;
语句2
语句n;
}catch(Exception1 e1){
异常处理;
}
finally{
finally语句;
}
正常语句;
- 有异常发生,假设语句1发生了异常,那么程序执行顺序 语句1、异常狐狸、finally语句、正常语句。
- 如果没有异常发生,那么程序执行顺序 语句1、语句2、语句n、finally语句、正常语句。
3 Spring Boot 中的异常处理示例
在 Spring Boot 应用程序中,通常统一处理异常的方法有
使用注解处理 @ControllerAdvice
本示例主要目的处理我们日常 Spring Boot 中的异常处理
- 在 Web 项目中通过
@ControllerAdvice
@RestControllerAdvice
实现全局异常处理
@ControllerAdvice
和@RestControllerAdvice
的区别 相当于Controller
和RestController
的区别。 - 在 Web 项目中实现 404、500 等状态的页面单独渲染
- 在 Spring Boot 项目中使用 Aop 切面编程实现全局异常处理
3.1 创建时示例 Spring Boot 项目
1)File > New > Project,如下图选择 Spring Initializr
然后点击 【Next】下一步
2)填写 GroupId
(包名)、Artifact
(项目名) 即可。点击 下一步
groupId=com.fishpro
artifactId=thymeleaf
3)选择依赖 Spring Web Starter
前面打钩。
4)项目名设置为 spring-boot-study-throwable
至此项目已经建好了,访问 http://localhost:8084/ (注意,配置文件已经修改为了 server.port=8084),会直接抛出异常如下图:
如图所示,表明 Spring Boot 具有默认的 出错处理机制,指向了 /error
目录。
3.2 引入依赖编辑Pom.xml
本章节用到 web 和 thymeleaf 两个依赖,注意只有引入了 thymeleaf
后在 templates 目录下增加 error.html
系统才能自动与 /error
路由匹配 否则会出现 Whitelabel Error Page
页面
<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>
3.3 创建基于 @RestControllerAdvice 全局异常类示例
@RestControllerAdvice
注解是 Spring Boot 用于捕获 @Controller
和 @RestController
层系统抛出的异常(注意,如果已经编写了 try-catch
且在 catch 模块中没有使用 throw 抛出异常, 则 @RestControllerAdvice
捕获不到异常)。
@ExceptionHandler
注解用于指定方法处理的 Exception 的类型
如上图所示,控制层 IndexRestController
编写了4个 api 方法,
- /index 是正常的方法;
- /err 是人为抛出异常;
- /matcherr 除数为0的异常 ;
- /nocatch 用了 try-catch 但没有抛出异常,不会被捕捉。
- 四个 api 其中 /err、/matcherr 会被
MyRestExceptionController
捕捉。
本示例需要新增的文件为 2 个,分别为:
- controller 下的
IndexRestController.java
- exception 下的
MyRestExceptionHandler.java
具体代码清单如下:
3.3.1 创建用于测试的 RestController 接口类 IndexRestController
@RestController
@RequestMapping("/api")
public class IndexRestController {
@RequestMapping("/index")
public Map index(){
Map<String,Object> map=new HashMap<>();
map.put("status","0");
map.put("msg","正常的输出");
return map;
}
/**
* 这里人为手动抛出一个异常
* */
@RequestMapping("/err")
public Map err(){
throw new RuntimeException("抛出一个异常");
}
/**
* 这里抛出的是 RuntimeException 不可查异常,虽然没有使用 try-catch 来捕捉 但系统以及帮助我们抛出了一次
* */
@RequestMapping("/matcherr")
public Map matcherr(){
Map<String,Object> map=new HashMap<>();
map.put("status","0");
map.put("msg","正常的输出");
int j=0;
Integer i=9/j;
return map;
}
/**
* 这里抛出的是 RuntimeException 不可查异 注意这里使用了 try-catch 来捕捉异常,但没有抛出异常
* */
@RequestMapping("/nocatch")
public Map nocatch(){
Map<String,Object> map=new HashMap<>();
map.put("status","0");
map.put("msg","正常的输出 注意这里使用了 try-catch 来捕捉异常,但没有抛出异常,所以没有异常,因为这里抛出的是 RuntimeException 不可查异常,系统也不会报错。");
int j=0;
try{
Integer i=9/j;
}catch (Exception ex){
}
return map;
}
}
3.3.2 创建自定义的全局异常处理类 MyRestExceptionController
/**
* 基于@ControllerAdvice注解的全局异常统一处理只能针对于Controller层的异常
* 为了和Controller 区分 ,我们可以指定 annotations = RestController.class,那么在Controller中抛出的异常 这里就不会被捕捉
* */
@RestControllerAdvice(annotations = RestController.class)
public class MyRestExceptionController {
private static final Logger logger= LoggerFactory.getLogger(MyRestExceptionController.class);
/**
* 处理所有的Controller层面的异常
* */
@ExceptionHandler(Exception.class)
@ResponseBody
public final Map handleAllExceptions(Exception ex, WebRequest request){
logger.error(ex.getMessage());
Map<String,Object> map=new HashMap<>();
map.put("status",-1);
map.put("msg",ex.getLocalizedMessage());
return map;
}
}
3.3.3 使用 Postman进行测试
http://localhost:8084/api/index
{
"msg": "正常的输出",
"status": "0"
}
{
"msg": "抛出一个异常",
"status": -1
}
http://localhost:8084/api/matcherr
{
"msg": "/ by zero",
"status": -1
}
http://localhost:8084/api/nocatch
{
"msg": "正常的输出 注意这里使用了 try-catch 来捕捉异常,但没有抛出异常",
"status": "0"
}
http://localhost:8084/api/noname 如果路由不存在,那么系统就会走404路由程序,如何统一格式,则不再此详细阐述。
{
"timestamp": "2019-07-12T14:53:26.513+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/noname"
}
3.4 创建基于 @ControllerAdvice 全局异常类示例
@ControllerAdvice 和 @RestControllerAdvice 其实就是 @Controller 和 @RestController 的区别。直观上就是会不会返回到前台界面的区别。
其实,无论是 @ControllerAdvice 还是 @RestControllerAdvice 都是可以捕捉 @Controller 和 @RestController 抛出的异常。
不同的是,@Controller 异常,我们往往需要更加友好的界面。下面我们使用了 thymeleaf 模板来重新定义 /error 默认路由。
如上图所示,控制层 IndexController
编写了2个方法,
- error.html 在 rerouces/templates/ 目录下,必须引入 thymeleaf 组件
- /index 是正常的方法;
- /index/err 是人为抛出异常,会被
MyExceptionController
捕捉; - /index/matcherr 除数为0的异常 会被
MyExceptionController
捕捉;
3.4.1 创建用于测试的 @Controller 文件IndexController
@Controller
public class IndexController {
/**
* 正常的页面 对应 /templates/index.html 页面
* */
@RequestMapping("/index")
public String index(Model model){
model.addAttribute("msg","这是一个index页面的正常消息");
return "index";
}
/**
* 抛出一个 RuntimeException 异常
* */
@RequestMapping("/index/err")
public String err(){
throw new RuntimeException("抛出一个 RuntimeException 异常");
}
/**
* 抛出一个 RuntimeException 异常
* */
@RequestMapping("/index/matherr")
public String matherr(Model model){
int j=0;
int i=0;
i=100/j;
return "index";
}
}
3.4.2 创建用于捕捉 @Controller 异常的全局文件MyExceptionController
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice(annotations = Controller.class)
public class MyExceptionController {
private static final Logger logger= LoggerFactory.getLogger(MyExceptionController.class);
public static final String DEFAULT_ERROR_VIEW = "error";
/**
* 处理所有的Controller层面的异常
* 如果这里添加 @ResponseBody 注解 表示抛出的异常以 Rest 的方式返回,这时就系统就不会指向到错误页面 /error
* */
@ExceptionHandler(Exception.class)
public final ModelAndView handleAllExceptions(Exception ex, HttpServletRequest request){
logger.error(ex.getMessage());
ModelAndView modelAndView = new ModelAndView();
//将异常信息设置如modelAndView
modelAndView.addObject("msg", ex);
modelAndView.addObject("url", request.getRequestURL());
modelAndView.setViewName(DEFAULT_ERROR_VIEW);
//返回ModelAndView
return modelAndView;
}
}
3.4.3 创建/error对应的出错页面 error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>异常统一处理页面</title>
</head>
<body>
this is error.html
<p th:text="${msg}"></p>
</body>
</html>
3.4.4 创建控制层对应的前端文件 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IndexController-index</title>
</head>
<body>
<p th:text="${msg}"></p>
</body>
</html>
3.4.5 使用浏览器测试
这是一个index页面的正常消息
http://localhost:8084/index/err
this is error.html
java.lang.RuntimeException: 抛出一个 RuntimeException 异常
http://localhost:8084/index/matherr
this is error.html
java.lang.ArithmeticException: / by zero
4 Spring Boot 自定义错误页面
在第3章节,我们知道可以通过 建立全局异常处理类来实现 基于 @Controller
的异常统一处理。我们也可以把统一异常展示到自定义错误页面。
在 Spring Boot 中使用了 ErrorController
来处理出错请求。在 Java 8 上又提供了 BasicErrorController
他继承与 AbstractErrorController
,AbstractErrorController
又继承于 ErrorController
。
4.1 基于 ErrorController 实现自定义错误页面
在本章节中 需要新增3个页面,自定义处理类、404、500、error 等页面。其原理是根据 HttpServletResponse
的返回状态 response.getStatus()
来判断如果是 404 就跳转到对应 404 路由。
- 增加controller 下的 CustomerErrorController 页面
- 增加 templates/error/404.html
- 增加 templates/error/500.html
- 增加 templates/error/error.html
CustomerErrorController 主要代码
public class CustomErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
@RequestMapping(
value = {ERROR_PATH},
produces = {"text/html"}
)
/**
* 用户 Controller 带返回的
* */
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
int code = response.getStatus();
if (404 == code) {
return new ModelAndView("error/404");
} else if (403 == code) {
return new ModelAndView("error/403");
} else {
return new ModelAndView("error/500");
}
}
@RequestMapping(value = ERROR_PATH)
public Map handleError(HttpServletRequest request, HttpServletResponse response) {
Map<String,Object> map=new HashMap<>();
int code = response.getStatus();
if (404 == code) {
map.put("status",404);
map.put("msg","未找到资源文件");
} else if (403 == code) {
map.put("status",403);
map.put("msg","没有访问权限");
} else if (401 == code) {
map.put("status",401);
map.put("msg","登录过期");
} else {
map.put("status",500);
map.put("msg","服务器错误");
}
return map;
}
@Override
public String getErrorPath(){return ERROR_PATH;}
}
测试效果 浏览器输入任意不存在的网站 http://localhost:8084/23/23 查看输出
404页面
4.2 实现自定义错误页面整合到全局异常处理类中
实际上这里是可以跟上面的全局异常处理合起来的,在我们定义的 MyExceptionController
。
我们需要在 MyExceptionController
类中增加判定即可。
注意因为 404 异常并不是我们的异常捕捉类可以捕捉的,所以 404 页面不在其中。
结束语
这篇文章前前后后,写了两天,找的参考资料很多都是不全,要么没有交代 默认的/error 问题,要么就是没有说明 @Controller @RestController 问题。总之我总结来有几个问题需要解决:
- 如何解决默认的 /error 路由映射问题,在有 thymeleaf 与没有的情况有什么区别
- 如何解决 404、505不同状态不同映射问题
- @Controller @RestController是否都能拦截,有人说只能拦截 @Controller 这是不正确的, @RestController 本来就是 @Controller 演变而来,同样是可以拦截的。
- @Controller 如何友好的返回
- @RestController 如何给远程调用方返回错误信息
问题:
- 没有捕捉的异常
这种情况一般是使用 try-catch 但没有 throw 出异常导致的
关联阅读: