使用最简单的方式扩展 retrofit 的注解类型

3,735 阅读4分钟

初衷

现在大部分的应用都会使用 Retrofit + OkHttp 的网络请求,仅从使用的角度来讲,这两者最大的特点是 注解 + 拦截器

在平时开发的过程中,Client 和 Server 的开发工作是并行的,这个时候 Client 就需要一些 mock 数据来进行开发,此外做一些单纯的演示功能或者离线 app 的时候也需要一些本地写死的数据,平常开发的时候我们都是将将数据放到 assets 下然后读取后 gson 序列化一下塞给数据源类,这样的做法显得有些笨拙且麻烦,需要每次修改一堆代码,所以就想着要是能用注解来让网络请求库自动返回 mock 数据就好了,比如像下面这样调用:

@Mock(assets = "github.json")
@GET("search/repositories")
fun searchRepos(@Query("q") query: String): LiveData<ApiResponse<RepoSearchResponse>>

同样如果有一些特殊需求都可以使用自定义的注解来处理,这样可以提高一些灵活性。

实现原理

大体流程如下:

关键问题就是在 OkHttp 的拦截器中如何获取 searchRepos 的注解信息。 要想解决这个问题我们就需要带着下面 3 个问题去看 Retrofit 的源码:

  1. Retrofit 是如何处理注解
  2. Retrofit 调用 OkHttp 的时候都传递了什么参数
  3. OkHttp 如何在拦截器中获取到 Retrofit Method 中注解

Retrofit 调用 OkHttp 的流程

使用 Retrofit 的步骤如下:

  • 先定一个服务接口
interface GithubService {
    @Mock(assets = "github.json")
    @GET("search/repositories")
    fun searchRepos(@Query("q") query: String): LiveData<ApiResponse<RepoSearchResponse>>
}
  • 创建一个 Retrofit 实例
Retrofit.Builder().client(okHttpClient)
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create(Gson()))
            .addCallAdapterFactory(LiveDataCallAdapterFactory())
            .build()
            .create(GithubService::class.java
)

当调用 create 方法的时候会为 GithubService 添加动态代理,每次调用 searchRepos 都会调用 HttpServiceMethod 的 invoke 方法:

public <T> T create(final Class<T> service) {
    ...
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          ...
            return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
          }
        });
  }
ServiceMethod<?> loadServiceMethod(Method method) {
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method);
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

可以看到 Retrofit 每次调用 metheod 的时候都会把 method 添加到这个缓存中:

public final class Retrofit {
  private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();
  }

key 就是 method,value 是 HttpServiceMethod 再来查看它是何方神圣:

HttpServiceMethod(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
      Converter<ResponseBody, ResponseT> responseConverter) {
    this.requestFactory = requestFactory;
    this.callFactory = callFactory;
    this.responseConverter = responseConverter;
  }

可以看到它有三个字段:

  • requestFactory:保存了所有的请求相关的数据,比如请求方法是 GET 还是 POST,url 以及请求参数等。
  • callFactory:创建 OkHttp 的 Call,用于请求网络
  • responseConverter: 用于序列化 response

在 requestFactory 中我们找到了第一个问题的答案:

完蛋,没法修改 Retrofit 的源码来额外解析自定义的注解。

那么再从 callFactory 寻找第二个问题的答案,HttpServiceMethod 的 invoke 方法中使用了 callFactory 创建了 OkHttpCall:

 @Override final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }

OkHttpCall 中创建了 OkHttp 的 Request 和 Call

private okhttp3.Call createRawCall() throws IOException {
    okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
    ...
    return call;
  }

关键的 tag

其中 requestFactory 创建了 okhttp3.Request 也就是在拦截器中可以获取到的 Request:

okhttp3.Request create(Object[] args) throws IOException {
   ...

    return requestBuilder.get()
        .tag(Invocation.class, new Invocation(method, argumentList))
        .build();
  }

在创建 Request 的时候发现 Retrofit 为 Request 添加了一个 tag: evernotecid://147782CD-B655-4EB8-8690-0C53A55A9C43/appyinxiangcom/24089108/ENResource/p86

tag 的 value 中包含了所调用的 method 也就是searchRepos,有了它就可以获取到上面的注解了。

解决了关键问题之后我们就可以很轻松的扩展注解了。

实现流程

自定义 Mock 注解

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mock(val value: String = "", val assets: String = "", val url: String = "")

我们定义了三个参数

  • value:json 字符串
  • assets:assets 下的文件名
  • url:mock url

自定义 Mock 拦截器

/**
 * Description: mock 拦截器
 *
 * @author wangzhen
 * @version 1.0
 */
class MockInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()

        // 获取 retrofit 中定义 method
        val retrofitMethod = getRetrofitMethod(request) ?: return chain.proceed(request)

        // 根据 method 获取它的 mock 注解
        val mock = getMock(retrofitMethod) ?: return chain.proceed(request)

        // 获取 mockUrl 进行重定向
        if (mock.url.isNotEmpty()) {
            return chain.proceed(mockRequest(request, mock.url))
        }

        // 根据 mock 注解获取 mockData
        val mockData = getMockData(mock)

        // 如果 mockData 不为空就短路拦截器
        if (!mockData.isNullOrEmpty()) {
            return Response.Builder()
                .protocol(Protocol.HTTP_1_0)
                .code(200)
                .request(request)
                .message("ok")
                .body(mockData.toResponseBody(null))
                .build()
        }
        return chain.proceed(request)
    }

    private fun mockRequest(request: Request, mockUrl: String): Request {
        return request.newBuilder().url(mockUrl).build()
    }
}

MockUtils如下:

package com.wangzhen.baselib.http.mock

import android.util.Log
import com.wangzhen.baselib.application.GlobalApplication
import com.wangzhen.baselib.utils.readAssets
import okhttp3.Request
import retrofit2.Invocation
import java.lang.reflect.Method

/**
 * Description: mock 相关工具工具
 *
 * @author wangzhen
 * @version 1.0
 */

fun getRetrofitMethod(request: Request): Method? {
    return request.tag(Invocation::class.java)?.method() ?: return null
}

fun getMock(method: Method): Mock? {
    val annotations = method.annotations
    for (annotation in annotations) {
        if (annotation is Mock) {
            return annotation
        }
    }
    return null
}

fun getMockData(mock: Mock): String? {
    val value = mock.value
    if (value.isNotEmpty()) {
        return value
    }
    val assetsUrl = mock.assets
    if (assetsUrl.isNotEmpty()) {
        return readAssets(GlobalApplication.getAppContext(), assetsUrl)
    }
    return null
}

可以看到关键的操作就是根据 request 的 tag 获取到当前调用的 method,然后获得 method 的 mock 注解:

val method = request.tag(Invocation::class.java).method()
val annotations = method.annotations

Retrofit 版本

不同的版本源码不同,这里使用的是:

com.squareup.retrofit2:retrofit:2.6.2

其他实现方式

调研过其他的实现方式,与现在的实现方式相比要复杂不少,感兴趣的可以参考下: 使用反射获取到 request 的 url,然后在拦截器中根据 url 匹配对应的注解,这种方式使用了大量的反射

自定义 CallAdapter.Factory 保存 ”url - 注解“ 的关系对然后在拦截器中处理,这种方式需要修改 retrofit 的源码

源码

查看 basiclib 下的 http 模块