Custom retrofit2 annotations - revisited

On the 10/09/2020 I updated my article from 2016, just rewriting it with Kotlin in mind.

Recently I found a simpler way of creating custom Annotations.

In this article I will explain how to create a custom annotation for retrofit.

Why do you need custom annotations?

I guess in most cases you don’t need to use custom annotations, but interceptors. Since retrofit2 is based on okhttp, you can use interceptors. But what does it do? Well, basically every request you call (using retrofit2 or plain okhttp) calls all setup interceptors before executing the requests. The Interceptor is an interface which contains only one method to implement, called (surprise ^^) intercept, which get’s an object of Chain to proceed.

How to use Interceptors?

You can use Chain to execute

1
return chain.proceed(chain.request()) // returns Response

or modify request and execute

1
2
3
4
5
val request = chain.request().newBuilder()
// modify the request as you need it
...
.build()
chain.proceed(request)

the next upcoming request.
This setup applies for ALL requests that run with the okhttp client in which you set up these interceptors.

Sometimes this is not enough, especially when you have runtime requirements for the modifications of the request. I actually came across this while I wanted to have a configurable annotation for caching some (not all) responses.

So I wrote a library, that provides an annotation for response cache.

Implementing a custom annotations

Defining the annotation

I guess this is the easiest part, just define your annotation as you like it. In my case I needed an annotation that can take some Information.

1
2
3
4
5
@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
@Retention(RUNTIME)
annotation class SomeAnnotation(
val value: String
)

Linking the request with the annotation

Now we need to read the annotation and pass containing data (in our example above the value: String) to an Interceptor in which we can modify using this data. Here comes to the biggest difference towards the previous version of this article.

In order to get information about the request, retrofit tags the request with an Invocation tag. Using this, we can extract the annotation for the request.

1
2
3
4
5
6
7
8
9
private fun findAnnotation(
request: Request
): SomeAnnotation? {
return request.tag(Invocation::class.java)
?.method()
?.annotations
?.filterIsInstance<SomeAnnotation>()
?.firstOrNull()
}

Manipulating the request

Now we can adjust the request/response as we would like to, using the data from our annotation.

1
2
3
4
5
6
7
8
9
10
11
12
class SomeCustomInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val annotation = findAnnotation(request)
if (annotation != null) {
request = chain.request().newBuilder()
.addHeader("SomeCustomHeader", annotation.value)
.build()
}
return chain.proceed(request)
}
}

When intercepting the request we check if the request was annotated. If so, we modify the request as we need it, if not we just execute it.

Is this it?

Actually, yes. Now we just need to make sure that our Interceptor is applied.

1
2
3
4
5
6
7
8
9
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com/")
.client(
OkHttpClient.Builder()
.addInterceptor(SomeCustomInterceptor())
.build()
)
// ...
.build()

And now we’re able to use our newly created annotation.

Improvements

Problem: Reflection

Compared to the previous article, this version is using reflection to find the annotation that was added to the request. In order to minimize the problem I added some caching to the reflection part, so that it only uses reflection once and not every time the call is done.

1
2
3
4
5
6
7
8
9
10
11
12
13
private val registration: MutableMap<Int, SomeAnnotation> = mutableMapOf()

private fun findAnnotation(
request: Request
): SomeAnnotation? {
val key = request.url.hashCode()
return registration[key] ?: request.tag(Invocation::class.java)
?.method()
?.annotations
?.filterIsInstance<SomeAnnotation>()
?.firstOrNull()
?.also { registration[key] = it }
}

If you want to go completely without reflections, use the other article.

Note: retrofit itself is also using reflections to stub the methods from the api interfaces, it’s also caching them, similar to this solution.

Summary

In this article we discovered how to implement a custom annotation for retrofit2.