源码地址

微服务开发系列:开篇
微服务开发系列:为什么选择 kotlin
微服务开发系列:为什么用 gradle 构建
微服务开发系列:目录结构,保持整洁的文件环境
微服务开发系列:服务发现,nacos 的小补充
微服务开发系列:怎样在框架中选择开源工具
微服务开发系列:数据库 orm 使用
微服务开发系列:如何打印好日志
微服务开发系列:鉴权
微服务开发系列:认识到序列化的重要性
微服务开发系列:设计一个统一的 http 接口内容形式
微服务开发系列:利用异常特性,把异常纳入框架管理之中
微服务开发系列:利用 knife4j,生成最适合微服务的文档

在系统中,异常被分为了两种,一种是已知异常,一种是未知异常。

已知异常在框架中被封装为 cn.vte.framework.common.http.InnerExp,一般为业务异常,可以在任何接口访问范围内的代码内抛出。

InnerExp 作用

InnerExp 是为了能够灵活返回信息到前端,当方法的调用过多时或者逻辑嵌套过深,在满足条件时,不用一层一层返回,直接抛出异常即可把信息返回。

配合 GlobalMvcExceptionHandler 使用,该类拦截了所有的 mvc http 接口异常,任何异常都会被处理成 RequestResult 结构。

InnerExp 特别的是,它会携带两种信息,一个是 RequestResult 还有一个是 Exception。这让 InnerExp 有更加灵活的功能。

InnerExp 携带 RequestResult 能够让返回的结果更加丰富,开发人员可以自定义 RequestResult 的内容,甚至与返回正常的结果。

比如当你使用递归调用时,满足条件收集到了足够的信息时,抛出一个异常直接把结果返回,所以异常的使用不完全是为了通知程序异常了,灵活使用也能够处理正常的业务。

InnerExp 携带 Exception 时,GlobalMvcExceptionHandler 会判断 cause 是否为空,如果不为空就直接打印堆栈信息,帮助排查问题,在业务代码遇见异常,并且不想处理时,直接交给最上层去做,有些时候会更加方便。

总结一下 InnerExp 的功能:

  1. 快速抛出异常,将信息返回给前台,函数嵌套的情况下不用层层传递信息,并且能够携带真实异常,顶层打印堆栈信息。
  2. 快速返回业务信息,同样不用层层传递。
  3. 继承 Supplier 接口,在流式处理中更加方便抛出异常。
  4. 利用 ifThr 传入条件判断是否应该抛出异常。

处理其它异常

在已知异常之外,其它异常情况的处理就相对棘手一些,出现什么种类的异常是完全未知的。

你只能在发生某些异常的时候处理。

可能你会问,为什么不直接都当成一种情况处理呢?只要发生异常一律告知内部异常。

这样处理虽然简单,但是很多异常实际上不是后端逻辑错误引擎的,更多的是数据校验方面的引起的,一般是由于前段发送数据不符合后端的要求。

此时,你就要处理这种用异常,把他们整理成前端开发人员,甚至是用户能够看懂的异常。

下面是框架中,所遇见过的一些异常类型处理。

1 ServiceUnavailable

由于 feign 调用不到的产生的异常。

框架会返回”后台服务未启动”的提醒。

2 HttpMessageNotReadableException

由于前端传入的 json 数据,无法正常序列化为 java 对象产生的异常。

此异常处理的情况较为复杂,因为处理异常本身不重要,更重要的是处理它携带的 cause

框架中处理了四种 cause

  1. InvalidFormatException
  2. MissingKotlinParameterException
  3. JsonMappingException
  4. 其它异常

1、2、3,实际上都为 jackson 所抛出的异常。

此处,又是 jackson 的一大亮点,那就是你能够通过这些异常,来捕捉到 json 序列化的错误位置。

因为这些异常,都携带有 getPath() 方法,能够找到嵌套的 json 错误路径,于是框架中利用了这一点,来获取 jackson 序列化错误的详细字段信息

fun List<JsonMappingException.Reference>.toPath(): String {
    val sb = StringBuilder()
    this.mapIndexed { index, reference ->
        sb.append(
            if (index == 0 || reference.fieldName == null) {
                reference.fieldName ?: "[${reference.index}]"
            } else {
                ".${reference.fieldName}"
            }
        )
    }
    return sb.toString()
}

举个例子,下面的这段代码,将会输出,a.type,来告知前端,a 对象下的 type 缺失。

data class A(val name: String, val type: String)
data class B(val name: String, val a: A)

val test = JacksonObject().put("name", "b").put("a", JacksonObject().put("name", "a"))
try {
    test.convert<B>()
} catch (e: IllegalArgumentException) {
    val cause = e.cause
    if (cause is MissingKotlinParameterException) {
        println(cause.path.toPath())
    }
}

除了上面的处理 JsonMappingException 还要单独拿出来处理,下面这段代码说明了处理的思路

is JsonMappingException -> {
    val subCause = cause.cause
    if (subCause is InnerExp) {
        // 发生于 class init 抛出异常
        subCause.result
    } else if (subCause is DateTimeParseException) {
        RequestResult.error("""字段"{}"时间值错误:"{}"""", cause.path.toPath(), subCause.parsedString)
    } else {
        log.warn("{}", request.requestURI, exception)
        RequestResult.error("""字段"{}"值错误:"{}"""", cause.path.toPath())
    }
}

这样处理异常的方式,很明显能够大大减少排查问题的难度。

3 NoHandlerFoundException

当地址找不到产生的异常,如果不处理,将返回 404。

4 MethodArgumentNotValidException

当 spring boot valid 校验失败产生的异常。

框架中,将会获取到校验失败的信息,然后以 RequestResult 的方式返回,减少前端的处理工作。

如果不处理,前端是无法获取到错误的详细信息的,那么校验所能产生的作用就会大打折扣。


zxdposter
3.9k 声望3.5k 粉丝