资讯专栏INFORMATION COLUMN

深入浅出 Retrofit,这么牛逼的框架你们还不来看看?

xumenger / 1085人阅读

摘要:注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是来完成的。多说无益,不要来段代码陶醉一下。注意在选择合适的时,主要依赖于需要转换的对象类型,在添加时,注意支持的类型的包含关系以及其顺序。

Android 开发中,从原生的 HttpUrlConnection 到经典的 Apache 的 HttpClient,再到对前面这些网络基础框架的封装,比如 VolleyAsync Http Client,Http 相关开源框架的选择还是很多的,其中由著名的 Square 公司开源的 Retrofit 更是以其简易的接口配置、强大的扩展支持、优雅的代码结构受到大家的追捧。也正是由于 Square 家的框架一如既往的简洁优雅,所以我一直在想,Square 公司是不是只招处女座的程序员?

1、初识 Retrofit

单从 Retrofit 这个单词,你似乎看不出它究竟是干嘛的,当然,我也看不出来 :)逃。。

Retrofitting refers to the addition of new technology or features to older systems.

--From Wikipedia

于是我们就明白了,冠以 Retrofit 这个名字的这个家伙,应该是某某某的 『Plus』 版本了。

1.1 Retrofit 概览

Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是 Retrofit 来完成的。Retrofit 2.0 开始内置 OkHttp,前者专注于接口的封装,后者专注于网络请求的高效,二者分工协作,宛如古人的『你耕地来我织布』,小日子别提多幸福了。

我们的应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作,在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,后者根据用户的需求对结果进行解析的过程。

讲到这里,你就会发现所谓 Retrofit,其实就是 Retrofitting OkHttp 了。

1.2 Hello Retrofit

多说无益,不要来段代码陶醉一下。使用 Retrofit 非常简单,首先你需要在你的 build.gradle 中添加依赖:

compile "com.squareup.retrofit2:retrofit:2.0.2"

你一定是想要访问 GitHub 的 api 对吧,那么我们就定义一个接口:

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

接口当中的 listRepos 方法,就是我们想要访问的 api 了:

https://api.github.com/users/{user}/repos

其中,在发起请求时, {user} 会被替换为方法的第一个参数 user

好,现在接口有了,我们要构造 Retrofit 了:

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

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

这里的 service 就好比我们的快递哥,还是往返的那种哈~

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

发请求的代码就像前面这一句,返回的 repos 其实并不是真正的数据结果,它更像一条指令,你可以在合适的时机去执行它:

// 同步调用
List data = repos.execute(); 

// 异步调用
repos.enqueue(new Callback>() {
            @Override
            public void onResponse(Call> call, Response> response) {
                List data = response.body();
            }

            @Override
            public void onFailure(Call> call, Throwable t) {
                t.printStackTrace();
            }
        });

啥感觉?有没有突然觉得请求接口就好像访问自家的方法一样简单?呐,前面我们看到的,就是 Retrofit 官方的 demo 了。你以为这就够了?噗~怎么可能。。

1.3 Url 配置

Retrofit 支持的协议包括 GET/POST/PUT/DELETE/HEAD/PATCH,当然你也可以直接用 HTTP 来自定义请求。这些协议均以注解的形式进行配置,比如我们已经见过 GET 的用法:

  @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);

这些注解都有一个参数 value,用来配置其路径,比如示例中的 users/{user}/repos,我们还注意到在构造 Retrofit 之时我们还传入了一个 baseUrl("https://api.github.com/"),请求的完整 Url 就是通过 baseUrl 与注解的 value(下面称 "path" ) 整合起来的,具体整合的规则如下:

path 是绝对路径的形式:

path = "/apath", baseUrl = "http://host:port/a/b"
Url = "http://host:port/apath"

path 是相对路径,baseUrl 是目录形式:

path = "apath", baseUrl = "http://host:port/a/b/"
Url = "http://host:port/a/b/apath"

path 是相对路径,baseUrl 是文件形式:

path = "apath", baseUrl = "http://host:port/a/b"
Url = "http://host:port/a/apath"

path 是完整的 Url:

path = "http://host:port/aa/apath", baseUrl = "http://host:port/a/b"
Url = "http://host:port/aa/apath"

建议采用第二种方式来配置,并尽量使用同一种路径形式。如果你在代码里面混合采用了多种配置形式,恰好赶上你哪天头晕眼花,信不信分分钟写一堆 bug 啊哈哈。

1.4 参数类型

发请求时,需要传入参数,Retrofit 通过注解的形式令 Http 请求的参数变得更加直接,而且类型安全。

