ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

网络 - 异常封装

网络 - 异常封装 一、概念传输层异常业务层错误码表达什么请求没通被框架抛出跨项目通用。请求通了但业务失败需要自己检查后抛出。来源HTTP 状态码、IO、解析。响应体里的 code 字段。数量4~5类跨项目通用。几十~几百个项目特有。例子404 / 500 / 断网 / 字段缺失余额不足、库存不足、已被封禁。1.1 传输层错误错误类型触发场景典型文案自动重试上报Http 4XX参数错、未授权、资源不存在内容不存在 / 请重新登录❌重试也没用401/403 上报Http 5XX服务端异常服务暂时不可用请稍后重试✅✅必须Network断网、超时、DNS 失败网络似乎不太稳✅❌ 用户环境问题Parse200 但 JSON 对不上模型数据异常❌✅ 必须要发版修Unknow没预料到的异常出了点问题❌✅1.2 业务层错误码项目特有换后端就作废大量后端习惯是业务失败也返回 HTTP 200失败原因放在响应体里。{ code: 20001, msg: 余额不足, data: null }二、封装2.1 业务错误码 BizCodes全局码集中处理登录态失效、版本过低 → 在网络数据源或全局处理器里统一跳登录/弹升级不允许每个页面各写一遍。业务码交给调用点只有下单页知道余额不足该弹充值还是改支付方式。默认文案兜底其余码用服务端 msg没有就用操作失败。object BizCodes { const val OK 0 // 全局码任何接口都可能返回框架统一处理 const val TOKEN_EXPIRED 10001 const val ACCOUNT_DISABLED 10002 const val APP_VERSION_TOO_LOW 10003 // 业务码只和具体场景有关 const val INSUFFICIENT_BALANCE 20001 const val OUT_OF_STOCK 20002 // 存起来方便判断 val GLOBAL_CODES setOf( TOKEN_EXPIRED, ACCOUNT_DISABLED, APP_VERSION_TOO_LOW ) val BUSINESS_CODES setOf( INSUFFICIENT_BALANCE, OUT_OF_STOCK ) val ALL GLOBAL_CODES BUSINESS_CODES fun isGlobal(code: Int) code in GLOBAL_CODES fun isBusiness(code: Int) code in BUSINESS_CODES fun contains(code: Int) code in ALL }2.2 自定义异常 ApiException网络层统一异常携带分类好的 ApiError。包一层 ApiException 而不是直接抛 ApiError 是因为 Kotlin 的协程和 Retrofit 的异常传播机制都是围绕 Throwable 的用异常才能自动向上冒泡调用链中间层不用层层判空。而 ApiError 作为数据挂在异常上负责分类。class ApiException( val errorCode: Int, val errorMsg: String, ) : Exception(errorMsg)2.3 通用返回类型 ApiResultTRetrofit 的 ApiService 方法返回的类型它是 Json 根节点类型。data class ApiResultT( val errorCode: Int, val errorMsg: String, val data: T? ) { fun getData(): ResultT { return if (errorCode BizCodes.OK) { // 业务码成功数据不为空就返回数据为空就转为 JsonParseException if (data ! null) { Result.success(data) } else { Result.failure(JsonParseException(data 为空)) } } else { // 业务码错误转为自定义的 ApiException Result.failure(ApiException(errorCode, errorMsg)) } } }GET(user/json) suspend fun getUser(): ApiResultListUserBean2.4 异常处理上报 弹Toast。异常类型谁抛出的触发时机HttpExceptionRetrofit收到响应但状态码非 2xx。IOException超时、未知主机、连接错误OkHttp请求没发成功走 onFailure。Json***ExceptionGson、MoshiConverter 解析 body 失败同样收进 onFailure。CancellationExceptionKotlin协程协程取消时挂起点直接抛出。Gson 抛出的是JsonParseException、JsonSyntaxException、JsonIOException。其它两个都是 JsonParseException 的子类型因此只需要对 JsonParseException 进行判断就行。Moshi 抛出的是JsonDataException、JsonEncodingException。fun handleException(throwable: Throwable) when(throwable) { // 协程取消需要放行否则失去结构化并发 is CancellationException - throw throwable // HTTP 错误 is HttpException - { val code throwable.code() when { code 401 || code 403 - 登录态失效请重新登录。错误码$code.toast() code in 400..499 - 请求有误。错误码$code.toast() code 500 - 服务暂时不可用。错误码$code.toast() else - HTTP错误。错误码$code.toast() } } // 网络错误 is IOException - 网络不可用${throwable.message}.toast() // Json 错误 is JsonParseException - 数据解析异常请升级应用。.toast() // 业务错误 is ApiException - 业务错误:${throwable.errorMsg}。错误码${throwable.errorCode}.toast() // 其它业务错误 else - 其它错误${throwable.message}.toast() }2.5 统一请求 fetchData()这里使用了 Kotlin Result API 封装详见。suspend fun T fetchData( block: suspend () - ApiResultT ) runCatching { block() }.then { it.getData() }.onFailure { // onFailure() 抛出的异常不会被后续链式的 onFailure() 捕获 // 除非 fetchData() 又被 runCatching 或 try-catch 捕获 handleException(it) }2.6 使用suspend fun getUser() { viewModelScope.launch { RetrofitClient.apiService.getUser() }.ononSuccess { // TODO 更新状态 } }2.7 最终封装成2个文件2.7.1 ApiResult.ktdata class ApiResultT( val errorCode: Int, val errorMsg: String, val data: T? ) { fun getData(): ResultT { return if (errorCode BizCodes.OK) { // 业务码成功数据不为空就返回数据为空就转为 JsonParseException if (data ! null) { Result.success(data) } else { Result.failure(JsonParseException(data 为空)) } } else { // 业务码错误转为自定义的 ApiException Result.failure(ApiException(errorCode, errorMsg)) } } } object BizCodes { const val OK 0 // 全局码任何接口都可能返回框架统一处理 const val TOKEN_EXPIRED 10001 const val ACCOUNT_DISABLED 10002 const val APP_VERSION_TOO_LOW 10003 // 业务码只和具体场景有关 const val INSUFFICIENT_BALANCE 20001 const val OUT_OF_STOCK 20002 // 存起来方便判断 val GLOBAL_CODES setOf( TOKEN_EXPIRED, ACCOUNT_DISABLED, APP_VERSION_TOO_LOW ) val BUSINESS_CODES setOf( INSUFFICIENT_BALANCE, OUT_OF_STOCK ) val ALL GLOBAL_CODES BUSINESS_CODES fun isGlobal(code: Int) code in GLOBAL_CODES fun isBusiness(code: Int) code in BUSINESS_CODES fun contains(code: Int) code in ALL } class ApiException( val errorCode: Int, val errorMsg: String, ) : Exception(errorMsg)2.7.2 ExceptionHandler.kt对 fetchData() 加内存缓存 LruCache加磁盘缓存 DiskLruCache。suspend fun T fetchData( block: suspend () - ApiResultT ) runCatching { block() }.then { it.getData() }.onFailure { // onFailure() 抛出的异常不会被后续链式的 onFailure() 捕获 // 除非 fetchData() 又被 runCatching 或 try-catch 捕获 handleException(it) } private fun handleException(throwable: Throwable) when(throwable) { // 协程取消需要放行否则失去结构化并发 is CancellationException - throw throwable // HTTP 错误 is HttpException - { val code throwable.code() when { code 401 || code 403 - 登录态失效请重新登录。错误码$code.toast() code in 400..499 - 请求有误。错误码$code.toast() code 500 - 服务暂时不可用。错误码$code.toast() else - HTTP错误。错误码$code.toast() } } // 网络错误 is IOException - 网络不可用${throwable.message} // Json 错误 is JsonParseException - 数据解析异常请升级应用。.toast() // 业务错误 is ApiException - 业务错误:${throwable.errorMsg}。错误码${throwable.errorCode}.toast() // 其它业务错误 else - 其它错误${throwable.message} }
返回列表