Retrofit + coroutine encapsulation, how to remove try catch gracefully?

background

Retrofit version 2.6.0 supports the suspend method, which is a boon for developers who use kotlin, but exception handling is still a cumbersome thing when executing the suspend method. It must be displayed to execute try catch, or use kotlin The built-in exception handling class CoroutineExceptionHandler handles it, but no matter which way, the code is very frustrating and not elegant.

elegant code

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") }
复制代码

without any try catch!!!

The results are as follows:

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/"}]
复制代码

If after reading this, you feel that this code is not the elegant code you imagined, then you can close the current web page.

Implementation principle

From the above example, the main function is the proxyRetrofit method, and the other onSuccess, onFailure and onFailureThen are just extension methods, so let's take a look at the implementation principle of proxyRetrofit.

Retrofit handles suspend method principle

Before understanding how to get rid of try catch, we need to understand how retrofit handles suspend methods

Here we focus on the following code:

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))
复制代码

From the source code of retrofit, it can be known that the culprit that causes the exception to be thrown at runtime is resumeWithException, so if we can intercept the resumeWithException method, the exception can be avoided.

The principle of intercepting 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))
        }
    }
}
复制代码

So far, we have uncovered the principle of intercepting coroutine exceptions. By wrapping the original Continuation, when the result is failure and returning a false success, the coroutine will not throw an exception.

summary

By replacing the Continuation of the suspend method, the interception of try catch can be completed, and then adding some extension methods to the BaseResponse in the project (it is recommended to imitate Kotlin's Result API), which can make our network requests extremely concise.

Guess you like

Origin juejin.im/post/7081558858149134372