1.4.1 Query & QueryMap
@GET("/list")
Call list(@Query("page") int page);

Query 其实就是 Url 中 "?" 后面的 key-value,比如:

http://www.println.net/?cate=android

这里的 cate=android 就是一个 Query,而我们在配置它的时候只需要在接口方法中增加一个参数,即可:

interface PrintlnServer{
    @GET("/")
    Call cate(@Query("cate") String cate);
}

这时候你肯定想,如果我有很多个 Query,这么一个个写岂不是很累?而且根据不同的情况,有些字段可能不传,这与方法的参数要求显然也不相符。于是,打群架版本的 QueryMap 横空出世了,使用方法很简单,我就不多说了。

1.4.2 Field & FieldMap

其实我们用 POST 的场景相对较多,绝大多数的服务端接口都需要做加密、鉴权和校验,GET 显然不能很好的满足这个需求。使用 POST 提交表单的场景就更是刚需了,怎么提呢?

   @FormUrlEncoded
   @POST("/")
   Call example(
       @Field("name") String name,
       @Field("occupation") String occupation);

其实也很简单,我们只需要定义上面的接口就可以了,我们用 Field 声明了表单的项,这样提交表单就跟普通的函数调用一样简单直接了。

等等,你说你的表单项不确定个数?还是说有很多项你懒得写?Field 同样有个打群架的版本——FieldMap,赶紧试试吧~~

1.4.3 Part & PartMap

这个是用来上传文件的。话说当年用 HttpClient 上传个文件老费劲了,一会儿编码不对,一会儿参数错误(也怪那时段位太低吧TT)。。。可是现在不同了,自从有了 Retrofit,妈妈再也不用担心文件上传费劲了~~~

public interface FileUploadService {  
    @Multipart
    @POST("upload")
    Call upload(@Part("description") RequestBody description,
                              @Part MultipartBody.Part file);
}

如果你需要上传文件,和我们前面的做法类似,定义一个接口方法,需要注意的是,这个方法不再有 @FormUrlEncoded 这个注解,而换成了 @Multipart,后面只需要在参数中增加 Part 就可以了。也许你会问,这里的 PartField 究竟有什么区别,其实从功能上讲,无非就是客户端向服务端发起请求携带参数的方式不同,并且前者可以携带的参数类型更加丰富,包括数据流。也正是因为这一点,我们可以通过这种方式来上传文件,下面我们就给出这个接口的使用方法:

//先创建 service
FileUploadService service = retrofit.create(FileUploadService.class);

//构建要上传的文件
File file = new File(filename);
RequestBody requestFile =
        RequestBody.create(MediaType.parse("application/otcet-stream"), file);

MultipartBody.Part body =
        MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);

String descriptionString = "This is a description";
RequestBody description =
        RequestBody.create(
                MediaType.parse("multipart/form-data"), descriptionString);

Call call = service.upload(description, body);
call.enqueue(new Callback() {
  @Override
  public void onResponse(Call call,
                         Response response) {
    System.out.println("success");
  }

  @Override
  public void onFailure(Call call, Throwable t) {
    t.printStackTrace();
  }
});

在实验时,我上传了一个只包含一行文字的文件:

Visit me: http://www.println.net

那么我们去服务端看下我们的请求是什么样的:

HEADERS

Accept-Encoding: gzip
Content-Length: 470
Content-Type: multipart/form-data; boundary=9b670d44-63dc-4a8a-833d-66e45e0156ca
User-Agent: okhttp/3.2.0
X-Request-Id: 9d70e8cc-958b-4f42-b979-4c1fcd474352
Via: 1.1 vegur
Host: requestb.in
Total-Route-Time: 0
Connection: close
Connect-Time: 0

FORM/POST PARAMETERS

description: This is a description

RAW BODY

--9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21

This is a description
--9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name="aFile"; filename="uploadedfile.txt"
Content-Type: application/otcet-stream
Content-Length: 32

Visit me: http://www.println.net
--9b670d44-63dc-4a8a-833d-66e45e0156ca--

我们看到,我们上传的文件的内容出现在请求当中了。如果你需要上传多个文件,就声明多个 Part 参数,或者试试 PartMap

1.5 Converter,让你的入参和返回类型丰富起来
1.5.1 RequestBodyConverter

1.4.3 当中,我为大家展示了如何用 Retrofit 上传文件,这个上传的过程其实。。还是有那么点儿不够简练,我们只是要提供一个文件用于上传,可我们前后构造了三个对象:

