第012课:Spring Boot 全局异常处理

在项目开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。如果对每个过程都单独作异常处理,那系统的代码耦合度会变得很高,此外,开发工作量也会加大而且不好统一,这也增加了代码的维护成本。

针对这种实际情况,我们需要将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能单一,也实现了异常信息的统一处理和维护。同时,我们也不希望直接把异常抛给用户,应该对异常进行处理,对错误信息进行封装,然后返回一个友好的信息给用户。

本节课程主要总结一下项目中使用 Spring Boot 如何拦截并处理全局的异常。

1. 定义返回的统一 json 结构

前端或者其他服务请求本服务的接口时,该接口需要返回对应的 json 数据,一般该服务只需要返回请求着需要的参数即可,但是在实际项目中,我们需要封装更多的信息,比如状态码 code、相关信息 msg 等等,这一方面是在项目中可以有个统一的返回结构,整个项目组都适用,另一方面是方便结合全局异常处理信息,因为异常处理信息中一般我们需要把状态码和异常内容反馈给调用方。
这个统一的 json 结构可以参考第008课:Spring Boot 返回JSON数据格式封装中封装的统一 json 结构,本节内容我们简化一下,只保留状态码 code 和异常信息 msg即可。如下:

/**
 * <h3>springboot-study</h3>
 * <p>统一返回的异常封装实体</p>
 * @author : zhang.bw
 * @date : 2020-07-17 21:37
 **/
public class JsonResult {

    /**
     * 异常码
     */
    protected String code;

    /**
     * 异常信息
     */
    protected String msg;

    public JsonResult() {
        this.code = "200";
        this.msg = "操作成功";
    }

    public JsonResult(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public JsonResult(BusinessErrorException ex) {
        this.code = ex.getCode();
        this.msg = ex.getMessage();
    }

    public JsonResult(BusinessMsgEnum msg) {
        this.code = msg.code();
        this.msg = msg.msg();
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

}

2. 处理系统异常

新建一个 GlobalExceptionHandler 全局异常处理类,然后加上 @ControllerAdvice 注解即可拦截项目中抛出的异常,如下:

/**
 * <h3>springboot-study</h3>
 * <p>增强Controller,全局异常处理</p>
 * @author : zhang.bw
 * @date : 2020-07-17 21:37
 **/
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    // ……
}

我们点开 @ControllerAdvice 注解可以看到,@ControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。除此之外,该注解还有个 basePackages 属性,该属性是用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。@ResponseBody 注解是为了异常处理完之后给调用方输出一个 json 格式的封装数据。
在项目中如何使用呢?Spring Boot 中很简单,在方法上通过 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,最后将结果通过统一的 json 结构体返回给调用者。下面我们举几个例子来说明如何来使用。

2.1 处理参数缺失异常

在前后端分离的架构中,前端请求后台的接口都是通过 rest 风格来调用,有时候,比如 POST 请求 需要携带一些参数,但是往往有时候参数会漏掉。另外,在微服务架构中,涉及到多个微服务之间的接口调用时,也可能出现这种情况,此时我们需要定义一个处理参数缺失异常的方法,来给前端或者调用方提示一个友好信息。

参数缺失的时候,会抛出 HttpMessageNotReadableException,我们可以拦截该异常,做一个友好处理,如下:

/**
 * 缺少请求参数异常
 * @param ex HttpMessageNotReadableException
 * @return
 */
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(
        MissingServletRequestParameterException ex) {
    logger.error("缺少请求参数,{}", ex.getMessage());
    return new JsonResult("400", "缺少必要的请求参数");
}

我们来写个简单的 Controller 测试一下该异常,通过 POST 请求方式接收两个参数:姓名和密码。

/**
 * <h3>springboot-study</h3>
 * <p>异常测试Controller类</p>
 * @author : zhang.bw
 * @date : 2020-07-17 21:37
 **/
@RestController
@RequestMapping("/exception")
public class ExceptionController {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

    @PostMapping("/test")
    public JsonResult test(@RequestParam("name") String name,
                           @RequestParam("pass") String pass) {

        logger.info("name:{}", name);
        logger.info("pass:{}", pass);
        return new JsonResult();
    }
}

然后使用 Postman 来调用一下该接口,调用的时候,只传姓名,不传密码,就会抛缺少参数异常,该异常被捕获之后,就会进入我们写好的逻辑,给调用方返回一个友好信息,如下:

 

2.2 处理空指针异常

和上面的逻辑一样,将异常信息换掉即可。如下:

/**
 * 空指针异常
 * @param ex NullPointerException
 * @return
 */
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleTypeMismatchException(NullPointerException ex) {
    logger.error("空指针异常,{}", ex.getMessage());
    return new JsonResult("500", "空指针异常");
}

然后使用 Postman 来调用一下该接口:localhost:8080/exception/null/point/exception

 

2.3 异常一劳永逸?

当然了,异常很多,比如还有 RuntimeException,数据库还有一些查询或者操作异常等等。由于 Exception 异常是父类,所有异常都会继承该异常,所以我们可以直接拦截 Exception 异常,一劳永逸:

/**
 * 系统异常 预期以外异常
 * @param ex
 * @return
 */
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
    logger.error("系统异常:", ex);
    return new JsonResult("500", "系统发生异常,请联系管理员");
}

