SpringBoot的全局异常处理
前言
本篇文章主要介绍的是SpringBoot的全局异常处理。
GitHub源码链接位于文章底部。
首先还是来看工程的结构
在pom文件中添加相关依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<!--父级依赖,它用来提供相关的 Maven 默认依赖。使用它之后,常用的springboot包依赖可以省去version 标签-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath ></relativePath>
</parent>
<dependencies>
<!-- Spring Boot Web 依赖 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok插件依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
</dependencies>
编码
Spring Boot的全局异常处理有两个很重要的注解,一个是ControllerAdvice,一个是ExceptionHandler,我们在类上使用ControllerAdvice注解,表示开启了全局异常的捕获;在方法上使用ExceptionHandler注解去捕获具体某一个异常,然后再该方法体内对捕获到的异常进行处理,进行输出、返回等操作。我们来看看下面这个示例是怎么操作的:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value =NullPointerException.class)
public String exceptionHandler(NullPointerException e){
System.out.println("发生空指针异常:"+e);
return e.getMessage();
}
@ExceptionHandler(value = ArithmeticException.class)
public String exceptionHandler(ArithmeticException e) {
System.out.println("发生数学运算异常:"+e);
return e.getMessage();
}
}
上面这个类中就全局捕获了两种具体的异常,我们可以不断地往这个类中添加一些异常。在实际运用过程中,我们会对方法体中捕获的异常进行二次处理,封装成需要的格式,比如一些通用的错误码,返回消息,返回标识等等,这里通过自定义的异常类以及枚举类来实现。
自定义枚举类
首先自定义一个枚举类,该枚举类中枚举一些数据操作错误定义。如果有需要,后期可以不断地增加对应枚举情况。
public enum CommonEnum {
// 数据操作错误定义
SUCCESS(true,200, "请求成功!"),
ERROR(false,201, "请求失败!"),
BODY_NOT_MATCH(false,400,"请求的参数错误!"),
SIGNATURE_NOT_MATCH(false,401,"请求的数字签名不匹配!"),
NOT_FOUND(false,404, "未找到该资源!"),
INTERNAL_SERVER_ERROR(false,500, "服务器内部错误!"),
SERVER_BUSY(false,503,"服务器正忙,请稍后再试!");
/** 错误码 */
private Integer resultCode;
/** 错误描述 */
private String resultMsg;
private Boolean flag;
CommonEnum(Boolean flag, Integer resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
this.flag = flag;
}
/** 返回结果集中用到get方法*/
public Boolean getFlag() {
return flag;
}
public Integer getResultCode() {
return resultCode;
}
public String getResultMsg() {
return resultMsg;
}
}
自定义一个异常类,用于处理发生的业务异常
@Data
public class BizException extends RuntimeException {
private static final long serialVersionUID = 1L;
/** 错误码*/
protected Integer errorCode;
/** 错误信息*/
protected String errorMsg;
public BizException(Integer errorCode, String errorMsg) {
super(errorCode.toString());
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
@Override
public String getMessage() {
return errorMsg;
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
自定义数据格式
定义返回的结果集的格式,也就是改造Result类,和枚举类结合起来
@Data
public class Result {
/**
* 返回结构状态
*/
private Boolean flag;
/**
* 返回状态码
*/
private Integer code;
/**
* 返回消息
*/
private String message;
/**
* 返回数据内容
*/
private Object data;
public Result(CommonEnum commonEnum, Object data) {
this.flag = commonEnum.getFlag();
this.code = commonEnum.getResultCode();
this.message = commonEnum.getResultMsg();
this.data = data;
}
public Result(CommonEnum commonEnum) {
this.flag = commonEnum.getFlag();
this.code = commonEnum.getResultCode();
this.message = commonEnum.getResultMsg();
}
/**
* 自定义异常
*/
public Result(Integer code, String message) {
this.flag = false;
this.code = code;
this.message = message;
}
}
自定义全局异常处理类
最后来自定义全局异常处理类,可以处理自己抛出的自定义的业务异常,也可以处理系统异常,如空指针等,或者一些其他异常。其实就是第一个例子的延伸。
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理自定义的业务异常
* @param e
*/
@ExceptionHandler(value = BizException.class)
@ResponseBody
public Result bizExceptionHandler(BizException e) {
logger.error("发生业务异常!原因是:{}", e.getErrorMsg());
return new Result(e.getErrorCode(), e.getErrorMsg());
}
/**
* 处理空指针的异常
* @param e
*/
@ExceptionHandler(value = NullPointerException.class)
@ResponseBody
public Result exceptionHandler(NullPointerException e) {
logger.error("发生空指针异常!原因是:", e);
return new Result(CommonEnum.BODY_NOT_MATCH,"空指针异常");
}
/**
* 格式转换异常
* @param e
*/
@ExceptionHandler(value = ParseException.class)
@ResponseBody
public Result exceptionHandler(ParseException e) {
logger.error("发生格式转换异常!原因是:", e);
return new Result(CommonEnum.BODY_NOT_MATCH,"格式转换错误");
}
/**
* 方法安全权限异常
* @param e
*/
@ExceptionHandler(value = IllegalAccessException.class)
@ResponseBody
public Result exceptionHandler(IllegalAccessException e) {
logger.error("发生方法安全权限异常!原因是反射了private方法:", e);
return new Result(CommonEnum.INTERNAL_SERVER_ERROR,"方法安全权限异常!不能反射private方法");
}
/**
* 数学运算异常
* @param e
* @return
*/
@ExceptionHandler(value = ArithmeticException.class)
@ResponseBody
public Result exceptionHandler(ArithmeticException e) {
logger.error("发生数学运算异常:", e);
return new Result(CommonEnum.INTERNAL_SERVER_ERROR,"数学运算异常");
}
/**
* 处理其他异常
* @param e
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result exceptionHandler(Exception e) {
logger.error("未知异常!原因是:", e);
return new Result(CommonEnum.INTERNAL_SERVER_ERROR,未知异常!);
}
}
logger.error是输出语句,为了在控制台看到结果,等同于System.out,等同于e.printStackTrace()。
可以看到,这里返回的是之前自定义的结果集,该结果集里定义了多个构造方法,比如new Result(枚举,Obj),new Result(枚举)以及其他,如果需要,可再自行增加。此外,其他遇到的异常也可以在这里按照格式增加。
创建User实体类
因为这里不连接数据库,所以只定义简单的属性,pom文件中也没有添加数据库依赖。
@Data
public class User implements Serializable {
/** id */
private String id;
/** 姓名 */
private String name;
/** 年龄 */
private Integer age;
}
UserController控制层
控制层也使用不同的请求方式,不一样的是,这里请求方式和具体逻辑没有关系,只是为了方便操作api,同时在各个api中故意制造了一些异常,进行测试用,可以参考代码注释。
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
public Result insert(@RequestBody User user) {
if (user.getName() == null) {
//抛出一个自定义异常
throw new BizException(-1, "用户名不能为空!!!");
}
//如果没有异常,则返回数据为上传的user
return new Result(CommonEnum.SUCCESS,user);
}
@DeleteMapping
public Result delete() {
//制造一个空指针异常,并且不进行处理,会被全局捕获
String str=null;
str.equals("test");
//如果没有异常,则返回请求成功
return new Result(CommonEnum.SUCCESS);
}
@PutMapping
public Result update() {
//这里故意造成数字异常,并且不进行处理
int i = 1 / 0;
//如果没有异常,则返回请求成功,返回数据为i
return new Result(CommonEnum.SUCCESS,i);
}
@GetMapping
public Result find() {
/**
* 这里故意造成索引越界异常,并且不进行处理,
* 因为没有进行处理,全局异常中也没有进行具体异常的捕获
* 所以被最后的Exception捕获了
*/
List<String> list = new ArrayList<>();
String str = list.get(0);
//如果没有异常,则返回请求成功,返回数据为str
return new Result(CommonEnum.SUCCESS,str);
}
}
程序入口
@SpringBootApplication
public class ExceptionHandlerApplication {
public static void main(String[] args) {
SpringApplication.run(ExceptionHandlerApplication.class, args);
}
}
配置文件
在application.yml中设置访问端口
server:
port: 8080
测试
在postman中调用接口测试
捕获自定义的业务异常(post方法)
这里没有传入name参数,所以捕获了自己抛出的自定义异常
在controller中的post方法中throw new BizException(-1, "用户名不能为空!!!");
,可以看到输出了我们封装的返回结果。这里的错误码和错误信息可以自己修改。
在传入name参数后,没有产生异常,自然也无从捕获,此时返回的结果集正常为传入的user对象,id和age没有传入,因此为null。
捕获系统异常(delete、put方法)
因为没有传入参数,这里的body就不截出来了。
可以看到,这里的flag,code,message,data的值其实都是全局异常类中封装的结果集,前三项是枚举类中的项。如delete方法出现空指针异常,返回的是new Result(CommonEnum.BODY_NOT_MATCH,"空指针异常");这里的CommonEnum.BODY_NOT_MATCH定义了flag,code,message的值,data就是"空指针异常"。
可以在枚举类中增加NullPointer(false,203,"空指针异常"),然后在全局异常中返回new Result(CommonEnum.NullPointer,"空指针异常"),返回的结果集就是
{
"flag": false,
"code": 203,
"message": "空指针异常",
"data": "空指针异常"
}
put方法同理。
捕获未在全局异常类中定义的其他异常(get方法)
这里是捕获了索引越界异常,但是因为全局异常类中没有捕获该具体异常,因此被最后一个Exception异常捕获到了。
因为这里使用了logger.error进行输出,因此可以在控制台中看到该具体异常,将其按照上面的格式加入到全局异常处理类中,下次再遇到这种异常就能顺利捕获了。
本文GitHub源码:https://github.com/lixianguo5097/springboot/tree/master/springboot-exceptionHandler
CSDN:https://blog.csdn.net/qq_27682773
简书:https://www.jianshu.com/u/e99381e6886e
博客园:https://www.cnblogs.com/lixianguo