天哪,肯定是哪里出了问题。实际上,Retrofit 允许我们自己定义入参和返回的类型,不过,如果这些类型比较特别,我们还需要准备相应的 Converter,也正是因为 Converter 的存在, Retrofit 在入参和返回类型上表现得非常灵活。

下面我们把刚才的 Service 代码稍作修改:

public interface FileUploadService {  
    @Multipart
    @POST("upload")
    Call upload(@Part("description") RequestBody description,
        //注意这里的参数 "aFile" 之前是在创建 MultipartBody.Part 的时候传入的
        @Part("aFile") File file);
}

现在我们把入参类型改成了我们熟悉的 File,如果你就这么拿去发请求,服务端收到的结果会让你哭了的。。。

RAW BODY

--7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21

This is a description
--7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name="aFile"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35

// 注意这里!!之前是文件的内容,现在变成了文件的路径
{"path":"samples/uploadedfile.txt"} 
--7d24e78e-4354-4ed4-9db4-57d799b6efb7--

服务端收到了一个文件的路径,它肯定会觉得

好了,不闹了,这明显是 Retrofit 在发现自己收到的实际入参是个 File 时,不知道该怎么办,情急之下给 toString了,而且还是个 JsonString(后来查证原来是使用了 GsonRequestBodyConverter。。)。

接下来我们就自己实现一个 FileRequestBodyConverter

  static class FileRequestBodyConverterFactory extends Converter.Factory {
    @Override
    public Converter requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return new FileRequestBodyConverter();
    }
  }

  static class FileRequestBodyConverter implements Converter {

    @Override
    public RequestBody convert(File file) throws IOException {
      return RequestBody.create(MediaType.parse("application/otcet-stream"), file);
    }
  }

在创建 Retrofit 的时候记得配置上它:

addConverterFactory(new FileRequestBodyConverterFactory())

这样,我们的文件内容就能上传了。来,看下结果吧:

RAW BODY

--25258f46-48b0-4a6b-a617-15318c168ed4
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21

This is a description
--25258f46-48b0-4a6b-a617-15318c168ed4
//注意看这里,filename 没了
Content-Disposition: form-data; name="aFile"
//多了这一句
Content-Transfer-Encoding: binary
Content-Type: application/otcet-stream
Content-Length: 32

Visit me: http://www.println.net
--25258f46-48b0-4a6b-a617-15318c168ed4--

文件内容成功上传了,当然其中还存在一些问题,这个目前直接使用 Retrofit 的 Converter 还做不到,原因主要在于我们没有办法通过 Converter 直接将 File 转换为 MultiPartBody.Part,如果想要做到这一点,我们可以对 Retrofit 的源码稍作修改,这个我们后面再谈。

1.5.2 ResponseBodyConverter

前面我们为大家简单示例了如何自定义 RequestBodyConverter,对应的,Retrofit 也支持自定义 ResponseBodyConverter

我们再来看下我们定义的接口:

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

返回值的类型为 List,而我们直接拿到的原始返回肯定就是字符串(或者字节流),那么这个返回值类型是怎么来的呢?首先说明的一点是,GitHub 的这个 api 返回的是 Json 字符串,也就是说,我们需要使用 Json 反序列化得到 List,这其中用到的其实是 GsonResponseBodyConverter

问题来了,如果请求得到的 Json 字符串与返回值类型不对应,比如:

接口返回的 Json 字符串:

{"err":0, "content":"This is a content.", "message":"OK"}

返回值类型

class Result{
    int code;//等价于 err
    String body;//等价于 content
    String msg;//等价于 message
}

哇,这时候肯定有人想说,你是不是脑残,偏偏跟服务端对着干?哈哈,我只是示例嘛,而且在生产环境中,你敢保证这种情况不会发生??

这种情况下, Gson 就是再牛逼,也只能默默无语俩眼泪了,它哪儿知道字段的映射关系怎么这么任性啊。好,现在让我们自定义一个 Converter 来解决这个问题吧!

  static class ArbitraryResponseBodyConverterFactory extends Converter.Factory{
    @Override
    public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
      return super.responseBodyConverter(type, annotations, retrofit);
    }
  }
  
  static class ArbitraryResponseBodyConverter implements Converter{

    @Override
    public Result convert(ResponseBody value) throws IOException {
      RawResult rawResult = new Gson().fromJson(value.string(), RawResult.class);
      Result result = new Result();
      result.body = rawResult.content;
      result.code = rawResult.err;
      result.msg = rawResult.message;
      return result;
    }
  }
  
  static class RawResult{
    int err;
    String content;
    String message;
  }

