微服务开发系列:开篇
微服务开发系列:为什么选择 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
的功能:
- 快速抛出异常,将信息返回给前台,函数嵌套的情况下不用层层传递信息,并且能够携带真实异常,顶层打印堆栈信息。
- 快速返回业务信息,同样不用层层传递。
- 继承
Supplier
接口,在流式处理中更加方便抛出异常。 - 利用
ifThr
传入条件判断是否应该抛出异常。
处理其它异常
在已知异常之外,其它异常情况的处理就相对棘手一些,出现什么种类的异常是完全未知的。
你只能在发生某些异常的时候处理。
可能你会问,为什么不直接都当成一种情况处理呢?只要发生异常一律告知内部异常。
这样处理虽然简单,但是很多异常实际上不是后端逻辑错误引擎的,更多的是数据校验方面的引起的,一般是由于前段发送数据不符合后端的要求。
此时,你就要处理这种用异常,把他们整理成前端开发人员,甚至是用户能够看懂的异常。
下面是框架中,所遇见过的一些异常类型处理。
1 ServiceUnavailable
由于 feign 调用不到的产生的异常。
框架会返回”后台服务未启动”的提醒。
2 HttpMessageNotReadableException
由于前端传入的 json 数据,无法正常序列化为 java 对象产生的异常。
此异常处理的情况较为复杂,因为处理异常本身不重要,更重要的是处理它携带的 cause
。
框架中处理了四种 cause
:
InvalidFormatException
MissingKotlinParameterException
JsonMappingException
- 其它异常
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
的方式返回,减少前端的处理工作。
如果不处理,前端是无法获取到错误的详细信息的,那么校验所能产生的作用就会大打折扣。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。