バックグラウンド
Retrofitバージョン2.6.0は、kotlinを使用する開発者にとってメリットとなるsuspendメソッドをサポートしていますが、suspendメソッドを実行する場合、例外処理は依然として面倒です。trycatchを実行するか、kotlinを使用するには、例外処理を表示する必要があります。例外処理クラスCoroutineExceptionHandlerがそれを処理しますが、どちらの方法でも、コードは非常に苛立たしく、エレガントではありません。
エレガントなコード
val service = retrofit.create(WanAndroidService::class.java).proxyRetrofit()
// 执行 test
service.test()
.onSuccess { println("execute test success ==> $it") }
.onFailure { println("execute test() failure ==> $it") }
// 执行 userInfo
.onFailureThen { service.userInfo() }
?.onSuccess { println("execute userInfo success ==> $it") }
?.onFailure { println("execute userInfo() failure ==> $it") }
// 执行 banner
?.onFailureThen { service.banner() }
?.onSuccess { println("execute banner() success ==> $it") }
?.onFailure { println("execute banner() failure ==> $it") }
复制代码
キャッチを試さずに!!!
結果は次のとおりです。
execute test() failure ==> Failure(code=-1, message=HTTP 404 Not Found)
execute userInfo() failure ==> Failure(code=-1001, message=请先登录!)
execute banner() success ==> [{"desc":"一起来做个App吧","id":10,"imagePath":"https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png","isVisible":1,"order":1,"title":"一起来做个App吧","type":0,"url":"https://www.wanandroid.com/blog/show/2"},{"desc":"","id":6,"imagePath":"https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png","isVisible":1,"order":1,"title":"我们新增了一个常用导航Tab~","type":1,"url":"https://www.wanandroid.com/navi"},{"desc":"","id":20,"imagePath":"https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png","isVisible":1,"order":2,"title":"flutter 中文社区 ","type":1,"url":"https://flutter.cn/"}]
复制代码
これを読んだ後、このコードがあなたが想像したエレガントなコードではないと感じた場合は、現在のWebページを閉じることができます。
実装の原則
上記の例から、主な関数はproxyRetrofitメソッドであり、他のonSuccess、onFailure、およびonFailureThenは単なる拡張メソッドなので、proxyRetrofitの実装原理を見てみましょう。
レトロフィットはサスペンドメソッドの原則を処理します
try catchを取り除く方法を理解する前に、レトロフィットがサスペンドメソッドを処理する方法を理解する必要があります
ここでは、次のコードに焦点を当てます。
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name +
'.' +
method.name +
" was null but response body type was declared as non-null")
// 请求失败
continuation.resumeWithException(e)
} else {
// 请求成功
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
// 请求失败
continuation.resumeWithException(t)
}
})
}
}
// resume 方法会返回 success
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
// resumeWithException 方法会返回 failure
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
复制代码
レトロフィットのソースコードから、実行時に例外がスローされる原因はresumeWithExceptionであることがわかります。したがって、resumeWithExceptionメソッドをインターセプトできれば、例外を回避できます。
resumeWithExceptionをインターセプトする原則
/** * ThrowableResolver 的作用是在运行时遇到异常后如果处理异常 */
interface ThrowableResolver<T> {
// 处理异常,并返回一个处理后的数据
fun resolve(throwable: Throwable): T
}
/** * `proxyRetrofit` 方法主要作用是重新对接口进行动态代理,这样就可以在 * `InvocationHandler#invoke` 中对异常进行拦截,这样调用方就不用显示地调用 * `try catch` 了。 */
inline fun <reified T> T.proxyRetrofit(): T {
// 获取原先的 retrofit 的代理对象的的 InvocationHandler
// 这样我就可以继续使用 retrofit 的能力进行网络请求
val retrofitHandler = Proxy.getInvocationHandler(this)
return Proxy.newProxyInstance(
T::class.java.classLoader, arrayOf(T::class.java)
) { proxy, method, args ->
// 判断当前是为 suspend 方法
method.takeIf { it.isSuspendMethod }?.getSuspendReturnType()
// 通过方法的返回值获取一个 ThrowableResolver 处理异常
?.let { FactoryRegistry.getThrowableResolver(it) }
?.let { resolver ->
// 替换原始的 Contiuation 对象,这样我们就可以对异常进行拦截
args.updateAt(
args.lastIndex,
FakeSuccessContinuationWrapper(
args.last() as Continuation<Any>,
resolver as ThrowableResolver<Any>
)
)
}
retrofitHandler.invoke(proxy, method, args)
} as T
}
/** * 给 Method 添加的一个扩展属性,判断当前方法是不是 suspend 方法 */
val Method.isSuspendMethod: Boolean
get() = genericParameterTypes.lastOrNull()
?.let { it as? ParameterizedType }?.rawType == Continuation::class.java
/** * 给 Method 添加的扩展方法,获取当前 suspend 方法的返回值类型 */
fun Method.getSuspendReturnType(): Type? {
return genericParameterTypes.lastOrNull()
?.let { it as? ParameterizedType }?.actualTypeArguments?.firstOrNull()
?.let { it as? WildcardType }?.lowerBounds?.firstOrNull()
}
/** * Array 的扩展方法,更新指定 index 的值 */
fun Array<Any?>.updateAt(index: Int, updated: Any?) {
this[index] = updated
}
/** * Continuation 包装类,通过返回一个假的 Success(里面包含异常信息)拦截异常抛出 */
class FakeSuccessContinuationWrapper<T>(
private val original: Continuation<T>,
private val throwableResolver: ThrowableResolver<T>,
) : Continuation<T> {
override val context: CoroutineContext = original.context
override fun resumeWith(result: Result<T>) {
// 判断 result 是否为 success
result.onSuccess {
// 如果为 success 直接返回
original.resumeWith(result)
}.onFailure {
// 如果为 failure, 返回一个假的 success, 这样协程判断当前为 success
// 就不会抛出异常了,这样我们就达到了拦截异常的作用
val fakeSuccessResult = throwableResolver.resolve(it)
original.resumeWith(Result.success(fakeSuccessResult))
}
}
}
复制代码
これまで、コルーチンの例外をインターセプトする原理を明らかにしてきました。元の継続をラップすることで、結果が失敗して誤った成功を返す場合、コルーチンは例外をスローしません。
まとめ
suspendメソッドのContinuationを置き換えることで、try catchのインターセプトを完了し、プロジェクトのBaseResponseにいくつかの拡張メソッドを追加できます(KotlinのResult APIを模倣することをお勧めします)。これにより、ネットワークリクエストを非常に簡潔にすることができます。