前言
本篇文章主要介绍的是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
个人博客:https://www.lxgblog.com
来源:https://www.cnblogs.com/lixianguo/p/12518984.html