当然,别忘了在构造 Retrofit 的时候添加这个 Converter,这样我们就能够愉快的让接口返回 Result 对象了。

注意!!Retrofit 在选择合适的 Converter 时,主要依赖于需要转换的对象类型,在添加 Converter 时,注意 Converter 支持的类型的包含关系以及其顺序。

2、Retrofit 原理剖析

前一个小节我们把 Retrofit 的基本用法和概念介绍了一下,如果你的目标是学会如何使用它,那么下面的内容你可以不用看了。

不过呢,我就知道你不是那种浅尝辄止的人!这一节我们主要把注意力放在 Retrofit 背后的魔法上面~~

2.1 是谁实际上完成了接口请求的处理?

前面讲了这么久,我们始终只看到了我们自己定义的接口,比如:

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

而真正我使用的时候肯定不能是接口啊,这个神秘的家伙究竟是谁?其实它是 Retrofit 创建的一个代理对象了,这里涉及点儿 Java 的动态代理的知识,直接来看代码:

  public  T create(final Class service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    //这里返回一个 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, 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);
            }
            //DefaultMethod 是 Java 8 的概念,是定义在 interface 当中的有实现的方法
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            //每一个接口最终实例化成一个 ServiceMethod,并且会缓存
            ServiceMethod serviceMethod = loadServiceMethod(method);
            
            //由此可见 Retrofit 与 OkHttp 完全耦合,不可分割
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            //下面这一句当中会发起请求,并解析服务端返回的结果
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

简单的说,在我们调用 GitHubService.listRepos 时,实际上调用的是这里的 InvocationHandler.invoke 方法~~

2.2 来一发完整的请求处理流程

前面我们已经看到 Retrofit 为我们构造了一个 OkHttpCall ,实际上每一个 OkHttpCall 都对应于一个请求,它主要完成最基础的网络请求,而我们在接口的返回中看到的 Call 默认情况下就是 OkHttpCall 了,如果我们添加了自定义的 callAdapter,那么它就会将 OkHttp 适配成我们需要的返回值,并返回给我们。

先来看下 Call 的接口:

public interface Call extends Cloneable {
  //同步发起请求
  Response execute() throws IOException;
  //异步发起请求,结果通过回调返回
  void enqueue(Callback callback);
  boolean isExecuted();
  void cancel();
  boolean isCanceled();
  Call clone();
  //返回原始请求
  Request request();
}

我们在使用接口时,大家肯定还记得这一句:

Call> repos = service.listRepos("octocat");
List data = repos.execute(); 

这个 repos 其实就是一个 OkHttpCall 实例,execute 就是要发起网络请求。

OkHttpCall.execute

  @Override public Response execute() throws IOException {
    //这个 call 是真正的 OkHttp 的 call,本质上 OkHttpCall 只是对它做了一层封装
    okhttp3.Call call;

    synchronized (this) {
      //处理重复执行的逻辑
      if (executed) throw new IllegalStateException("Already executed.");
      executed = true;

      if (creationFailure != null) {
        if (creationFailure instanceof IOException) {
          throw (IOException) creationFailure;
        } else {
          throw (RuntimeException) creationFailure;
        }
      }
      
      call = rawCall;
      if (call == null) {
        try {
          call = rawCall = createRawCall();
        } catch (IOException | RuntimeException e) {
          creationFailure = e;
          throw e;
        }
      }
    }

    if (canceled) {
      call.cancel();
    }
    //发起请求,并解析结果
    return parseResponse(call.execute());
  }

我们看到 OkHttpCall 其实也是封装了 okhttp3.Call,在这个方法中,我们通过 okhttp3.Call 发起了进攻,额,发起了请求。有关 OkHttp 的内容,我在这里就不再展开了。

parseResponse 主要完成了由 okhttp3.Responseretrofit.Response 的转换,同时也处理了对原始返回的解析:

  Response parseResponse(okhttp3.Response rawResponse) throws IOException {
    ResponseBody rawBody = rawResponse.body();

    //略掉一些代码
    try {
      //在这里完成了原始 Response 的解析,T 就是我们想要的结果,比如 GitHubService.listRepos 的 List
      T body = serviceMethod.toResponse(catchingBody);
      return Response.success(body, rawResponse);
    } catch (RuntimeException e) {
      // If the underlying source threw an exception, propagate that rather than indicating it was
      // a runtime exception.
      catchingBody.throwIfCaught();
      throw e;
    }
  }

