如何使用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>