如何使用Retrofit请求非Restful API

cqpw8262 8年前
   <h2>前言</h2>    <p>2016年以来,越来越多Android开发者使用<code>Retrofit</code>作为HTTP请求框架。原因其一,Google发布<a href="/misc/goto?guid=4959672167809561404">Android 6.0 SDK (API 23) 抛弃了HttpClient</a>;其二,Square在2016.1.2发布<code>okhttp3.0</code>、2016.3.11正式发布<code>Retrofit 2.0</code>。</p>    <h3>HttpClient时代</h3>    <p>作为深受<code>Apache HttpClient</code>毒害的一代青年,不得不吐槽<code>HttpClient</code>的版本维护和API文档有多糟糕。诟病缠身的<code>HttpClient</code>从3.x到4.x,api变更面目全非,甚至4.0-4.5,api改动也不少。如果你以前使用3.x,升级到4.0后,http代码几乎全改了。大家可以看看<strong><a href="/misc/goto?guid=4959672167897567180">Apache官网</a></strong>看看<code>httpClient</code>发布历史(<a href="/misc/goto?guid=4959672167979763922">3.x历史</a>、<a href="/misc/goto?guid=4959672168058236469">4.x历史</a>)。文档嘛,Apache官网简直....连程序猿这审美观都不想看!</p>    <p><code>HttpClient</code>发展历史相当长,最早是2001.10发布<code>2.0-alpha 1</code>,2004.11发布<code>3.0-beta1</code>,2008.1发布<code>4.0-beta1</code>,直到2012.2才发布<code>4.2-beta1</code>,2014.12发布<code>4.4-release</code>,2016.1发布<code>5.0-alpha</code>。由于源远流长,<code>httpClient</code>在国人心中根心蒂固。可以想象当年读书(也就4年前嘻嘻^_^),KX上网未普及,天朝百度蛮横,搜“java http请求”出来的几乎都是<code>httpClient</code>(不信你现在百度)。</p>    <p>2013年以来,Google逐渐意识到<code>httpClient</code>的诟病,狠心之下,抛弃httpClient,因为我们有更好的选择:<code>okhttp</code>.</p>    <h3>OkHttp</h3>    <p>美国移动支付公司<strong>Square</strong>,在2013.5.6开源一款 <strong>java http请求框架——<a href="/misc/goto?guid=4958860115824511695">OkHttp</a></strong>. 发布之后,在国外迅速流行起来,一方面是<code>httpClient</code>太繁琐、更新慢,另一方面<code>okHttp</code>确实好用。<code>okHttp</code>发布之后不断地改进,2014.5发布<code>2.0-rc1</code>,2016.1发布<code>3.0</code>,更新速度相当快,而且开发人员经常对代码进行维护,看看<a href="/misc/goto?guid=4958964956476581901">http://square.github.io/okhttp</a>就知道了。相比之下,httpClient维护相当糟糕。</p>    <p>Api文档方面,我非常喜欢Square公司的设计风格,<code>okHttp</code>首页相当简洁,Overview、Example、Download全在首页展示,详细使用案例、说明,在<code>github</code>上很清晰。</p>    <h3>Retrofit</h3>    <p>从发布历史上来看,<code>Retrofit</code>和<code>okhttp</code>是兄弟,Square公司在2013.5.13发布<code>1.0</code>,2015.8发布<code>2.0-beta1</code>。</p>    <p><code>Retrofit</code>底层基于<code>OkHttp</code>·,并且可以加很多Square开发的“周边产品”:<code>converter-gson</code>、<code>adapter-rxjava</code>等。<code>Retrofit</code>抱着<code>gson</code>&<code>rxjava</code>的大腿,这种聪明做法,也是最近大受欢迎的原因之一,所谓“<code>Rxjava</code>火了,<code>Retrofit</code>也火了”。<code>Retrofit</code>·不仅仅支持这两种周边,我们可以自定义<code>converter</code>&<code>call adapter</code>,可以你喜欢的其他第三方库。</p>    <p>介绍了主流java http请求库历史,大家对“为什么用retrofit”有个印象了吧?想想,如果没有Square公司,apahce httpClient还将毒害多少无知青年。</p>    <h2>何为非Restful Api?</h2>    <p>Restful Api</p>    <p><code>User</code>数据,有uid、name,Restful Api返回数据:</p>    <pre>  <code class="language-java">{      "name": "kkmike999",      "uid": 1  }</code></pre>    <p>在数据库没找到User,直接返回错误的http code。但弊端是当在浏览器调试api,后端查询出错时,很难查看错误码&错误信息。(当然用chrome的开发者工具可以看,但麻烦)</p>    <p>Not Restful Api</p>    <p>但不少后端工程师,并不一定喜欢用Restful Api,他们会自己在json中加入ret、msg这种数据。当User正确返回:</p>    <pre>  <code class="language-java">{      "ret": 0,      "msg": "成功",      "data": {          "uid": 1,          "name": "kkmike999"      }  }</code></pre>    <p>错误返回:</p>    <pre>  <code class="language-java">{      "ret": -1,      "msg": "失败"  }</code></pre>    <p>这样的好处,就是调试api方便,在任意浏览器都可以直观地看到错误码&错误信息。</p>    <h2>Retrofit一般用法</h2>    <p>本来<code>Retrofit</code>对<code>restful</code>的支持,可以让我们写少很多冤枉代码。但后端这么搞一套,前端怎么玩呀?既然木已成舟,我们做APP的总不能老对后端指手画脚,友谊小船说翻就翻。</p>    <p>先说说<code>retrofit</code>普通用法</p>    <pre>  <code class="language-java">public class User {      int    uid;      String name;  }    public interface UserService {        @GET("not_restful/user/{name}.json")      Call<User> loadUser(@Path("name") String name);  }</code></pre>    <p><code>Bean</code>和<code>Service</code>准备好,接下来就是调用<code>Retrofit</code>了:</p>    <pre>  <code class="language-java">OkHttpClient client = new OkHttpClient.Builder().build();    Retrofit retrofit = new Retrofit.Builder().baseUrl("http://***.b0.upaiyun.com/")                                            .addConverterFactory(GsonConverterFactory.create())                                            .client(client)                                            .build();    UserService userService = retrofit.create(UserService.class);    User user = userService.loadUser("kkmike999")                         .execute()                         .body();</code></pre>    <p>此处加入了<code>GsonConverterFactory</code>,没有使用<code>RxJavaCallAdapter</code>。如果是restful api,直接返回<code>User</code>的<code>json</code>,那调用<code>execute().body()</code>就能获得正确的<code>User</code>了。然而,not restful api,返回一个不正确的<code>User</code> ,也不抛错,挺难堪的。</p>    <h3>ResponseConverter</h3>    <p>我们留意到<code>GsonConverterFactory</code>,看看源码:</p>    <pre>  <code class="language-java">package retrofit2.converter.gson;    import com.google.gson.Gson;  import com.google.gson.TypeAdapter;  import com.google.gson.reflect.TypeToken;  import java.lang.annotation.Annotation;  import java.lang.reflect.Type;  import okhttp3.RequestBody;  import okhttp3.ResponseBody;  import retrofit2.Converter;  import retrofit2.Retrofit;    public final class GsonConverterFactory extends Converter.Factory {      public static GsonConverterFactory create() {        return create(new Gson());    }      public static GsonConverterFactory create(Gson gson) {        return new GsonConverterFactory(gson);    }      private final Gson gson;      private GsonConverterFactory(Gson gson) {        if (gson == null) throw new NullPointerException("gson == null");        this.gson = gson;    }      @Override    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));        return new GsonResponseBodyConverter<>(gson, adapter);    }      @Override    public Converter<?, RequestBody> requestBodyConverter(Type type,        Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {          TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));        return new GsonRequestBodyConverter<>(gson, adapter);    }  }</code></pre>    <p><code>responseBodyConverter</code>方法返回<code>GsonResponseBodyConverter</code>,我们再看看<code>GsonResponseBodyConverter</code>源码:</p>    <pre>  <code class="language-java">package retrofit2.converter.gson;    final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {      private final Gson           gson;      private final TypeAdapter<T> adapter;        GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {          this.gson = gson;          this.adapter = adapter;      }        @Override      public T convert(ResponseBody value) throws IOException {          JsonReader jsonReader = gson.newJsonReader(value.charStream());          try {              return adapter.read(jsonReader);          } finally {              value.close();          }      }  }</code></pre>    <p>先给大家科普下,</p>    <p><code>TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));</code> 这里<code>TypeAdapter</code>是什么。<code>TypeAdapter</code>是<code>gson</code>让使用者自定义解析的json,<code>Type</code>是<code>service</code>方法返回值<code>Call<?></code>的泛型类型。<code>UserService</code>中<code>Call<User> loadUser(...)</code>,泛型参数是<code>User</code>,所以<code>type</code>就是<code>User</code>类型。详细用法参考:<a href="/misc/goto?guid=4959672168196090804">你真的会用Gson吗?Gson使用指南(四)</a></p>    <p>重写GsonResponseConverter</p>    <p>由源码看出,是<code>GsonResponseBodyConverter</code>对<code>json</code>进行解析的,只要重写<code>GsonResponseBodyConverter</code>,自定义解析,就能达到我们目的了。</p>    <p>但<code>GsonResponseBodyConverter</code>和<code>GsonConverterFactory</code>都是<code>final class</code>,并不能重写。靠~ 不让重写,我就copy代码!</p>    <p>新建<code>retrofit2.converter.gson</code>目录,新建<code>CustomConverterFactory</code>,把<code>GsonConverterFactory</code>源码拷贝过去,同时新建<code>CustomResponseConverter</code>。 把<code>CustomConverterFactory</code>的<code>GsonResponseBodyConverter</code>替换成<code>CustomResponseConverter</code>:</p>    <pre>  <code class="language-java">public final class CustomConverterFactory extends Converter.Factory {      ......        @Override      public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {          TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));          return new CustomResponseConverter<>(gson, adapter);      }      ......  }</code></pre>    <p>写<code>CustomResponseConverter</code>:</p>    <pre>  <code class="language-java">public class CustomResponseConverter<T> implements Converter<ResponseBody, T> {        private final Gson gson;      private final TypeAdapter<T> adapter;        public CustomResponseConverter(Gson gson, TypeAdapter<T> adapter) {          this.gson = gson;          this.adapter = adapter;      }        @Override      public T convert(ResponseBody value) throws IOException {          try {              String body = value.string();                JSONObject json = new JSONObject(body);                int    ret = json.optInt("ret");              String msg = json.optString("msg", "");                if (ret == 0) {                  if (json.has("data")) {                      Object data = json.get("data");                        body = data.toString();                        return adapter.fromJson(body);                  } else {                      return (T) msg;                  }              } else {                  throw new RuntimeException(msg);              }          } catch (Exception e) {              throw new RuntimeException(e.getMessage());          } finally {              value.close();          }      }  }</code></pre>    <p>为什么我们要新建<code>retrofit2.converter.gson</code>目录?因为<code>GsonRequestBodyConverter</code>不是<code>public class</code>,所以<code>CustomConverterFactory</code>要<code>import GsonRequestBodyConverter</code>就得在同一目录下。当然你喜欢放在自己目录下,可以拷贝源码如法炮制。</p>    <p>接下来,只要</p>    <p><code>new Retrofit.Builder().addConverterFactory(CustomConverterFactory.create())</code>就大功告成了!</p>    <p>更灵活的写法</p>    <p>上述做法,我们仅仅踏入半条腿进门,为什么?万一后端不喜欢全用"data",而是根据返回数据类型命名,例如返回<code>User</code>用<code>"user"</code>,返回<code>Student</code>用<code>"student"</code>呢?</p>    <pre>  <code class="language-java">{      "ret": 0,      "msg": "成功",      "user": {          "uid": 1,          "name": "小明"      }  }    {      "ret": 0,      "msg": "成功",      "student": {          "uid": 1,          "name": "小红"      }  }</code></pre>    <p>(此时是否有打死后端工程师的冲动?)</p>    <blockquote>     <p>别怒,魔高一尺,道高一丈。</p>    </blockquote>    <h3>玩转Service注解</h3>    <p>既然<code>retrofit</code>能“理解”<code>service</code>方法中的注解,我们为何不试试?<code>GsonConverterFactory</code>的方法</p>    <p><code>responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)</code>,这里有<code>Annotation[]</code>,没错,这就是<code>service</code>方法中的注解。</p>    <p>我们写一个<code>@Data</code>注解类:</p>    <pre>  <code class="language-java">@Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  public @interface Data {      String value() default "data";  }</code></pre>    <p>在<code>loadUser(...)</code>添加<code>@Data</code>:</p>    <pre>  <code class="language-java">@Data("user")  @GET("not_restful/user/{name}.json")  Call<User> loadUser(@Path("name") String name);</code></pre>    <p>修改<code>CustomResponseConverter</code></p>    <pre>  <code class="language-java">public class CustomResponseConverter<T> implements Converter<ResponseBody, T> {        private final Gson gson;      private final TypeAdapter<T> adapter;      private final String name;        public CustomResponseConverter(Gson gson, TypeAdapter<T> adapter, String name) {          this.gson = gson;          this.adapter = adapter;          this.name = name;      }        @Override      public T convert(ResponseBody value) throws IOException {          try {              ...              if (ret == 0) {                  if (json.has(name)) {                      Object data = json.get(name);                        body = data.toString();                        return adapter.fromJson(body);                  }                  ...      }  }</code></pre>    <p>给<code>CustomConverterFactory</code>的<code>responseBodyConverter(...)</code>加上</p>    <pre>  <code class="language-java">@Override  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)      String name = "data";// 默认"data"        for (Annotation annotation : annotations) {          if (annotation instanceof Data) {              name = ((Data) annotation).value();              break;          }      }      ...        return new CustomResponseConverter<>(gson, adapter, name);  }</code></pre>    <p>这么写后,后端改什么名称都不怕!</p>    <h3>更灵活的Converter</h3>    <p>有个需求:APP显示某班级信息&学生信息。后台拍拍脑袋:</p>    <pre>  <code class="language-java">{      "ret": 0,      "msg": "",      "users": [          {              "name": "鸣人",              "uid": 1          },          {              "name": "佐助",              "uid": 2          }      ],      "info": {          "cid": 7,          "name": "第七班"      }  }</code></pre>    <p>哭了吧,灭了后端工程师恐怕也难解心头之恨!</p>    <p>阿尼陀佛, 我不是说了吗?</p>    <blockquote>     <p>魔高又一尺,道又高一丈。</p>    </blockquote>    <p>我们意识到,<code>CustomResponseConverter</code>责任太重,又是判断<code>ret</code>、<code>msg</code>,又是解析<code>json</code>数据并返回<code>bean</code>,如果遇到奇葩json,<code>CustomResponseConverter</code>远远不够强大,而且不灵活。</p>    <p>怎么办,干嘛不自定义converter呢?<br> 问题来了,这个converter应该如何传给<code>CustomConverterFactory</code>?因为在<code>new Retrofit.Builder().addConvertFactory(…)</code>时就要添加<code>ConverterFactory</code>,那时并不知道返回<code>json</code>是怎样,哪个<code>service</code>要用哪个<code>adapter</code>。反正通过构造方法给<code>CustomConverterFactory</code>传<code>Converter</code>肯定行不通。</p>    <p>我们上面不是用过<strong>Annotaion</strong>吗?同样手段再玩一把如何。写一个<code>@Converter</code>注解:</p>    <pre>  <code class="language-java">@Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  public @interface Converter {        Class<? extends AbstractResponseConverter> converter();  }</code></pre>    <p>并且写一个<code>Converter</code>抽象类:</p>    <pre>  <code class="language-java">public abstract class AbstractResponseConverter<T> implements Converter<ResponseBody, T>{        protected Gson gson;        public AbstractResponseConverter(Gson gson) {          this.gson = gson;      }  }</code></pre>    <p>为什么要写一个继承<code>Converter</code>抽象类?让我们自定义的<code>Converter</code>直接继承<code>Converter</code>不行吗?<br> 注意了,<code>@Adapter</code>只能携带<code>Class<?></code>和<code>int``String</code>等基本类型,并不能带<code>converter对象</code>。而我们需要<code>CustomConverterFactory</code>在<code>responseBodyConverter()</code>方法中,通过反射,<code>new</code>一个<code>converter对象</code>,而<code>CustomConverterFactory</code>并不知道调用<code>Converter</code>哪个构造函数,传什么参数。所以,干脆就写一个<code>AbstractResponseConverter</code>,让子类继承它,实现固定的构造方法。这样<code>CustomConverterFactory</code>就可以获取固定的构造方法,生成Converter对象并传入如<code>gson``typeAdapter</code>参数了。</p>    <pre>  <code class="language-java">public class ClazzInfo{      List<Student> students;      Info     info;  }    public class ClassConverter implements AbstractResponseConverter<ClazzInfo>{        public ClassConverter(Gson gson){          super(gson);      }        @Override      public ClazzInfo convert(ResponseBody value) throws IOException {          // 这里你想怎么解析json就怎么解析啦          ClazzInfo clazz = ...          return clazz;      }  }</code></pre>    <pre>  <code class="language-java">    @Override      public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {            for (Annotation annotation : annotations) {              if (annotation instanceof Adapter) {                  try {                      Class<? extends AbstractResponseConverter> converterClazz = ((Adapter) annotation).adapter();                      // 获取有 以gson参数的 构造函数                      Constructor<? extends AbstractResponseConverter> constructor = converterClazz .getConstructor(Gson.class);                      AbstractResponseConverter  converter = constructor.newInstance(gson);                        return converter;                  } catch (Exception e) {                      e.printStackTrace();                  }              }          }          ...          return new CustomResponseConverter<>(gson, adapter, name);      }</code></pre>    <p>Service方法注解:</p>    <pre>  <code class="language-java">@Converter(converter = ClassConverter.class)  @GET("not_restful/class/{cid}.json")  Call<ClazzInfo> loadClass(@Path("cid") String cid);</code></pre>    <p>写到这里,已经快吐血了。怎么会有这么奇葩的后端.... 正常情况下,应该把<code>"users"</code>和<code>"class"</code>封装在<code>"data"</code>里,这样我们就可以直接把返回结果写成<code>Call<ClassInfo></code>就可以了。</p>    <h2>小结</h2>    <p><code>Retrofit</code>可以大量减少写无谓的代码,减少工作量之余,还能让http层更加清晰、解耦。当你遇到非Restful Api时,应该跟后端协商一种固定的<code>json</code>格式,便于APP写代码。</p>    <blockquote>     <p>代码越少,错得越少</p>    </blockquote>    <p>同时,使用Retrofit让你更容易写单元测试。由于<code>Retrofit</code>基于<code>okhttp</code>,完全不依赖<code>android</code>库,所以可以用<code>junit</code>直接进行单元测试,而不需要<code>robolectric</code>或者在真机、模拟器上运行单元测试。之后有空我会写关于Android单元测试的文章。</p>    <blockquote>     <p>“我们可以相信的变革”( CHANGE WE CAN BELIEVE IN ) ——美国总统第44任总统,奥巴马</p>    </blockquote>    <p>如果你还用<code>httpClient</code>,请尽管大胆尝试<code>Retrofit</code>,don't afraid change,绝对给你意想不到的惊喜!并希望作为开发者的你,受此启发,写出更加灵活的代码。</p>    <p><br>  </p>    <p>文/<a href="/misc/goto?guid=4959672168271061556">苦逼键盘男kkmike999</a>(简书)<br>  </p>