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