在日常 web 开发中发生了异常,往往需要通过一个统一的 异常处理,来保证客户端能够收到友好的提示。本文将会介绍 Spring Boot 中的全局统一异常处理。
要点:
- 介绍Spring Boot默认的异常处理机制
- 如何自定义错误页面
- 通过@ControllerAdvice注解来处理异常
从 spring 3.2 开始,新增了 @ControllerAdvice 注解,可以用于定义@ExceptionHandler,并应用到配置了@RequestMapping 的控制器中。
默认情况下,Spring Boot为两种情况提供了不同的响应方式
- 一种是浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求头中Accept: text/html,所以Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”。
如果这接口是给第三方调用那是不行的,至此大致能了解到为啥需要对异常进行全局捕获了。
- 另一种是使用Postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,Spring Boot会返回类似如下的Json格式字符串信息
{
"timestamp": "2018-05-12T06:11:45.209+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/index.html"
}
原理也很简单,Spring Boot 默认提供了程序出错的结果映射路径/error。这个/error请求会在BasicErrorController中处理,其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自客户端浏览器(浏览器通常默认自动发送请求头内容Accept:text/html)还是客户端接口的调用,以此来决定返回页面视图还是 JSON 消息内容。
自定义错误页面
浏览器端访问的话,任何错误Spring Boot返回的都是一个Whitelabel Error Page的错误页面,这个很不友好,所以我们可以自定义下错误页面。
- 先从最简单的开始,直接在/resources/templates下面创建error.html就可以覆盖默认的Whitelabel Error Page的错误页面,我项目用的是thymeleaf模板,对应的error.html代码如下:
Title
动态error错误页面
这样运行的时候,请求一个不存在的页面或服务端处理发生异常时,展示的自定义错误界面。
通过@ControllerAdvice注解来处理异常
Spring Boot提供的ErrorController是一种全局性的容错机制。此外,你还可以用@ControllerAdvice注解和@ExceptionHandler注解实现对指定异常的特殊处理。
这里介绍两种情况:
- 局部异常处理 @Controller + @ExceptionHandler
- 全局异常处理 @ControllerAdvice + @ExceptionHandler
局部异常处理 @Controller + @ExceptionHandler
局部异常主要用到的是@ExceptionHandler注解,此注解注解到类的方法上,当此注解里定义的异常抛出时,此方法会被执行。如果@ExceptionHandler所在的类是@Controller,则此方法只作用在此类。如果@ExceptionHandler所在的类带有@ControllerAdvice注解,则此方法会作用在全局。
全局异常处理 @ControllerAdvice + @ExceptionHandler
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。
简单的说,进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。
我们实际开发中,如果是要实现RESTful API,那么默认的JSON错误信息就不是我们想要的,这时候就需要统一一下JSON格式,所以需要封装一下。
package com.pay.common.message;
import lombok.Data;
import java.io.Serializable;
/**
* 统一返回对象
*/
@Data
public class JsonResult implements Serializable {
/**
*
*/
private static final long serialVersionUID = 17721020985L;
/**
* 通信数据
*/
private T data;
/**
* 通信状态
*/
private boolean flag = true;
/**
* 状态码,0000代表无错误,错误代码应该用枚举定义。
*/
private String code;
/**
* 通信描述
*/
private String msg = "";
/**
* 通过静态方法获取实例
*/
public static JsonResult of(T data) {
return new JsonResult<>(data);
}
public static JsonResult of(T data, boolean flag) {
return new JsonResult<>(data, flag);
}
public static JsonResult of(T data, boolean flag, String msg) {
return new JsonResult<>(data, flag, msg);
}
public static JsonResult of(T data, boolean flag, String code, String msg) {
return new JsonResult<>(data, flag, code, msg);
}
@Deprecated
public JsonResult() {
}
private JsonResult(T data) {
this.data = data;
}
private JsonResult(T data, boolean flag) {
this.data = data;
this.flag = flag;
}
private JsonResult(T data, boolean flag, String msg) {
this.data = data;
this.flag = flag;
this.msg = msg;
}
private JsonResult(T data, boolean flag, String code, String msg) {
this.data = data;
this.flag = flag;
this.code = code;
this.msg = msg;
}
}
创建全局异常处理类
package com.pay.common.exception;
import com.pay.common.message.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.persistence.EntityNotFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
/**
* @ClassName: GlobalExceptionHandler
* @Description: 全局异常处理器
* @author: 郭秀志 [email protected]
* @date: 2020/4/25 15:35
* @Copyright:
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 用来处理bean validation异常
*
* @param ex
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public JsonResult resolveConstraintViolationException(ConstraintViolationException ex) {
Set> constraintViolations = ex.getConstraintViolations();
if (!CollectionUtils.isEmpty(constraintViolations)) {
StringBuilder msgBuilder = new StringBuilder();
for (ConstraintViolation constraintViolation : constraintViolations) {
msgBuilder.append(constraintViolation.getMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if (errorMessage.length() > 1) {
errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
}
return JsonResult.of(errorMessage);
}
return JsonResult.of(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public JsonResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
List objectErrors = ex.getBindingResult().getAllErrors();
if (!CollectionUtils.isEmpty(objectErrors)) {
StringBuilder msgBuilder = new StringBuilder();
for (ObjectError objectError : objectErrors) {
msgBuilder.append(objectError.getDefaultMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if (errorMessage.length() > 1) {
errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
}
return JsonResult.of(errorMessage, false, "MethodArgumentNotValid");
}
return JsonResult.of(ex.getMessage(), false, "MethodArgumentNotValid");
}
@ExceptionHandler(value = EntityNotFoundException.class)
@ResponseBody
public JsonResult resolveEntityNotFoundException(EntityNotFoundException ex) {
return JsonResult.of(ex.getMessage(), false, "未查到数据");
}
}
此类在common的项目,要暴露出去给依赖的项目使用,在文件src\main\resources\META-INF\spring.factories中添加最后一行
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pay.common.autoconfig.schedlock.ShedlockConfig,\
com.pay.common.service.MailService,\
com.pay.common.exception.GlobalExceptionHandler
controller进行抛出异常
可以被全局异常捕捉并处理成json
@ApiVersion(5)
@RequestMapping(value = "/findByOne") // 加入接口的版本控制http://localhost:8080/v5/packageIndex/findByOne
public JsonResult findByOne() {
BzPackageIndex bzPackageIndex = new BzPackageIndex();
bzPackageIndex.setPackageId("BZ-20200107000001");
bzPackageIndex.setSort(5);
Example example = Example.of(bzPackageIndex);
Optional one = packageIndexService.findOne(example);
return JsonResult.of(one.orElseThrow(() -> new EntityNotFoundException("package id为:BZ-20200107000005 的indexpackage无记录")), true, "0000", "获取数据成功");
// return JsonResult.of("v5接口", true, "成功调用");
}
访问接口,如果无数据,则输出异常信息
{"data":"package id为:BZ-20200107000005 的indexpackage无记录","flag":false,"code":null,"msg":"未查到数据"}
全局异常类可以用@RestControllerAdvice
,替代@ControllerAdvice
,因为这里返回的主要是json格式,这样可以少写一个@ResponseBody
。