如何使用Retrofit請求非Restful API
前言
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年前嘻嘻^_^),KX上網未普及,天朝百度蠻橫,搜“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方便,在任意瀏覽器都可以直觀地看到錯誤碼&錯誤信息。
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);
}</code></pre>
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();</code></pre>
此處加入了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);
}
}</code></pre>
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();
}
}
}</code></pre>
先給大家科普下,
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);
}
......
}</code></pre>
寫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();
}
}
}</code></pre>
為什么我們要新建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": "小紅"
}
}</code></pre>
(此時是否有打死后端工程師的沖動?)
別怒,魔高一尺,道高一丈。
玩轉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);
}
...
}
}</code></pre>
給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);
}</code></pre>
這么寫后,后端改什么名稱都不怕!
更靈活的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();
}</code></pre>
并且寫一個Converter
抽象類:
public abstract class AbstractResponseConverter<T> implements Converter<ResponseBody, T>{
protected Gson gson;
public AbstractResponseConverter(Gson gson) {
this.gson = gson;
}
}</code></pre>
為什么要寫一個繼承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;
}
}</code></pre>
@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>
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,絕對給你意想不到的驚喜!并希望作為開發者的你,受此啟發,寫出更加靈活的代碼。
文/苦逼鍵盤男kkmike999(簡書)