思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在SpringBoot
应用开发中,利用@ControllerAdvice
结合 @ExceptionHandler
来实现对后端服务的统一异常
管理似乎已经成为了开发者约定俗成的规范了,为此网上也有很多文章来阐述如何更加优雅的来实现统一异常
管理,但这样做真的好吗?
前言
在SpringBoot
中的全局统一异常管理通常是指利用@ControllerAdvice
结合 @ExceptionHandler
来自定义一个全局的异常处理器
,从而避免了在程序中频繁写书写try-catch
来对异常进行处理。但结合笔者最近惨痛debug
旧有项目的经历来看,笔者不推荐这样去做。
在讨论之前我们不妨先来看看实际开发中都有哪些地方可能出现的异常。
全局异常所带来的困惑
目前,大部分应用
在开发时,通常会将代码划分为控制层,服务层,数据访问层
三层结构,每个模块负责自己独立的逻辑。简单来看,控制层
主要作用在于对外暴露 ur1
访问地址,并将前台的处理请求委托给服务层
来处理;而对于服务层
来说其主要是业务逻辑处理的地方,以登录请求为例,用户名、密码的校验通常会放在服务层来处理;数据访问层
则主要用于对数据库进行访问。如下这张图直观的反映了三层架构
下各个模块所肩负的责任。
知晓了软件开发过程中的分层
架构模式后。我们再来看每个模块可能产生的异常信息。其中:
-
控制层
主要用于处理实现前后端交互逻辑,其主要负责信息收禁、参数校验等,抛出的异常也多是参数校验、请求服务异常等异常信息。 -
服务层
主要用于处理业务逻辑,以及各种外部服务调用、访问数据作、缓存处理、消息处理等处理操作,这部分可能抛出的逻辑就非常多了。例如,数据库访问异常、服务调用异常等问题。 -
数据访问层
则主要负责数据访问实现,其一般没有业务逻辑。由于目前持久层使用技术通常为Mybatis
,所以这一层抛出的异常多是Mybatis
框架内部所抛出的异常。
不难发现,其中每一层所抛出的异常类型有着很大的差异。而我们在使用全局异常管理时,通常使用如下 代码逻辑:
java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGlobalException(Exception ex) {
// 在这里实现异常处理逻辑
log.error(ex.getMessage());//在控制台打印错误信息
return Result.error(ex.getMessage());
}
}
即我们通过在ExceptionHandler
执行捕获异常为Exception
来尽可能捕获业务中可能遇到的各种异常,相信网上已经有很多博主
都在不断讲授这样的写法。
但这样做真的好吗?在笔者最近接手的旧项目中,当后端无法处理前端请求后会返回如下信息:
json
{
"message": "服务器异常",
"code":602
}
而后台
服务通常会打印出如下信息:
(注:此处仅为展示,真实环境出现的异常远比此复杂)
不难发现,通过网上所盛传
全局异常处理逻辑,我根本不知道问题出在哪里。我只知道当前程序出现异常,且异常信息为/ by zero
。除此之外我无法得到任何有用的信息。
此外,通过在@ExceptionHandler
配置Exception.class
使得程序可以捕获多种异常信息,但这样粗粒度
做法所导致的直接问题就是无法精确的定位异常问题发生所在地,极大的提升了问题定位的难度 。以笔者
项目同事debug
经历来看,当项目无法正常运行时,组内的程序员通常会通过打日志的方法来一行一行定位问题所在。
破解之道
其实造成这样调试困难得本质原因在于全局异常
机制的滥用,如下这张图真实的反映了引入全局异常机制后,异常的处理逻辑。
不难发现,当引入全局异常
处理后,所有的异常信息都会交由RestExceptionHandler
来进行处理。当程序遇到异常时,会将异常信息抛给RestExceptionHandler
来处理,并由其定义错误的处理逻辑。
分析到此处,其实你已经发现了。对于全局异常处理逻辑而言,其更适合做异常
的兜底工作。即如果当前层出现异常,并且不断上抛的仍然无法解决的话,不妨通过全局统一的异常管理来进行处理,以对这些未处理
的异常进行捕获
。
此外,异常处理不应该进行像很多博客
说的那样,仅是通过e.getMessage
打印异常信息就可以了。这对于排查问题没有以一丁点的帮助,可以说是百害而无一利。
对于此,笔者更推荐在打印异常信息时,记录异常以及当前 URL、执行方法等信息以便后期方便问题排查。具体可参考如下代码:
java
@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,请求参数
等信息。
如果觉文章对你有帮助,不妨点赞+关注,不错过笔者之后的每一次更新!