但是项目中,我们一般都会比较详细的去拦截一些常见异常,拦截 Exception 虽然可以一劳永逸,但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下面,如果都没有找到,最后再拦截一下 Exception 异常。

3. 拦截自定义异常

在实际项目中,除了拦截一些系统异常外,在某些业务上,我们需要自定义一些业务异常,比如在微服务中,服务之间的相互调用很平凡,很常见。要处理一个服务的调用时,那么可能会调用失败或者调用超时等等,此时我们需要自定义一个异常,当调用失败时抛出该异常,给 GlobalExceptionHandler 去捕获。

3.1 定义异常信息

由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。比如:

/**
 * <h3>springboot-study</h3>
 * <p>业务异常提示信息枚举类</p>
 * @author : zhang.bw
 * @date : 2020-07-17 21:37
 **/
public enum BusinessMsgEnum {
    /** 参数异常 */
    PARMETER_EXCEPTION("100", "参数异常!"),
    /** 等待超时 */
    SERVICE_TIME_OUT("101", "服务调用超时!"),
    /** 500异常 */
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");

    /**
     * 消息码
     */
    private String code;
    /**
     * 消息内容
     */
    private String msg;

    BusinessMsgEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public String code() {
        return code;
    }

    public String msg() {
        return msg;
    }

}

3.2 拦截自定义异常

然后我们可以定义一个业务异常,当出现业务异常时,我们就抛这个自定义的业务异常即可。比如我们定义一个 BusinessErrorException 异常,如下:

/**
 * <h3>springboot-study</h3>
 * <p>自定义业务异常</p>
 * @author : zhang.bw
 * @date : 2020-07-17 21:37
 **/
public class BusinessErrorException extends RuntimeException {

    private static final long serialVersionUID = -7480022450501760611L;

    /**
     * 异常码
     */
    private String code;

    /**
     * 异常提示信息
     */
    private String message;

    public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
        this.code = businessMsgEnum.code();
        this.message = businessMsgEnum.msg();
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。

/**
 * 拦截业务异常,返回业务异常信息
 * @param ex
 * @return
 */
@ExceptionHandler(BusinessErrorException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleBusinessError(BusinessErrorException ex) {
    String code = ex.getCode();
    String message = ex.getMessage();
    return new JsonResult(code, message);
}

在业务代码中,我们可以直接模拟一下抛出业务异常,测试一下:

/**
 * <h3>springboot-study</h3>
 * <p>异常测试Controller类</p>
 * @author : zhang.bw
 * @date : 2020-07-17 21:37
 **/
@RestController
@RequestMapping("/exception")
public class ExceptionController {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

    @GetMapping("/business")
    public JsonResult testException() {
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            throw new BusinessErrorException( BusinessMsgEnum.UNEXPECTED_EXCEPTION);
        }
        return new JsonResult();
    }
}

运行一下项目,测试一下,返回 json 如下,说明我们自定义的业务异常捕获成功:

 

4. 总结

本节课程主要讲解了Spring Boot 的全局异常处理,包括异常信息的封装、异常信息的捕获和处理,以及在实际项目中,我们用到的自定义异常枚举类和业务异常的捕获与处理,在项目中运用的非常广泛,基本上每个项目中都需要做全局异常处理。

源码下载地址:关注公众号【猿码天地】并回复 springboot 获取

文章推荐

第011课:Spring Boot 集成Thymeleaf模板引擎

第008课:Spring Boot 返回JSON数据格式封装

第010课:Spring Boot 集成Swagger接口文档

第009课:Spring Boot 使用slf4j日志记录

第006课:Spring Boot集成MyBatis

第007课:Spring Boot MyBatis Druid 多数据源配置

第004课:Spring Boot 项目属性配置

第005课:Spring Boot 中MVC支持

第003课:Spring Boot 快速体验 Web 开发

第001课:Spring Boot 课程概览

第002课:Spring Boot 开发环境搭建

扫描二维码关注公众号 : 猿码天地

你多学一样本事,就少说一句求人的话,现在的努力,是为了以后的不求别人,实力是最强的底气。记住,活着不是靠泪水博得同情,而是靠汗水赢得掌声。

——《写给程序员朋友》

猜你喜欢

转载自blog.csdn.net/zbw125/article/details/107888781