深入淺出 Retrofit,這么牛逼的框架你們還不來看看?
Android 開發中,從原生的 HttpUrlConnection 到經典的 Apache 的 HttpClient ,再到對前面這些網絡基礎框架的封裝,比如 Volley 、 Async 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<List<Repo>> 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<List<Repo>> repos = service.listRepos("octocat");
發請求的代碼就像前面這一句,返回的 repos 其實并不是真正的數據結果,它更像一條指令,你可以在合適的時機去執行它:
// 同步調用
List<Repo> data = repos.execute();
// 異步調用
repos.enqueue(new Callback<List<Repo>>() {
@Override
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
List<Repo> data = response.body();
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
t.printStackTrace();
}
});
啥感覺?有沒有突然覺得請求接口就好像訪問自家的方法一樣簡單?吶,前面我們看到的,就是 Retrofit 官方的 demo 了。你以為這就夠了?噗~怎么可能。。
1.3 Url 配置
Retrofit 支持的協議包括 GET / POST / PUT / DELETE / HEAD / PATCH ,當然你也可以直接用 HTTP 來自定義請求。這些協議均以注解的形式進行配置,比如我們已經見過 GET 的用法:
@GET("users/{user}/repos")
Call<List<Repo>> 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<ResponseBody> list(@Query("page") int page);
Query 其實就是 Url 中 ‘?’ 后面的 key-value,比如:
http://www.println.net/?cate=android
這里的 cate=android 就是一個 Query ,而我們在配置它的時候只需要在接口方法中增加一個參數,即可:
interface PrintlnServer{
@GET("/")
Call<String> cate(@Query("cate") String cate);
}
這時候你肯定想,如果我有很多個 Query ,這么一個個寫豈不是很累?而且根據不同的情況,有些字段可能不傳,這與方法的參數要求顯然也不相符。于是,打群架版本的 QueryMap 橫空出世了,使用方法很簡單,我就不多說了。
1.4.2 Field & FieldMap
其實我們用 POST 的場景相對較多,絕大多數的服務端接口都需要做加密、鑒權和校驗, GET 顯然不能很好的滿足這個需求。使用 POST 提交表單的場景就更是剛需了,怎么提呢?
@FormUrlEncoded
@POST("/")
Call<ResponseBody> 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<ResponseBody> upload(@Part("description") RequestBody description,
@Part MultipartBody.Part file);
}
如果你需要上傳文件,和我們前面的做法類似,定義一個接口方法,需要注意的是,這個方法不再有 @FormUrlEncoded 這個注解,而換成了 @Multipart ,后面只需要在參數中增加 Part 就可以了。也許你會問,這里的 Part 和 Field 究竟有什么區別,其實從功能上講,無非就是客戶端向服務端發起請求攜帶參數的方式不同,并且前者可以攜帶的參數類型更加豐富,包括數據流。也正是因為這一點,我們可以通過這種方式來上傳文件,下面我們就給出這個接口的使用方法:
//先創建 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<ResponseBody> call = service.upload(description, body);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call,
Response<ResponseBody> response) {
System.out.println("success");
}
@Override
public void onFailure(Call<ResponseBody> 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<ResponseBody> 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<File, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return new FileRequestBodyConverter();
}
}
static class FileRequestBodyConverter implements Converter<File, RequestBody> {
@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<List<Repo>> listRepos(@Path("user") String user);
}
返回值的類型為 List<Repo> ,而我們直接拿到的原始返回肯定就是字符串(或者字節流),那么這個返回值類型是怎么來的呢?首先說明的一點是,GitHub 的這個 api 返回的是 Json 字符串,也就是說,我們需要使用 Json 反序列化得到 List<Repo> ,這其中用到的其實是 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<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return super.responseBodyConverter(type, annotations, retrofit);
}
}
static class ArbitraryResponseBodyConverter implements Converter<ResponseBody, Result>{
@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<List<Repo>> listRepos(@Path("user") String user);
}
而真正我使用的時候肯定不能是接口啊,這個神秘的家伙究竟是誰?其實它是 Retrofit 創建的一個代理對象了,這里涉及點兒 Java 的動態代理的知識,直接來看代碼:
public <T> T create(final Class<T> 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 的接口:
我們在使用接口時,大家肯定還記得這一句:
這個 repos 其實就是一個 OkHttpCall 實例, execute 就是要發起網絡請求。
OkHttpCall.execute
我們看到 OkHttpCall 其實也是封裝了 okhttp3.Call ,在這個方法中,我們通過 okhttp3.Call 發起了進攻,額,發起了請求。有關 OkHttp 的內容,我在這里就不再展開了。
parseResponse 主要完成了由 okhttp3.Response 向 retrofit.Response 的轉換,同時也處理了對原始返回的解析:
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
//略掉一些代碼
try {
//在這里完成了原始 Response 的解析,T 就是我們想要的結果,比如 GitHubService.listRepos 的 List<Repo>
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;
}
}
至此,我們就拿到了我們想要的數據~~
來自:http://dev.qq.com/topic/591aa71ae315487c53deeca9