码猿技术专栏

微信公众号:码猿技术专栏

满屏的try-catch,你不瘆得慌?

前言

软件开发过程中难免遇到各种的BUG,各种的异常,一直就是在解决异常的路上永不停歇,如果你的代码中再出现try(){...}catch(){...}finally{...}代码块,你还有心情看下去吗?自己不觉得恶心吗?

冗余的代码往往回丧失写代码的动力,每天搬砖似的写代码,真的很难受。今天这篇文章教你如何去掉满屏的try(){...}catch(){...}finally{...},解放你的双手。

Spring Boot 版本

本文基于的Spring Boot的版本是2.3.4.RELEASE

全局统一异常处理的前世今生

早在Spring 3.x就已经提出了@ControllerAdvice,可以与@ExceptionHandler@InitBinder@ModelAttribute 等注解注解配套使用,这几个此处就不再详细解释了。

这几个注解小眼一瞟只有@ExceptionHandler与异常有关啊,翻译过来就是异常处理器。**其实异常的处理可以分为两类,分别是局部异常处理全局异常处理**。

**局部异常处理**:@ExceptionHandler@Controller注解搭配使用,只有指定的controller层出现了异常才会被@ExceptionHandler捕获到,实际生产中怕是有成百上千个controller了吧,显然这种方式不合适。

**全局异常处理**:既然局部异常处理不合适了,自然有人站出来解决问题了,于是就有了@ControllerAdvice这个注解的横空出世了,@ControllerAdvice搭配@ExceptionHandler彻底解决了全局统一异常处理。当然后面还出现了@RestControllerAdvice这个注解,其实就是@ControllerAdvice@ResponseBody结晶。

Spring Boot的异常如何分类?

Java中的异常就很多,更别说Spring Boot中的异常了,这里不再根据传统意义上Java的异常进行分类了,而是按照controller进行分类,分为进入controller前的异常业务层的异常,如下图:

进入controller之前异常一般是javax.servlet.ServletException类型的异常,因此在全局异常处理的时候需要统一处理。几个常见的异常如下:

  1. NoHandlerFoundException:客户端的请求没有找到对应的controller,将会抛出404异常。
  2. HttpRequestMethodNotSupportedException:若匹配到了(匹配结果是一个列表,不同的是http方法不同,如:Get、Post等),则尝试将请求的http方法与列表的控制器做匹配,若没有对应http方法的控制器,则抛该异常
  3. HttpMediaTypeNotSupportedException:然后再对请求头与控制器支持的做比较,比如content-type请求头,若控制器的参数签名包含注解@RequestBody,但是请求的content-type请求头的值没有包含application/json,那么会抛该异常(当然,不止这种情况会抛这个异常)
  4. MissingPathVariableException:未检测到路径参数。比如url为:/user/{userId},参数签名包含@PathVariable("userId"),当请求的url为/user,在没有明确定义url为/user的情况下,会被判定为:缺少路径参数

如何统一异常处理?

在统一异常处理之前其实还有许多东西需要优化的,比如统一结果返回的形式。当然这里不再细说了,不属于本文范畴。

统一异常处理很简单,这里以前后端分离的项目为例,步骤如下

  1. 新建一个统一异常处理的一个类
  2. 类上标注@RestControllerAdvice这一个注解,或者同时标注@ControllerAdvice@ResponseBody这两个注解。
  3. 在方法上标注@ExceptionHandler注解,并且指定需要捕获的异常,可以同时捕获多个。

下面是作者随便配置一个demo,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 全局统一的异常处理,简单的配置下,根据自己的业务要求详细配置
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


/**
* 重复请求的异常
* @param ex
* @return
*/
@ExceptionHandler(RepeatSubmitException.class)
public ResultResponse onException(RepeatSubmitException ex){
//打印日志
log.error(ex.getMessage());
//todo 日志入库等等操作

//统一结果返回
return new ResultResponse(ResultCodeEnum.CODE_NOT_REPEAT_SUBMIT);
}


/**
* 自定义的业务上的异常
*/
@ExceptionHandler(ServiceException.class)
public ResultResponse onException(ServiceException ex){
//打印日志
log.error(ex.getMessage());
//todo 日志入库等等操作

//统一结果返回
return new ResultResponse(ResultCodeEnum.CODE_SERVICE_FAIL);
}


/**
* 捕获一些进入controller之前的异常,有些4xx的状态码统一设置为200
* @param ex
* @return
*/
@ExceptionHandler({HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class, MissingServletRequestParameterException.class,
ServletRequestBindingException.class, ConversionNotSupportedException.class,
TypeMismatchException.class, HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MissingServletRequestPartException.class, BindException.class,
NoHandlerFoundException.class, AsyncRequestTimeoutException.class})
public ResultResponse onException(Exception ex){
//打印日志
log.error(ex.getMessage());
//todo 日志入库等等操作

//统一结果返回
return new ResultResponse(ResultCodeEnum.CODE_FAIL);
}
}

注意上面的只是一个例子,实际开发中还有许多的异常需要捕获,比如TOKEN失效过期等等异常,如果整合了其他的框架,还要注意这些框架抛出的异常,比如ShiroSpring Security等等框架。

异常匹配的顺序是什么?

有些朋友可能疑惑了,如果我同时捕获了父类和子类,那么到底能够被那个异常处理器捕获呢?比如ExceptionServiceException

此时可能就疑惑了,这里先揭晓一下答案,当然是ServiceException的异常处理器捕获了,精确匹配,如果没有ServiceException的异常处理器才会轮到它的父亲父亲没有才会到祖父。总之一句话,精准匹配,找那个关系最近的。

为什么呢?这可不是凭空瞎说的,源码为证,出处org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#getMappedMethod,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
//遍历异常处理器中定义的异常类型
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
//是否是抛出异常的父类,如果是添加到集合中
if (mappedException.isAssignableFrom(exceptionType)) {
//添加到集合中
matches.add(mappedException);
}
}
//如果集合不为空,则按照规则进行排序
if (!matches.isEmpty()) {
matches.sort(new ExceptionDepthComparator(exceptionType));
//取第一个
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}

在初次异常处理的时候会执行上述的代码找到最匹配的那个异常处理器方法,后续都是直接从缓存中(一个Map结构,key是异常类型,value是异常处理器方法)。

别着急,上面代码最精华的地方就是对matches进行排序的代码了,我们来看看ExceptionDepthComparator这个比较器的关键代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//递归调用,获取深度,depth值越小越精准匹配
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
//如果匹配了,返回
if (exceptionToMatch.equals(declaredException)) {
// Found it!
return depth;
}
// 递归结束的条件,最大限度了
if (exceptionToMatch == Throwable.class) {
return Integer.MAX_VALUE;
}
//继续匹配父类
return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
}

精髓全在这里了,一个递归搞定,计算深度,depth初始值为0。值越小,匹配度越高越精准。

总结

全局异常的文章万万千,能够讲清楚的能有几篇呢?只出最精的文章,做最野的程序员,如果觉得不错的,关注分享走一波,谢谢支持!!!

另外作者的第一本PDF书籍已经整理好了,由浅入深的详细介绍了Mybatis基础以及底层源码,有需要的朋友公众号回复关键词Mybatis进阶即可获取,目录如下:

欢迎关注我的其它发布渠道