至此,我们就拿到了我们想要的数据~~

2.3 结果适配,你是不是想用 RxJava?

前面我们已经提到过 CallAdapter 的事儿,默认情况下,它并不会对 OkHttpCall 实例做任何处理:

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory();

  @Override
  public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    ... 毫不留情的省略一些代码 ...
    return new CallAdapter>() {
      ... 省略一些代码 ...

      @Override public  Call adapt(Call call) {
        //看这里,直接把传入的 call 返回了
        return call;
      }
    };
  }
}

现在的需求是,我想要接入 RxJava,让接口的返回结果改为 Observable

  public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Observable> contributors(
        @Path("owner") String owner,
        @Path("repo") String repo);
  }

可不可以呢?当然是可以的,只需要提供一个 Adapter,将 OkHttpCall 转换为 Observable 即可呀!Retrofit 的开发者们早就想到了这个问题,并且为我们提供了相应的 Adapter:

RxJavaCallAdapterFactory

我们只需要在构造 Retrofit 时,添加它:

addCallAdapterFactory(RxJavaCallAdapterFactory.create())

这样我们的接口就可以以 RxJava 的方式工作了。

好,歇会儿,抽一袋烟。。。

接着我们搞清楚 RxJavaCallAdapterFactory 是怎么工作的,首先让我们来看下 CallAdapter 的接口:

public interface CallAdapter {
  /*
   *返回 Http 返回解析后的类型。需要注意的是这个并不是接口的返回类型,
   *而是接口返回类型中的泛型参数的实参。
   */
  Type responseType();
  /*
   * T 是我们需要转换成的接口返回类型,参数 call 其实最初就是 OkHttpCall 的实例
   * 在这里 T 其实是 RxJava 支持的类型,比如 Observable
   */
   T adapt(Call call);

  //我们需要将 Factory 的子类对应的实例在构造 Retrofit 时添加到其中。
  abstract class Factory {
    //根据接口的返回类型(Observable),注解类型等等来判断是否是当前 Adapter 支持的类型,不是则返回null
    public abstract CallAdapter get(Type returnType, Annotation[] annotations,
        Retrofit retrofit);

    //获取指定 index 的泛型参数的上限,比如对于 Map,index为 1 的参数上限是 Number
    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }

