SpringBoot项目优雅的全局异常处理

596 阅读4分钟

为什么要使用全局异常处理?

通常在代码中,我们处理异常是通过try-catch方法来捕获,进行后续处理,以便我们的代码能够在发生异常时进行一些操作,比如记录日志以便后续分析,或者继续执行后续的代码等等。

而SpringBoot项目都是按照MVC来进行代码分层,规范来说,通常我们后端Controller层提供接口方法,供前端等调用。而Controller层会继续调用Service层进行业务逻辑处理。这个过程中,通常我们需要在Controller层进行try-catch处理,以便在处理业务逻辑时发生异常,可以给调用者返回清晰的错误提示信息。比如下面的Controller层提供的调用方法代码:

@PostMapping("/modify")
public Result dataModify(@RequestBody @Validated Form form) {
    int row = 0;
    try {
        OrderDto orderDto = new OrderDto();
        BeanUtils.copyProperties(form, orderDto);
        row = orderService.modify(orderDto);
        if (row >= 1) {
            return Result.success();
        } else {
             return Result.fail("修改失败");
        }
        } catch (Exception ex) {
              log.error("order修改失败:" + ExceptionUtil.stacktraceToString(ex));
              return new Result(ex);
        }
}

Controller提供一个订单修改方法,调用Sercive层的modify方法进行具体业务逻辑处理。这个过程中,Service层的modify方法可能抛出异常,如果我们不在Controller层的方法进行处理,那么最终返回给前端的就是一个错误异常了。而通常我们希望返回前端的是一个标准格式,比如我们自定义一个Result类,里面的字段包括具体的错误提示信息,响应状态码等。这样无论发生什么异常,前端都可以接收到一个标准格式,按照标准格式进行处理,保证代码正常运行。

按照上述思路,我们就需要在Controller层的每个方法都用try-catch进行捕获处理,这块的代码很明显重复繁琐了,按照代码复用原则,我们可以将其抽离出来,统一处理,这就是SpringBoot提供的全局异常捕获处理功能,有了它,我们可以优雅的实现全局异常处理,减少代码耦合和重复。

如何使用SpringBoot提供的全局异常处理?

先看结果,比如我设置了全局异常处理,那么上述dataModify方法就可以优化成下面这个样子:

@PostMapping("/modify")
public Result dataModify(@RequestBody @Validated Form form) {
        int row = 0;
        OrderDto orderDto = new OrderDto();
        BeanUtils.copyProperties(form, orderDto);
        row = orderService.modify(orderDto);
        if (row >= 1) {
            return Result.success();
        } else {
             return Result.fail("修改失败");
        }
}

代码正常运行,返回Result格式数据。代码异常,由全局异常捕获处理,也返回Result格式。那么全局异常捕获具体如何处理呢?我们继续往下看。

SpringBoot为处理全局异常,提供了ControllerAdvice注解,这个注解的功能,就是在发生异常时,如果前面的异常链路没人处理,就会最终找到这个注解所在的类,从类中找到对应的异常类型处理方法。

比如我们这么写一个全局异常处理类:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(value = BusException.class)
    public Result<?> handleBizException(BusException e) {
    log.error("发生自定义业务异常:" + ExceptionUtil.stacktraceToString(e));
     return new Result<>(e);
    }
}

我们在类上添加了ControllerAdvice注解,那么发生无人处理的全局异常时,就会找到这个类。再根据异常类型,找到对应的处理方法。如果抛出的是我们自定义的一个BusException异常,那么通过方法上的@ExceptionHandler(value = BusinessException.class)注解,就可以找到我们对应的异常处理方法。比如我们可以先记录发生异常的栈信息,再返回标准的错误格式Result类给前端。

有了这个全局异常处理类和对应的异常类型处理方法,我们的Controller层代码就可以优雅起来了,不必每个接口方法都try-catch一遍。

另外,一些常见的异常,我们都可以在全局异常类中进行扩充处理,以便代码发生异常时,没人处理,也没记录重要的信息,后面不好排查。

比如通常我们会添加空指针异常处理方法,还有我们上述代码中,Controller入参使用了@Validated注解,那么我们就可以添加@Validated注解参数校验不通过时的异常处理方法,最后还有兜底的Throwable.class异常处理方法等。

到这,一个SpringBoot项目如何优雅的处理全局异常,已经可以正常运行跑通了。当然这样还是比较粗糙的,后面根据自己的项目需要进行一些定制优化就可以了。比如某些异常,需要发送告警信息给运维手动紧急处理,有些返回值需要跳转到一个优雅的错误页面等。

感谢大家看到这,我是创作者卡兰,热衷用简单易懂的小文章,分享总结自己的技术经验,欢迎大家关注。