学堂 学堂 学堂公众号手机端

后端 - 微服务开发系列:利用异常特性,把异常纳入框架管理之中

lewis 1年前 (2024-05-02) 阅读数 17 #技术

源码地址

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

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


InnerExp 作用

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

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

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

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

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

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

总结一下 InnerExp 的功能:

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

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

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

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

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

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

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

1 ServiceUnavailable

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

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

2 HttpMessageNotReadableException

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

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

框架中处理了四种 cause

InvalidFormatExceptionMissingKotlinParameterExceptionJsonMappingException其它异常

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 的方式返回,减少前端的处理工作。

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

本文参与了思否技术征文,欢迎正在阅读的你也加入。
版权声明

本文仅代表作者观点,不代表博信信息网立场。

热门