Last active
August 28, 2018 15:31
-
-
Save fullkomnun/1707cf55fcab41122544e7da5ae1cb35 to your computer and use it in GitHub Desktop.
A retrofit CallAdapterFactory that attaches the original service method name to the corresponding request by using it's tag property. For debugging purposes only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class DebugCallAdapterFactory(private val callAdapterFactories: List<CallAdapter.Factory>) : CallAdapter.Factory() { | |
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<out Any, out Any>? = | |
callAdapterFactories.asSequence() | |
.map { it.get(returnType, annotations, retrofit) } | |
.filterNotNull().firstOrNull()?.let { WrappingCallAdapter(it, retrofit) } | |
} | |
private class WrappingCallAdapter<R, T>(private val adapter: CallAdapter<R, T>, | |
private val retrofit: Retrofit) : CallAdapter<R, T> by adapter { | |
override fun adapt(call: Call<R>): T = adapter.adapt(WrapperCall(call, retrofit)) | |
} | |
private class WrapperCall<T>(private val call: Call<T>, | |
private val retrofit: Retrofit) : Call<T> by call { | |
override fun request(): Request = request | |
override fun enqueue(callback: Callback<T>) = request.let { call.enqueue(callback) } | |
override fun execute(): Response<T> = request.let { call.execute() } | |
override fun clone() = WrapperCall<T>(call.clone(), retrofit) | |
@Suppress("UNCHECKED_CAST") | |
private val request by lazy { | |
call.request().apply { | |
(tag() as? Array<String>)?.set(0, extractMethodName()) | |
} | |
} | |
private fun extractMethodName(): String = | |
serviceMethodCache?.let { cache -> | |
synchronized(cache) { | |
cache.entries.singleOrNull { it.value == serviceMethod }?.key?.name | |
} | |
} ?: "" | |
private val serviceMethodCache by lazy { | |
serviceMethodCacheField.getTyped<Map<Method, ServiceMethod<*, *>>>(retrofit) | |
} | |
private val serviceMethod by lazy { | |
serviceMethodField.getTyped<ServiceMethod<*, *>>(call) | |
} | |
} | |
private val serviceMethodField by lazy { accessDeclaredField<OkHttpCall<*>>("serviceMethod") } | |
private val serviceMethodCacheField by lazy { accessDeclaredField<Retrofit>("serviceMethodCache") } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun setup() { | |
val moshi = Moshi.Builder() | |
.add(ApplicationJsonAdapterFactory.INSTANCE) | |
.build() | |
val callAdapterFactories = listOf(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) | |
val httpClient = createOkHttpClient() | |
val retrofit = Retrofit.Builder() | |
.baseUrl("https://api.github.com") | |
.client(httpClient) | |
.callFactory { request -> | |
httpClient.newCall(request.newBuilder().tag(arrayOf("")).build()) | |
} | |
.addConverterFactory(MoshiConverterFactory.create(moshi)) | |
.addCallAdapterFactory(DebugCallAdapterFactory(callAdapterFactories)) | |
.build() | |
target = retrofit.create(GithubApiService::class.java) | |
} | |
private fun createOkHttpClient(): OkHttpClient { | |
return OkHttpClient.Builder() | |
.addInterceptor { | |
println("operation is ${extractTag(it)}") | |
it.proceed(it.request()) | |
}.build() | |
} | |
private fun extractTag(it: Interceptor.Chain) = (it.request().tag() as? Array<String>)?.get(0) ?: "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
inline fun <reified T> Field?.getTyped(obj: Any): T? { | |
return try { | |
this?.get(obj) as? T | |
} catch (e: Exception) { | |
Timber.w("failed to get field '${this?.name}' on $obj") | |
null | |
} | |
} | |
inline fun <reified T> Field?.setTyped(obj: Any, value: T): T? { | |
return try { | |
this?.set(obj, value)?.let { value } | |
} catch (e: Exception) { | |
Timber.w("failed to set field '${this?.name}' on $obj") | |
null | |
} | |
} | |
inline fun <reified T> accessDeclaredField(name: String): Field? { | |
return try { | |
T::class.java.getDeclaredField(name)?.apply { isAccessible = true } | |
} catch (e: Exception) { | |
Timber.w(e, "failed to access '$name' field") | |
null | |
} | |
} | |
inline fun <T1 : Any, T2 : Any, R : Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2) -> R?): R? { | |
return if (p1 != null && p2 != null) block(p1, p2) else null | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Purpose: (Debugging) Retaining the service method name of retrofit async operations, making them available for okhttp interceptors.
This way, the meaningful operation name can be logged alongside the corresponding request/response which is useful
when uri and http method alone are not enough to identify the operation.
Usage: pass all other call adapter factories to the 'DebugCallAdapterFactory' then register it as the sole call adapter factory of retrofit.
Operation name is then accessible for interceptors through the request's tag.
Implementation: There's no easy way to extract service method name without having to
pass custom headers for each service method (https://goo.gl/dxSnUz)
or decorate each service method with custom annotations (https://goo.gl/gcs6Nt).
There's also no easy way of leveraging OhHttp request's tag through retrofit (https://goo.gl/BLHFuS)
So, the implementation is quite hacky and has to rely on internal workings of Retrofit through Reflection :-(.
It is however implemented as a Retrofit 'CallAdapterFacrory' alongside other factories through composition and is easy to use.