一起宏观地分析 Retrofit 这么直观易用的原理(一)

Feb 01, 2018

两年前第一眼见到 Retrofit,我像是看到了世外桃园。Retrofit 使用注解的方式让 Rest 请求变得及其直观、易用。用桥接的方式把 Converter、OkHttpClient、CallAdapter 等组件组合在一起,形成一个灵活多变的框架。这个令人血脉喷张的框架,让人忍不住扒开它的外衣,一窥它的骨骼肌肉。

今日得闲,饮茶的同时,我们来看一看,到底是什么造就了它。

同样,我们先简单回顾一下 Retrofit。

Retrofit

A type-safe HTTP client for Android and Java

使用方法 DBC 三步走,定义(Declare)/创建(Build)/引用(Call)

1. Declare
定义接口,说明 API 参数格式

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

2. Build
使用 Retrofit 创建 GitHubService 实例

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

3. Call
填入参数,调用接口

Call<List<Repo>> repos = service.listRepos("octocat");

这样我们就完成了对接口 https://api.github.com/users/octocat/repos的一次请求。

实现原理

上面这段代码看起来很奇怪,我们没有实现 GitHubService 接口,但却能直接调用 service.listRepos(…) 方法。
可想而知,自然是 Retrofit.create(..) 做了手脚。
那么问题来了:「Retrofit.create(..) 对 GitHubService 做了什么?」

瞎猜一下:

  • 使用动态代理让我们能直接调用接口方法
  • 通过注解获取 listRepos 接口的 Method 和 参数
  • 新建 Retrofit 时添加了 baseUrl(...)

注:动态代理技术两个很重要的应用:热更新和 AOP。大家应该都了解下。

至此,入口有了,url 有了,方法是 Get, 参数也齐活了。

究竟事实怎样,打开源码一探究竟:

一看目录结构

从包名可以看出来一些东西,retrofit 应该是核心代码,retrofit-adapters 和 retrofit-converters 是核心类 CallAdapter 和 Converter 在不同框架的实现,retrofit-mock 没看出来,猜测是测试相关,先放着。

retrofit
└── src
retrofit-adapters
├── guava
├── java8
├── rxjava
├── rxjava2
└── scala
retrofit-converters
├── gson
├── guava
├── jackson
├── java8
├── jaxb
├── moshi
├── protobuf
├── scalars
├── simplexml
└── wire
retrofit-mock
└── src

我们来重点看看 retrofit 这个包

.
├── BuiltInConverters.java
├── Call.java
├── CallAdapter.java
├── Callback.java
├── Converter.java
├── DefaultCallAdapterFactory.java
├── ExecutorCallAdapterFactory.java
├── HttpException.java
├── OkHttpCall.java
├── ParameterHandler.java
├── Platform.java
├── RequestBuilder.java
├── Response.java
├── Retrofit.java
├── ServiceMethod.java
├── Utils.java
├── http
│   ├── Body.java
│   ├── DELETE.java
│   ├── Field.java
│   ├── FieldMap.java
│   ├── FormUrlEncoded.java
│   ├── GET.java
│   ├── HEAD.java
│   ├── HTTP.java
│   ├── Header.java
│   ├── HeaderMap.java
│   ├── Headers.java
│   ├── Multipart.java
│   ├── OPTIONS.java
│   ├── PATCH.java
│   ├── POST.java
│   ├── PUT.java
│   ├── Part.java
│   ├── PartMap.java
│   ├── Path.java
│   ├── Query.java
│   ├── QueryMap.java
│   ├── QueryName.java
│   ├── Streaming.java
│   ├── Url.java
│   └── package-info.java
└── package-info.java

包 http 里定义了所有用到的注解,代码很简单。
用上面用到的 GET 方法举个栗子:

/** Make a GET request. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
  /**
   * A relative or absolute path, or full URL of the endpoint. This value is optional if the first
   * parameter of the method is annotated with {@link Url @Url}.
   * <p>
   * See {@linkplain retrofit2.Retrofit.Builder#baseUrl(HttpUrl) base URL} for details of how
   * this is resolved against a base URL to create the full endpoint URL.
   */
  String value() default "";
}

不到 20 行,真是精简呢。一半还是注释。
@Target(METHOD) 限定注解只能用在方法上
@Retention(RUNTIME)说这个注解在运行时有效

关于注解的详细用法不是今天的主要内容,就不再赘述了。

什么是注解?
注解是 Java 5 的一个新特性。注解是插入你代码中的一种注释或者说是一种元数据(meta data)。这些注解信息可以在编译期使用预编译工具进行处理(pre-compiler tools),也可以在运行期使用 Java 反射机制进行处理。

二看入口代码

还记得我们的疑惑是什么吗?
「Retrofit.create(..) 对 GitHubService 做了什么?」
入口就是这个方法,我们来看看它到底做了什么。

@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.adapt(okHttpCall);
          }
        });
  }

果然没错,Retrofit 做了动态代理,返回给我们 Proxy.newProxyInstance()方法返回的结果。我们一步步来看:

  • 最开始 4 行应该是在 validate 什么事情,不用看
  • return (T) Proxy.newProxyInstance() 为 Service 做了代理。你可以理解为在运行时动态地帮 Service 做了实现
  • Service 的每个方法调用都会通过这里的 invoke() 方法
  • invock() 方法相当于为 Service 方法做了分发

什么是动态代理?
Using Java Reflection you create dynamic implementations of interfaces at runtime. You do so using the class java.lang.reflect.Proxy. The name of this class is why I refer to these dynamic interface implementations as dynamic proxies. Dynamic proxies can be used for many different purposes, e.g. database connection and transaction management, dynamic mock objects for unit testing, and other AOP-like method intercepting purposes.
大意是 Proxy 这个类可以帮助你在运行时动态地创建接口的实现。AOP 等场景中,动态代理应用十分广泛。

三追踪代码

上面两步,我们从宏观上认识了使用注解+接口进行网络请求的原理。
那接下来 Retrofit 具体做了哪些工作,我想大家应该心里有数了。
无非是:

  1. 判断调用了 Service 的哪个方法
  2. 获取注解和参数,拼接目标 URL
  3. 使用相应的方法做网络请求

下一个疑问就出来了:

「invoke(..) 方法做了哪些工作?」

于是从这个入口,继续追踪每一个方法。我们便能理解所有的原理了。

我们今天只是从宏观上阐述,就不深追具体逻辑了。

四拓展思路

上面我们分析了 Retrofit 进行简单的网络请求的原理,但 Retrofit 的强大远不止如此。

想想在 Retrofit 使用过程中还有哪些及其便利之处?

自动解析Json
增加全局参数
在不同的网络请求库之间切换
拦截请求输出日志
...

这些不同的组件和功能又是如何和 Retrofit 的本体——网络请求结合在一起的呢?


今天的下午茶已经喝完了,不宜久坐。
原理真相究竟如何,我们下次再一起探个究竟。

/* 看板娘 */