SpringBoot的全局异常处理

风格不统一 提交于 2020-03-18 18:30:55

前言

本篇文章主要介绍的是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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!