    /*
     * 获取原始类型,比如 List 返回 List.class,这里传入的 type 情况可能比较复杂,因此不能直接当做
     * Class 去做判断。这个方法在判断类型是否为支持的类型时经常用到。
    protected static Class getRawType(Type type) {
      return Utils.getRawType(type);
    }
  }
}

代码中做了较为详细的注释,简单来说,我们只需要实现 CallAdapter 类来提供具体的适配逻辑,并实现相应的 Factory,用来将当前的 CallAdapter注册到 Retrofit 当中,并在 Factory.get 方法中根据类型来返回当前的 CallAdapter即可。知道了这些,我们再来看 RxJavaCallAdapterFactory

public final class RxJavaCallAdapterFactory extends CallAdapter.Factory {

  ... 请叫我省略君,为了省地方,一个都不放过! ...

  @Override
  public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    //注意下面的代码主要是判断 returnType 是否为 RxJava 支持的类型
    Class rawType = getRawType(returnType);
    String canonicalName = rawType.getCanonicalName();
    boolean isSingle = "rx.Single".equals(canonicalName);
    boolean isCompletable = "rx.Completable".equals(canonicalName);
    if (rawType != Observable.class && !isSingle && !isCompletable) {
      return null;
    }
    ... 这里省略掉的代码主要是根据返回类型获取合适的 Adapter ...
    return callAdapter;
  }

  ... 我又来了,继续略去一些代码 ...

  static final class SimpleCallAdapter implements CallAdapter> {
    private final Type responseType;
    private final Scheduler scheduler;

    SimpleCallAdapter(Type responseType, Scheduler scheduler) {
      this.responseType = responseType;
      this.scheduler = scheduler;
    }

    @Override public Type responseType() {
      return responseType;
    }

    @Override public  Observable adapt(Call call) {
      //在这里创建需作为返回值的 Observable 实例,并持有 call 实例
      //可以想象得到,在 Observable.subscribe 触发时, call.execute 将会被调用
      Observable observable = Observable.create(new CallOnSubscribe<>(call)) 
          .lift(OperatorMapResponseToBodyOrError.instance());
      if (scheduler != null) {
        return observable.subscribeOn(scheduler);
      }
      return observable;
    }
  }

  ... 略去一些代码 ...
}

RxJavaCallAdapterFactory 提供了不止一种 Adapter,但原理大同小异,有兴趣的读者可以自行参阅其源码。

至此,我们已经对 CallAdapter 的机制有了一个清晰的认识了。

3、几个进阶玩法

前面我们已经介绍了很多东西了。。可,挖掘机专业的同学们,你们觉得这就够了么?当然是不够!

3.1 继续简化文件上传的接口

在 1.5.1 当中我们曾试图简化文件上传接口的使用,尽管我们已经给出了相应的 File -> RequestBodyConverter,不过基于 Retrofit本身的限制,我们还是不能像直接构造 MultiPartBody.Part 那样来获得更多的灵活性。这时候该怎么办?当然是 Hack~~

首先明确我们的需求:

文件的 Content-Type 需要更多的灵活性,不应该写死在 Converter 当中,可以的话,最好可以根据文件的扩展名来映射出来对应的 Content-Type, 比如 image.png -> image/png;

在请求的数据中,能够正常携带 filename 这个字段。

为此,我增加了一套完整的参数解析方案:

增加任意类型转换的 Converter,这一步主要是满足后续我们直接将入参类型转换为 MultiPartBody.Part 类型:

public interface Converter {
  ...
  
  abstract class Factory {
    ...
    //返回一个满足条件的不限制类型的 Converter
    public Converter arbitraryConverter(Type originalType,
          Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit){
      return null;
    }
  }
}

需要注意的是,Retrofit 类当中也需要增加相应的方法:

  public  Converter arbitraryConverter(Type orignalType,
                                                   Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
    return nextArbitraryConverter(null, orignalType, convertedType, parameterAnnotations, methodAnnotations);
  }

  public  Converter nextArbitraryConverter(Converter.Factory skipPast,
                                Type type, Type convertedType,  Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
    checkNotNull(type, "type == null");
    checkNotNull(parameterAnnotations, "parameterAnnotations == null");
    checkNotNull(methodAnnotations, "methodAnnotations == null");

    int start = converterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = converterFactories.size(); i < count; i++) {
      Converter.Factory factory = converterFactories.get(i);
      Converter converter =
              factory.arbitraryConverter(type, convertedType, parameterAnnotations, methodAnnotations, this);
      if (converter != null) {
        //noinspection unchecked
        return (Converter) converter;
      }
    }
    return null;
  }

再给出 arbitraryConverter 的具体实现:

public class TypedFileMultiPartBodyConverterFactory extends Converter.Factory {
    @Override
    public Converter arbitraryConverter(Type originalType, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        if (originalType == TypedFile.class && convertedType == MultipartBody.Part.class) {
            return new FileRequestBodyConverter();
        }
        return null;
    }
}
public class TypedFileMultiPartBodyConverter implements Converter {

    @Override
    public MultipartBody.Part convert(TypedFile typedFile) throws IOException {
        RequestBody requestFile =
                RequestBody.create(typedFile.getMediaType(), typedFile.getFile());
        return MultipartBody.Part.createFormData(typedFile.getName(), typedFile.getFile().getName(), requestFile);
    }
}
public class TypedFile {
    private MediaType mediaType;
    private String name;
    private File file;

    public TypedFile(String name, String filepath){
        this(name, new File(filepath));
    }

    public TypedFile(String name, File file) {
        this(MediaType.parse(MediaTypes.getMediaType(file)), name, file);
    }

    public TypedFile(MediaType mediaType, String name, String filepath) {
        this(mediaType, name, new File(filepath));
    }

    public TypedFile(MediaType mediaType, String name, File file) {
        this.mediaType = mediaType;
        this.name = name;
        this.file = file;
    }

    public String getName() {
        return name;
    }

    public MediaType getMediaType() {
        return mediaType;
    }

    public File getFile() {
        return file;
    }
}

在声明接口时,@Part 不要传入参数,这样 RetrofitServiceMethod.Builder.parseParameterAnnotation 方法中解析 Part时,就会认为我们传入的参数为 MultiPartBody.Part 类型(实际上我们将在后面自己转换)。那么解析的时候,我们拿到前面定义好的 Converter,构造一个 ParameterHandler

...
} else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
    return ParameterHandler.RawPart.INSTANCE;
} else {
    Converter converter =
            retrofit.arbitraryConverter(type, MultipartBody.Part.class, annotations, methodAnnotations);
    if(converter == null) {
        throw parameterError(p,
                "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
    }
    return new ParameterHandler.TypedFileHandler((Converter) converter);
}
...

ParameterHandler.TypedFileHandler的实现:

  static final class TypedFileHandler extends ParameterHandler{

    private final Converter converter;

    TypedFileHandler(Converter converter) {
      this.converter = converter;
    }

    @Override
    void apply(RequestBuilder builder, TypedFile value) throws IOException {
      if(value != null){
        builder.addPart(converter.convert(value));
      }
    }
  }

这时候再看我们的接口声明:

  public interface FileUploadService {
    @Multipart
    @POST("upload")
    Call upload(@Part("description") RequestBody description,
                              @Part TypedFile typedFile);
  }

以及使用方法:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://www.println.net/")
    .addConverterFactory(new TypedFileMultiPartBodyConverterFactory())
    .addConverterFactory(GsonConverterFactory.create())
    .build();

FileUploadService service = retrofit.create(FileUploadService.class);
TypedFile typedFile = new TypedFile("aFile", filename);
String descriptionString = "This is a description";
RequestBody description =
        RequestBody.create(
                MediaType.parse("multipart/form-data"), descriptionString);

Call call = service.upload(description, typedFile);
call.enqueue(...);

至此,我们已经通过自己的双手,让 Retrofit 的点亮了自定义上传文件的技能,风骚等级更上一层楼!

3.2 Mock Server

我们在开发过程中,经常遇到服务端不稳定的情况,测试开发环境,这是难免的。于是我们需要能够模拟网络请求来调试我们的客户端逻辑,Retrofit 自然是支持这个功能的。

真是太贴心,Retrofit 提供了一个 MockServer 的功能,可以在几乎不改动客户端原有代码的前提下,实现接口数据返回的自定义,我们在自己的工程中增加下面的依赖:

compile "com.squareup.retrofit2:retrofit-mock:2.0.2

还是先让我们来看看官方 demo,首先定义了一个 GituHb api,好熟悉的感觉:

  public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Call> contributors(
        @Path("owner") String owner,
        @Path("repo") String repo);
  }

这就是我们要请求的接口了,怎么 Mock 呢?

定义一个接口实现类 MockGitHub,我们可以看到,所有我们需要请求的接口都在这里得到了实现,也就是说,我们待会儿调用 GitHub 的 api 时,实际上是访问 MockGitHub 的方法:

static final class MockGitHub implements GitHub {
    private final BehaviorDelegate delegate;
    private final Map>> ownerRepoContributors;

    public MockGitHub(BehaviorDelegate delegate) {
      this.delegate = delegate;
      ownerRepoContributors = new LinkedHashMap<>();

      // Seed some mock data.
      addContributor("square", "retrofit", "John Doe", 12);
      addContributor("square", "retrofit", "Bob Smith", 2);
      addContributor("square", "retrofit", "Big Bird", 40);
      addContributor("square", "picasso", "Proposition Joe", 39);
      addContributor("square", "picasso", "Keiser Soze", 152);
    }

    @Override public Call> contributors(String owner, String repo) {
      List response = Collections.emptyList();
      Map> repoContributors = ownerRepoContributors.get(owner);
      if (repoContributors != null) {
        List contributors = repoContributors.get(repo);
        if (contributors != null) {
          response = contributors;
        }
      }
      return delegate.returningResponse(response).contributors(owner, repo);
    }

    public void addContributor(String owner, String repo, String name, int contributions) {
      Map> repoContributors = ownerRepoContributors.get(owner);
      if (repoContributors == null) {
        repoContributors = new LinkedHashMap<>();
        ownerRepoContributors.put(owner, repoContributors);
      }
      List contributors = repoContributors.get(repo);
      if (contributors == null) {
        contributors = new ArrayList<>();
        repoContributors.put(repo, contributors);
      }
      contributors.add(new Contributor(name, contributions));
    }
}

构建 Mock Server 对象:

// Create a very simple Retrofit adapter which points the GitHub API.
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(SimpleService.API_URL)
    .build();

// Create a MockRetrofit object with a NetworkBehavior which manages the fake behavior of calls.
NetworkBehavior behavior = NetworkBehavior.create();
MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit)
    .networkBehavior(behavior)
    .build();

BehaviorDelegate delegate = mockRetrofit.create(GitHub.class);
MockGitHub gitHub = new MockGitHub(delegate);

使用 Mock Server

Call> contributors = gitHub.contributors(owner, repo);
...

也就是说,我们完全可以自己造一个假的数据源,通过 Mock Server 来返回这些写数据。

那么问题来了,这其实并没有完全模拟网络请求的解析流程,如果我只能提供原始的 json 字符串,怎么通过 Retrofit 来实现 Mock Server

时间已经不早啦,我就不猥琐发育了,直接推塔~

本文前面一直专注于介绍 Retrofit,很少提及 OkHttp,殊不知 OkHttp 有一套拦截器的机制,也就是说,我们可以任性的检查 Retrofit 即将发出或者正在发出的所有请求,并且篡改它。所以我们只需要找到我们想要的接口,定制自己的返回结果就好了,下面是一段示例:

OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = null;
        if(BuildConfig.DEBUG && chain.request().url().uri().getPath().equals("/contributors")) {
            //这里读取我们需要返回的 Json 字符串
            String responseString = ...;
            
            response = new Response.Builder()
                    .code(200)
                    .message(responseString)
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_0)
                    .body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
                    .addHeader("content-type", "application/json")
                    .build();
        } else {
            response = chain.proceed(chain.request());
        }

        return response;
    }
}).build();

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();

这样,我们就会拦截 contributors 这个 api 并定制其返回了。

4、小结

Retrofit 是非常强大的,本文通过丰富的示例和对源码的挖掘,向大家展示了 Retrofit 自身强大的功能以及扩展性,就算它本身功能不能满足你的需求,你也可以很容易的进行改造,毕竟人家的代码真是写的漂亮啊。

另外,我之前也写过两篇文章介绍我对 Retrofit 的 Hack,欢迎赏光~

Android 下午茶:Hack Retrofit 之 增强参数

Android 下午茶:Hack Retrofit (2) 之 Mock Server

文中 Hack 之后的 Retrofit 代码见 GitHub。

如果你觉得内容意犹未尽,如果你想了解更多相关信息,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/12862.html

相关文章

  • retrofit - 收藏集 - 掘金

    摘要:一本适合基础入门的中文翻译书掘金中文翻译版本书是对所写一书的中文翻译版本,仅供交流学习使用,严禁商业用途。我们在中使用监测事件掘金源码解析掘金看看调用的代码又是什么抱着一贯的好奇,点进去看看。 一本适合 RxJava 基础入门的中文翻译书 - Android - 掘金RxJava Essentials 中文翻译版 本书是对Ivan.Morgillo所写一书的中文翻译版本,仅供交流学习使...

    Fourierr 评论0 收藏0
  • Retrofit2.0的简单使用

    摘要:是提交的表单数据。各注解含义注解都是在定义接口的时候使用的。用来表明请求方式为请求。基本使用动态替换参数,返回解析后的数据请求必须添加的使用如下所示,会把转成数据进行请求。 原文地址:http://blog.magicer.xyz/2017/... 简介 Retrofit是square公司全家桶中的一员。在okhttp基础上封装的一个网络请求框架。其他废话就不多说了。有几篇不错的文章,...

    learning 评论0 收藏0
  • 完整的开源APP - 收藏集 - 掘金

    摘要:该框架旨在向你展示一个轻量级的应用的总体结构以及如何构建多应用,为了译一步一步教你做一个风格的安卓天气掘金这篇文章教我们如何根据设计指南来创建一个风格的安卓天气。 分享 50 个完整的 React Native 项目 - 掘金本文为 Marno 原创,转载必须保留出处! 公众号 aMarno,关注后回复 RN 加入交流群 简书专题《 React-Native 开发阵营 》,欢迎关注和投...

    Karuru 评论0 收藏0
  • jquery源码分析

    摘要:前言随着前端的不断发展,很多开发人员已经开始使用等框架,但是很少有人去深入分析以及的源码本人也是,至今还停留在使用的层面。最近还在写一些的笔记,有兴趣的小白也可以看下我的博客文章源码分析地址 前言 随着前端的不断发展,很多开发人员已经开始使用react、vue等web框架,但是很少有人去深入分析vue以及react的源码(本人也是,至今还停留在使用的层面)。框架的使用势必会有更新迭代的...

    SHERlocked93 评论0 收藏0
  • [ 学习路线 ] 2015 前端(JS)工程师必知必会 (2)

    摘要:转自前端外刊评论非常感谢,翻译的很好,受益很多,转到此处让前端小伙伴们也惊呆下上次我写前端工程师必知必会已经是三年前了,那是我写过最火的文章了。测试的第二大障碍是工具。 转自:前端外刊评论 非常感谢,翻译的很好,受益很多,转到此处让前端小伙伴们也惊呆下........ 上次我写《前端工程师必知必会》已经是三年前了,那是我写过最火的文章了。三年了,我仍然会在Twitter上...

    stefan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<