• 你有在SpringBoot中使用全局异常统一处理的习惯吗?
  • 发布于 2个月前
  • 150 热度
    0 评论

在SpringBoot应用开发中,利用@ControllerAdvice 结合 @ExceptionHandler来实现对后端服务的统一异常管理似乎已经成为了开发者约定俗成的规范了,为此网上也有很多文章来阐述如何更加优雅的来实现统一异常管理,但这样做真的好吗?


前言

在SpringBoot中的全局统一异常管理通常是指利用@ControllerAdvice 结合 @ExceptionHandler来自定义一个全局的异常处理器,从而避免了在程序中频繁写书写try-catch来对异常进行处理。但结合笔者最近惨痛debug旧有项目的经历来看,笔者不推荐这样去做。在讨论之前我们不妨先来看看实际开发中都有哪些地方可能出现的异常。


全局异常所带来的困惑
目前,大部分应用在开发时,通常会将代码划分为控制层,服务层,数据访问层三层结构,每个模块负责自己独立的逻辑。简单来看,控制层主要作用在于对外暴露 ur1访问地址,并将前台的处理请求委托给服务层来处理;而对于服务层来说其主要是业务逻辑处理的地方,以登录请求为例,用户名、密码的校验通常会放在服务层来处理;数据访问层则主要用于对数据库进行访问。如下这张图直观的反映了三层架构下各个模块所肩负的责任。

知晓了软件开发过程中的分层架构模式后。我们再来看每个模块可能产生的异常信息。其中:
控制层:主要用于处理实现前后端交互逻辑,其主要负责信息收禁、参数校验等,抛出的异常也多是参数校验、请求服务异常等异常信息。

服务层:主要用于处理业务逻辑,以及各种外部服务调用、访问数据作、缓存处理、消息处理等处理操作,这部分可能抛出的逻辑就非常多了。例如,数据库访问异常、服务调用异常等问题。

数据访问层:则主要负责数据访问实现,其一般没有业务逻辑。由于目前持久层使用技术通常为Mybatis,所以这一层抛出的异常多是Mybatis框架内部所抛出的异常。

不难发现,其中每一层所抛出的异常类型有着很大的差异。而我们在使用全局异常管理时,通常使用如下:
代码逻辑:
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGlobalException(Exception ex) {
         // 堆代码 duidaima.com
        // 在这里实现异常处理逻辑
       log.error(ex.getMessage());//在控制台打印错误信息
       return Result.error(ex.getMessage());
    }
}
即我们通过在ExceptionHandler执行捕获异常为Exception来尽可能捕获业务中可能遇到的各种异常,相信网上已经有很多博主都在不断讲授这样的写法。
但这样做真的好吗?在笔者最近接手的旧项目中,当后端无法处理前端请求后会返回如下信息:
{
   "message": “服务器异常”,
   "code":602
}
而后台服务通常会打印出如下信息:
(注:此处仅为展示,真实环境出现的异常远比此复杂)

不难发现,通过网上所盛传全局异常处理逻辑,我根本不知道问题出在哪里。我只知道当前程序出现异常,且异常信息为/ by zero。除此之外我无法得到任何有用的信息。


此外,通过在@ExceptionHandler配置Exception.class使得程序可以捕获多种异常信息,但这样粗粒度做法所导致的直接问题就是无法精确的定位异常问题发生所在地,极大的提升了问题定位的难度。以笔者项目同事debug经历来看,当项目无法正常运行时,组内的程序员通常会通过打日志的方法来一行一行定位问题所在。


破解之道

其实造成这样调试困难得本质原因在于全局异常机制的滥用,如下这张图真实的反映了引入全局异常机制后,异常的处理逻辑。不难发现,当引入全局异常处理后,所有的异常信息都会交由RestExceptionHandler来进行处理。当程序遇到异常时,会将异常信息抛给RestExceptionHandler来处理,并由其定义错误的处理逻辑。


分析到此处,其实你已经发现了。对于全局异常处理逻辑而言,其更适合做异常的兜底工作。即如果当前层出现异常,并且不断上抛的仍然无法解决的话,不妨通过全局统一的异常管理来进行处理,以对这些未处理的异常进行捕获。


此外,异常处理不应该进行像很多博客说的那样,仅是通过e.getMessage打印异常信息就可以了。这对于排查问题没有以一丁点的帮助,可以说是百害而无一利。


对于此,笔者更推荐在打印异常信息时,记录异常以及当前 URL、执行方法等信息以便后期方便问题排查。具体可参考如下代码:
@Slf4j
@RestControllerAdvice
public class GlobExceptionHandler {


    @ExceptionHandler(ArithmeticException.class)//ArithmeticException异常类型通过注解拿到
    public String exceptionHandler(HttpServletRequest request,ArithmeticException exception){
       // 打印详细信息
        log(request,exception);
        return exception.getMessage();

    }


    public void log(HttpServletRequest request, Exception exception) {
        //换行符
        String lineSeparatorStr = System.getProperty("line.separator");

        StringBuilder exStr = new StringBuilder();
        StackTraceElement[] trace = exception.getStackTrace();
        // 获取堆栈信息并输出为打印的形式
        for (StackTraceElement s : trace) {
            exStr.append("\tat " + s + "\r\n");
        }
        //打印error级别的堆栈日志
        log.error("访问地址:" + request.getRequestURL() + ",请求方法:" + request.getMethod() +
                ",远程地址:" + request.getRemoteAddr() + lineSeparatorStr +
                "错误堆栈信息如下:" + exception.toString() + lineSeparatorStr + exStr);
    }
}

当程序发生错误时,其打印的日志信息如下:不难发现,其完整的打印出了url、方法信息,错误参数、请求地址等信息,极大的降低了线上Bug的排查难度。


总结

技术本身并没有什么对与错之分,只不过有时我们用的方式和时机不对,进而使得本该提效的工具,反而在不断拖垮我们的效率。就如同本文分析的全局异常处理机制一样,其确实可以帮助我们降低try-catch的使用,但错误且不加考虑的乱用只会使得当系统出现问题时,我们只能两眼一抹黑,然后一行一行打日志来定位问题。


最后,对于代码中异常的捕获处理,笔者认为全局异常应该作为异常处理的都兜底操作,而不应该成为异常处理的灵丹妙药! 此外,全局异常处理过程不应该仅是简单的 e.getMessage()打印异常消息即可,其更应记录更加有助于异常排查的信息例如,方法,请求的url,请求参数等信息。
用户评论