1 基本思路
1.1 使用到的异常种类
Java的错误异常分为两个体系:Error、Exception,均继承自Throwable->Object
Error错误类体系:Java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等严重情况
Exception异常类体系:其它因编程错误或偶然的外在因素导致的一般性问题。又分为编译时异常(checked)和运行时异常(unchecked)。
1.2 使用到的技术点
- 类:使用@ControllerAdvice+@ExceptionHandler处理全局抛出的异常
- 方法:上面定义的全局异常处理类,方法上标注不同的注解。可实现不同的异常进入到不同的处理模块。
- 方法返回值:根据不同的情况判断是返回的页面还是Json对象。
2 具体实现
2.1 通览:自定义全局异常类
package com.efly.gulimall.coupon.helper.exception;
import com.efly.gulimall.coupon.helper.base.AjaxResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* 面向切面的异常:自定义异常处理器
*
* @author zhuyf
*/
@RestControllerAdvice
@Slf4j
public class DefaultExceptionHandler {
/**
* 自定义的业务异常(可在代码处手动抛出throw new BusinessPageException)
*
* @param request 请求对象,可不传
* @param response 响应对象,可不传
* @param ex 异常类(这个要和你当前捕获的异常类是同一个)
*/
@ExceptionHandler(BusinessPageException.class) //也可以只对一个类进行捕获
public Object errorHandler(BusinessPageException ex, HttpServletRequest request, HttpServletResponse response) {
/*
* 这里你拿到了request和response对象,你可以做任何你想做的事
* 比如:
* 1.用request从头信息中拿到Accept来判断是请求方可接收的类型从而进行第一个方法的判断
* 2.如果你也想返回一个页面,使用response对象进行重定向到自己的错误页面就可以了
*/
//异常写入数据库日志框架
log.error(ex.getMessage(), ex);
//异常协商:如果接受类型为html,就返回错误页面,否则返回json文本
if (request.getHeader("accept").indexOf("text/html") != -1) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("/error/5xx.html"); // 指定错误跳转页面 需要在templates里面新建 一个error.html
modelAndView.addObject("messages", ex.getMessage());
modelAndView.addObject("code", ex.getCode());
modelAndView.addObject("url", request.getRequestURL());
return modelAndView;
}
return AjaxResult.error("服务器错误,请联系管理员" + ex.getMessage());
}
/**
* Throwable->Exception->HttpRequestMethodNotSupportedException
* 捕获请求方式不支持的异常
*/
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
public AjaxResult handleException(HttpRequestMethodNotSupportedException e) {
log.error(e.getMessage(), e);
return AjaxResult.error("不支持' " + e.getMethod() + "'请求");
}
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
public AjaxResult handleVaildException(BindException e) {
log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError) -> {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return AjaxResult.error("数据校验出现问题", errorMap);
}
/**
* Throwable->Exception->RuntimeException
* 捕获未知的运行时RuntimeException异常
*/
@ExceptionHandler(RuntimeException.class)
public AjaxResult notFount(RuntimeException e) {
//内容可决定是返回AjaxResult还是回到404
log.error("运行时异常:", e);
return AjaxResult.error("运行时异常:" + e.getMessage());
}
/**
* Throwable->Exception
* 捕获Exception异常
*/
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e) {
log.error(e.getMessage(), e);
return AjaxResult.error("服务器错误,请联系管理员", e.getMessage());
}
/**
* Throwable:最大的异常捕获类
* 捕获异常体系最大的Throwable异常
*/
@ExceptionHandler(value = Throwable.class)
public AjaxResult handleException(Throwable throwable) {
log.error("错误:", throwable);
return AjaxResult.error("服务器错误,请联系管理员" + throwable.getMessage());
}
}
2.2 拆解:自定义异常类及捕获自定义异常类的处理方法
上面的BussinessPageException是自定义的异常类,该方法识别请求的accept判断是网页发起的请求还是json对象发起的,如果是网页发起的则到模板引擎下的/error/5xx.html,如果是非text/html则返回json对象。
/**
* 自定义业务异常
*
* @param request 请求对象,可不传
* @param response 响应对象,可不传
* @param ex 异常类(这个要和你当前捕获的异常类是同一个)
*/
@ExceptionHandler(BusinessPageException.class)
public Object errorHandler(BusinessPageException ex,
HttpServletRequest request,
HttpServletResponse response) {
/*
* 这里你拿到了request和response对象,你可以做任何你想做的事
* 比如:
* 1.用request从头信息中拿到Accept来判断是请求方可接收的类型从而进行第一个方法的判断
* 2.如果你也想返回一个页面,使用response对象进行重定向到自己的错误页面就可以了
*/
//异常写入数据库日志框架
log.error(ex.getMessage(), ex);
//异常协商:如果接受类型为html,就返回错误页面,否则返回json文本
if (request.getHeader("accept").indexOf("text/html") != -1) {
ModelAndView modelAndView = new ModelAndView();
// 指定错误跳转页面 需要在templates里面新建 一个error.html
modelAndView.setViewName("/error/5xx.html");
modelAndView.addObject("messages", ex.getMessage());
modelAndView.addObject("code", ex.getCode());
modelAndView.addObject("url", request.getRequestURL());
return modelAndView;
}
return AjaxResult.error("服务器错误,请联系管理员" + ex.getMessage());
}
自定义异常类
/**
* 自定义异常类
*
* @author zhuyf
*/
public class BusinessPageException extends RuntimeException {
private static final long serialVersionUID = 1L;
protected final String message;
protected final Integer code;
public BusinessPageException(String message, Integer code) {
this.message = message;
this.code = code;
}
@Override
public String getMessage() {
return message;
}
public Integer getCode() {
return code;
}
}
自定义返回的类型AjaxResult
package com.efly.gulimall.coupon.helper.base;
import java.util.HashMap;
/**
* 操作消息提醒
*
* @author zhuyf
*/
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/**
* 初始化一个新创建的 Message 对象
*/
public AjaxResult() {
}
/**
* 返回错误消息
*
* @return 错误消息
*/
public static AjaxResult error() {
return error(1, "操作失败", false, "");
}
/**
* 返回错误消息
*
* @param msg 内容
* @return 错误消息
*/
public static AjaxResult error(String msg) {
return error(500, msg, false, "");
}
public static AjaxResult error(String msg, Object data) {
return error(500, msg, false, data);
}
/**
* 返回错误消息
*
* @param code 错误码
* @param msg 内容
* @return 错误消息
*/
public static AjaxResult error(int code, String msg, boolean isSuccess, Object data) {
AjaxResult json = new AjaxResult();
json.put("code", code);
json.put("msg", msg);
json.put("success", isSuccess);
json.put("data", data);
return json;
}
/**
* 返回成功消息
*
* @param msg 内容
* @return 成功消息
*/
public static AjaxResult success(String msg) {
AjaxResult json = new AjaxResult();
json.put("msg", msg);
json.put("code", 200);
json.put("success", true);
json.put("data", "");
return json;
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
/**
* 返回成功消息
*
* @param key 键值
* @param value 内容
* @return 成功消息
*/
@Override
public AjaxResult put(String key, Object value) {
super.put(key, value);
return this;
}
}
2.3 拆解:thymeleaf引擎渲染的error/5xx.html页面
此处使用的是Thymeleaf。注意在实际生产环境里面,具体的错误信息不要显示在错误页面上,在测试环境下可以显示以提高排错效率。
pom加载thymeleaf启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
ThymeleafView默认路径为:classpath:/templates,加页面放入到templates/下
上面分发的代码
ModelAndView modelAndView = new ModelAndView();
// 指定错误跳转页面 需要在templates里面新建 一个error.html
modelAndView.setViewName("/error/5xx.html");
- 5xx.html的具体错误内容
```
<!DOCTYPE html>
错误信息:错误状态码:失败API地址:
<a name="xRKOU"></a>
## 2.4 拆解:SpringBoot默认的异常处理静态错误页面
除了像上面可以使用服务器端渲染的页面(可以动态捕获到具体的错误日志、错误代码、错误路径等信息),SpringBoot也提供了默认的异常信息静态页面的处理功能。<br />对于机器客户端,它将生成并返回JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/696107/1626495013380-4069a248-c45f-46b5-82b4-205b1b23fa07.png#clientId=u5b5b6f01-0bb3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=142&id=ufed797bf&margin=%5Bobject%20Object%5D&name=image.png&originHeight=142&originWidth=356&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19568&status=done&style=none&taskId=u6d23838d-97c9-4843-9268-2b001074dff&title=&width=356)<br />对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/696107/1626495005821-808e486d-2c7f-4072-a110-8713866d2082.png#clientId=u5b5b6f01-0bb3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=126&id=ue95b3250&margin=%5Bobject%20Object%5D&name=image.png&originHeight=126&originWidth=482&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33754&status=done&style=none&taskId=u7992a54c-919d-474f-a830-b6c892af16e&title=&width=482)<br />**具体的规则即为**:SpringMVC在执行过程中会捕获异常,然后去找异常解析器去解析异常,如果可以解析就拿到解析器返回的ModelAndView去指定的视图/JSON数据(就是我们上面定义的Thymeleaf引擎下的error/5xx、4xx.html),如果不能就由底层tomcat发送一个error错误,这个error错误会去SpringBoot默认的静态目录下的error/404或error/500页面,404或500是错误代码。
```java
扩展:springboot将从服务器此目录下读取静态资源数据,默认值为
classpath:/META-INF/resources/
classpath:/resources/,
classpath:/static/,
classpath:/public/
其中classpath为resources目录
所以我们可以将默认的5xx.html和4xx.html写入到如上默认规则目录里面
具体的页面内容
error/5xx.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CGWANG - 500</title>
<link href="/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/css/animate.css" rel="stylesheet"/>
<link href="/css/style.css" rel="stylesheet"/>
</head>
<body class="gray-bg">
<div class="middle-box text-center animated fadeInDown">
<h1>500</h1>
<h3 class="font-bold">内部服务器错误!</h3>
<div class="error-desc">
服务器遇到意外事件,不允许完成请求。我们抱歉。您可以返回主页面。
<a href="javascript:top.document.location.href='/'" class="btn btn-primary m-t">主页</a>
</div>
</div>
<script src="/js/jquery.min.js?v=2.1.4"></script>
<script src="/js/bootstrap.min.js?v=3.3.6"></script>
</body>
</html>
error/4xx.html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body>
404
</body>
</html>
2.6 测试
有如下的代码
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
//手动抛出异常
throw new BusinessPageException("进入了错误页面", 500);
}
在网页端访问
如果使用PostMan由于没有像网页端一样传递了Accept:text/html,则返回的是Jason对象