從零開始實現一個 mini-Retrofit 框架
前言
本篇文章將採用循序漸進的編碼方式,從零開始實現一個Retorift框架,在實現過程中不斷提出問題並分析實現,最終開發出一個mini版的Retrofit框架

演示一個使用OkHttp的專案Demo
為了更好的演示框架的實現過程,這裡我先建立了一個簡單的Demo專案
這個Demo專案中主要包含3個部分
-
Json資料對應JavaEntity類
-
專案中包裝網路請求回撥的Callback
-
一個包含專案所有網路介面請求的管理類RestService
JavaBean
@Data @ToString public class BaseResponse<T> { private boolean error; private T results; }
package com.knight.sample.entity; import java.util.List; import java.util.Map; public class XianduResponse extends BaseResponse<List<GankEntity>> { }
NetCallback
package com.knight.sample; import java.io.IOException; /** * 專案封裝的統一網路請求的回撥 * @param <T> */ public interface NetCallback<T> { void onFailure(Exception e); void onSuccess(T data); }
NetWorkService
package com.knight.sample; import android.util.Log; import com.google.gson.Gson; import com.google.gson.stream.JsonReader; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class RestService { private static OkHttpClient okHttpClient; public static void init() { okHttpClient = new OkHttpClient.Builder() .build(); } public static<T>void todayGank(Class<T> responseClazz,NetCallback<T> callback) { Request request = new Request.Builder().url("http://gank.io/api/today") .get() .build(); okHttpClient.newCall(request).enqueue(new WrapperOkHttpCallback<>(responseClazz,callback)); } public static<T>void xianduGank(int count, int page,Class<T> responseClazz,NetCallback<T> callback) { Request request = new Request.Builder() .url("http://gank.io/api/xiandu/data/id/appinn/count/" + count + "/page/" + page) .get().build(); okHttpClient.newCall(request).enqueue(new WrapperOkHttpCallback<>(responseClazz,callback)); } static class WrapperOkHttpCallback<T> implements Callback { private static Gson gson = new Gson(); private Class<T> clazz; private NetCallback<T> callback; public WrapperOkHttpCallback(Class<T> responseClazz, NetCallback<T> netCallback) { this.clazz = responseClazz; this.callback = netCallback; } @Override public void onFailure(Call call, IOException e) { Log.e("WrapperOkHttpCallback", "onFailure"); e.printStackTrace(); callback.onFailure(e); } @Override public void onResponse(Call call, Response response) throws IOException { JsonReader jsonReader = gson.newJsonReader(response.body().charStream()); T entity = gson.getAdapter(clazz).read(jsonReader); Log.d("response", entity.toString()); callback.onSuccess(entity); } } }
在NetworkService類中我們目前定義了2個Http 請求 todayGank 和 xianduGank ,目前兩個請求方式都是 Get 其中 xianduGank 需要傳入 count 及 page 引數分別表示每頁資料的資料以及請求的頁碼,除此之外這兩個網路請求都需要傳入 一個 Class 物件表示響應的Json資料對應的Model,以便在內部使用Gson來解析,以及網路請求的非同步回撥 NetCallback
我們不直接使用OkHttp提供的Callback 而是在內部簡單的做了封裝轉換成專案自己的NetCallback,因為對專案的開發人員來說,更希望的是能夠直接在Callback的success回撥中直接得到響應的Json資料對應的JavaBean.
本次提交詳細程式碼: https://github.com/Knight-ZXW/MiniRetrofit/commit/8c5443b752bd85706b4290c0b54b35a13e58c4e2
思考專案現狀
上文模擬的程式碼只是一個簡單的例子,可能會有更好的封裝方式,但這並不是我們這篇文章想要討論的重點。我們回到示例中RestService類中的程式碼部分,看下目前網路請求的寫法
因為我們專案中已經有了OKHttp這個網路庫了,有關Http具體的連線及通訊的髒話累活都可以交給他來處理,對於專案開發者,事實上我們只需要配置以下Http請求部分
- 請求的url 地址
- 請求的方式 (GET、POST、PUT…)
- 請求內容
假設我們已經具備了 Java註解 以及 動態代理的相關知識,知道以下資訊
- 註解可以新增在方法上
- Retention為RUNTIME的註解可以在虛擬機器執行時也獲取到註解上的資訊
- Java的動態代理可以執行時生成原介面型別的代理實現類並hook方法的呼叫
每一個網路介面呼叫請求的url地址和請求方式都是唯一的 ,那麼對於一個簡單的網路請求 我們能不能使用 註解 + 動態代理 來簡化這一過程,改為宣告式的程式設計方式來實現網路呼叫,比如就像這樣
/** * Created by zhuoxiuwu * on 2019/4/25 * email [email protected] */ public interface NetRestService { @GET("http://gank.io/api/today") public Call todayGank(); }
我們在一個抽象介面類中添加了一個方法,在方法上添加了註解 @GET 表示這是一個Http GET請求的呼叫,註解中 GET 帶的預設引數表示GET請求的地址。宣告這個方法後,我們再通過Java動態代理技術在執行時解析這個方法上的註解的資訊,內部通過呼叫 OKHttp 的相關方法生成一個 Call 物件
有了大概思路了,我們接下來先簡單的實現這樣一個小例子來驗證我們的想法是否可行
編碼實現
3.1 簡單實現一個支援GET、POST請求的Retrofit
新建一個註解類@GET
package retrofit2.http; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by zhuoxiuwu * on 2019/4/25 * email [email protected] */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface GET { //註解中 方法名寫成value 這樣的話,在使用註解傳入引數時就不用帶key了,它會作為一個預設的呼叫 String value(); }
新建一個處理Http介面類的動態代理的類 Retrofit ,因為我們實際網路請求的呼叫是依賴OKHttp,所以我們要求建構函式傳入 OkHttp 物件
目前 Retrofit 類只有一個方法 publicT createService(final Classservice) 它接收一個抽象類,並生成該抽象類的代理實現。
package retrofit2; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import okhttp3.OkHttpClient; import okhttp3.Request; import retrofit2.http.GET; public class Retrofit { private OkHttpClient mOkHttpClient; public Retrofit(OkHttpClient mOkHttpClient) { this.mOkHttpClient = mOkHttpClient; } @SuppressWarnings("unchecked") public <T> T createService(final Class<T> service) { return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //獲取方法所有的註解 final Annotation[] annotations = method.getAnnotations(); for (int i = 0; i < annotations.length; i++) { if (annotations[i] instanceof GET) { //如果註解是GET型別 final GET annotation = (GET) annotations[i]; final String url = annotation.value(); final Request request = new Request.Builder() .url(url) .get().build(); return mOkHttpClient.newCall(request); } } return null; } }); } }
目前我們主要的目標是為了驗證這個方案的可行性,因此createService方法內部的邏輯很簡單
1.獲取方法上的所有註解
//獲取方法所有的註解 final Annotation[] annotations = method.getAnnotations();
2.判斷如果存在@GET註解則獲取註解內的值作為請求的地址
if (annotations[i] instanceof GET) { //如果註解是GET型別 final GET annotation = (GET) annotations[i]; final String url = annotation.value();
3.根據url構造GET請求的Request物件,並作為引數呼叫OkHttpClient的newCall方法生成Call物件作為該方法呼叫的返回值
final Request request = new Request.Builder() .url(url) .get().build(); return mOkHttpClient.newCall(request);
以上完成了一個對@GET註解申明的Http請求的動態代理封裝,下面我們在自己的專案中驗證一下
3.2 在專案中驗證
1.建立一個介面類,並新增一個方法,方法的返回型別為 Call ,方法是添加了@GET註解
package com.knight.sample; import okhttp3.Call; import retrofit2.http.GET; /** * Created by zhuoxiuwu * on 2019/4/25 * email [email protected] */ public interface NetRestService { @GET("http://gank.io/api/today") public Call todayGank(); }
2.在專案中新增測試方法並呼叫
private void getToDayGankByRetrofit() { final Retrofit retrofit = new Retrofit(new OkHttpClient()); retrofit.createService(NetRestService.class).todayGank().enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { JsonReader jsonReader = gson.newJsonReader(response.body().charStream()); TodayGankResponse todayGankResponse = gson.getAdapter(TodayGankResponse.class).read(jsonReader); showHttpResult(todayGankResponse.toString()); Log.d("RetrofitTest","呼叫成功,結果為"+todayGankResponse.toString()); } }); }
執行之後,方法呼叫成功並得到了響應結果
D/RetrofitTest: 呼叫成功,結果為BaseResponse(error=false, results={Android=[GankEntity(url=https://github.com/iqiyi/Neptune, desc=適用於Android的靈活,強大且輕量級的外掛框架...
通過簡單的一個實現,我們成功驗證了使用註解加動態代理的方式實現一個宣告式的網路請求框架是可行的,那麼後續我們需要繼續完善這個專案,提供對更多請求方式 以及引數的支援
對於其他請求方式的支援,我們可以新增更多的表示請求方式的註解,當用戶設定了不同的註解,在內部我們使用OKHttp呼叫相應的方法。Http的請求方式大概如下
- @DELETE
- @GET
- @HEAD
- @PATCH
- @POST
- @PUT
- @OPTIONS
3.3 繼續實現POST註解
為了加深理解,我們繼續簡單的實現一個POST請求,並支援傳入一個引數物件,作為POST請求的JSON資料
首先我們新增一個POST註解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface POST { String value(); }
package retrofit2; import com.google.gson.Gson; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import okhttp3.Call; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import retrofit2.http.GET; import retrofit2.http.POST; public class Retrofit { private OkHttpClient mOkHttpClient; public Retrofit(OkHttpClient mOkHttpClient) { this.mOkHttpClient = mOkHttpClient; } @SuppressWarnings("unchecked") public <T> T createService(final Class<T> service) { return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //獲取方法所有的註解 final Annotation[] annotations = method.getAnnotations(); for (int i = 0; i < annotations.length; i++) { if (annotations[i] instanceof GET) { //如果註解是GET型別 final GET annotation = (GET) annotations[i]; return parseGet(annotation.value(), method, args); } else if (annotations[i] instanceof POST) { final POST annotation = (POST) annotations[i]; return parsePost(annotation.value(), method, args); } } return null; } }); } private Call parseGet(String url, Method method, Object args[]) { final Request request = new Request.Builder() .url(url) .get().build(); return mOkHttpClient.newCall(request); } private Gson gson = new Gson(); private static final MediaType MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8"); private Call parsePost(String url, Method method, Object args[]) { final Type[] genericParameterTypes = method.getGenericParameterTypes(); if (genericParameterTypes.length > 0) { final Class<?> clazz = Utils.getRawType(genericParameterTypes[0]); final String jsonBody = gson.toJson(args[0], clazz); final Request request = new Request.Builder() .url(url) .post(RequestBody.create(MEDIA_TYPE, jsonBody)) .build(); return mOkHttpClient.newCall(request); } return null; } }
在 paresePost方法中我們首先通過Method的getGenericParameterTypes方法獲取所有引數的Type型別,並且通過Type類獲得引數的原始Class型別,之後就可以使用Gson轉換成對應的Json物件了。
3.4 實現ConverterFactory 解耦Json轉換
在上面的例子中,我們直接在框架Retrofit中使用了Gson庫做Json轉換,但作為一個框架來說 我們不希望直接強耦合一個第三方Json轉換庫,這部分更希望交由開發者根據具體情況自由選擇;因此我們可以對這部分做下抽象封裝,提取成一個負責Json轉換的介面 由應用層傳入具體的實現.
package retrofit2; import java.lang.reflect.Type; import javax.annotation.Nullable; import okhttp3.RequestBody; /** * Created by zhuoxiuwu * on 2019/4/25 * email [email protected] */ public interface Converter<F, T> { @Nullable T convert(F value); abstract class Factory { public @Nullable Converter<?, RequestBody> requestBodyConverter(Type type) { return null; } } }
應用層需要傳入一個ConverterFactory,該工廠類負責根據傳入的Type型別,返回一個能夠將該Type型別的物件轉換成RequestBody的Converter
我們對Retrofit的建構函式以及paresePost方法做下修改,要求建構函式中傳入一個ConverterFactory的實現,並在paresePost方法中使用這個ConverterFactory來做Java物件到ReqeustBody的轉換
public class Retrofit { private OkHttpClient mOkHttpClient; private Converter.Factory mConverterFactory; public Retrofit(OkHttpClient mOkHttpClient, Converter.Factory mConverterFactory) { this.mOkHttpClient = mOkHttpClient; this.mConverterFactory = mConverterFactory; } //..省略部分程式碼 private Call parsePost(String url, Method method, Object args[]) { final Type[] genericParameterTypes = method.getGenericParameterTypes(); if (genericParameterTypes.length > 0) { //直接呼叫得到RequestBody final RequestBody requestBody = requestBodyConverter(genericParameterTypes[0]).convert(args[0]); final Request request = new Request.Builder() .url(url) .post(requestBody) .build(); return mOkHttpClient.newCall(request); } return null; } public <T> Converter<T, RequestBody> requestBodyConverter(Type type) { return (Converter<T, RequestBody>) mConverterFactory.requestBodyConverter(type); }
在應用層,我們實現並傳入一個Gson的ConvertFactory的實現
package com.knight.sample; import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Type; import java.nio.charset.Charset; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.Buffer; import retrofit2.Converter; /** * Created by zhuoxiuwu * on 2019/4/25 * email [email protected] */ public class GsonConverterFactory extends Converter.Factory { public static GsonConverterFactory create() { return create(new Gson()); } public static GsonConverterFactory create(Gson gson) { if (gson == null) throw new NullPointerException("gson == null"); return new GsonConverterFactory(gson); } private final Gson gson; private GsonConverterFactory(Gson gson) { this.gson = gson; } @Override public Converter<?, RequestBody> requestBodyConverter(Type type) { //通過Type 轉換成Gson的TypeAdapter //具體型別的json轉換依賴於這個TypeAdapter TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); return new GsonRequestBodyConverter<>(gson, adapter); } final static class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> { private static final MediaType MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8"); private static final Charset UTF_8 = Charset.forName("UTF-8"); private final Gson gson; private final TypeAdapter<T> adapter; GsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) { this.gson = gson; this.adapter = adapter; } @Override public RequestBody convert(T value) { Buffer buffer = new Buffer(); Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8); JsonWriter jsonWriter = null; try { jsonWriter = gson.newJsonWriter(writer); adapter.write(jsonWriter, value); jsonWriter.close(); return RequestBody.create(MEDIA_TYPE, buffer.readByteString()); } catch (IOException e) { e.printStackTrace(); return null; } } } }
3.5 實現CallAdapter 支援方法返回型別
繼續回到Http請求的宣告中,目前我們方法所支援的返回型別都是OKHttp的Call物件,而Call物件從使用上來說,目前還是有些繁瑣,原生的Call物件返回的是ResponseBody還需要開發者自己處理並做轉換。
public interface NetRestService { @GET("http://gank.io/api/today") public Call todayGank(); }
也許我們希望這個方法可以這樣定義
public interface NetRestService { @GET("http://gank.io/api/today") public TodayGankResponse todayGank(); }
也許我們可以在框架內部通過判斷方法的返回型別是不是Call物件,如果不是,就在框架內部直接同步呼叫網路請求得到響應的Json內容後直接轉換成JavaBean物件作為方法的返回值,但是這個設想存在這樣幾個問題
-
要實現直接返回Http結果則方法呼叫是同步呼叫,如果在主執行緒做IO請求肯定是不合理的
-
如果內部IO異常了,或者JSON轉換失敗了方法返回的是什麼呢?為null嗎?
因此更合理的話,在應用我們希望的是返回一個包裝的支援非同步呼叫的型別
比如我們的專案自己新增了一個支援非同步呼叫的NetCall抽象介面
/** * Created by zhuoxiuwu * on 2019/4/26 * email [email protected] */ public interface NetCall<T> { public void execute(NetCallback<T> netCallback); }
我們希望我們的方法可以這樣申明
public interface NetRestService { @GET("http://gank.io/api/today") public NetCall<TodayGankResponse> todayGank(); }
這樣的話在應用層我們呼叫的時候就可以像這樣使用
retrofit.createService(NetRestService.class).todayGank() .execute(new NetCallback<TodayGankResponse>() { @Override public void onFailure(Exception e) { } @Override public void onSuccess(TodayGankResponse data) { Log.d("RetrofitTest","呼叫成功,結果為"+data.toString()); showHttpResult(data.toString()); } });
那麼具體要怎麼實現呢,這個功能相當於讓Retrofit框架支援 對方法返回型別的自定義適配,和Converter介面一樣的思路,我們在框架可以定義一個 CallAdapter介面,讓應用層來具體實現並傳入
package retrofit2; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import okhttp3.Call; /** * Created by zhuoxiuwu * on 2019/4/26 * email [email protected] */ public interface CallAdapter<T> { T adapt(Call call); abstract class Factory { public abstract CallAdapter<?> get(Type returnType,Retrofit retrofit); /** * 這是一個框架提供給開發者的util方法 * 用於獲取型別的泛型上的型別 * 比如 Call<Response> 則 第0個泛型是Response.class */ protected static Type getParameterUpperBound(int index, ParameterizedType type) { return Utils.getParameterUpperBound(index, type); } /** * 獲取Type對應的Class * @param type * @return */ protected static Class<?> getRawType(Type type) { return Utils.getRawType(type); } } }
在應用層我們可以實現一個NetCallAdapter,支援Call物件到 NetCall物件的轉換
package com.knight.sample; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Response; import retrofit2.CallAdapter; import retrofit2.Retrofit; /** * Created by zhuoxiuwu * on 2019/4/26 * email [email protected] */ package com.knight.sample; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Response; import retrofit2.CallAdapter; import retrofit2.Retrofit; /** * Created by zhuoxiuwu * on 2019/4/26 * email [email protected] */ public class NetCallAdapterFactory extends CallAdapter.Factory { /** * returnType引數 和 retroift引數 由底層框架傳遞給開發者 * @param returnType * @param retrofit * @return */ @Override public CallAdapter<?> get(final Type returnType, final Retrofit retrofit) { //判斷返回型別是否是 NetCall if (getRawType(returnType) != NetCall.class) { return null; } //要求開發者方法的返回型別必須寫成 NetCall<T> 或者NetCall<? extends Foo> 的形式,泛型內的型別就是Json資料對應的Class if (!(returnType instanceof ParameterizedType)) { throw new IllegalStateException( "NetCall return type must be parameterized as NetCall<Foo> or NetCall<? extends Foo>"); } final Type innerType = getParameterUpperBound(0, (ParameterizedType) returnType); return new CallAdapter<NetCall>() { @Override public NetCall adapt(final Call call) { return new NetCall() { @Override public void execute(final NetCallback netCallback) { call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { netCallback.onFailure(e); } @Override public void onResponse(Call call, Response response) throws IOException { //由retrofit 提供 ResponseBody 到 某個Type Class的轉換 final Object value = retrofit.responseBodyTConverter(innerType).convert(response.body()); netCallback.onSuccess(value); } }); } }; } }; } }
框架的後續實現及優化
到目前為止我們已經實現了一個簡單的Retrofit框架,也許程式碼不夠精簡,邊界處理沒有十分嚴謹,但已經初具雛形。我們可以繼續思考現有專案的不足,新增更多的支援。
比如在網路請求方面目前只支援GET、POST,那麼我們後續需要新增更多請求方式的支援。
以上提出的一些優化點,大家可以自己先思考實現並重新閱讀寫Retrofit原始碼來加深自己的理解。從整個思考流程及實現上來看Retrofit的實現並不複雜,但是從實現一個簡單可用的網路封裝庫到實現一個拓展性強、職責分離的框架,中間的過程還是有很多細節的,如果你看完了這篇文章,可以再抽1個小時左右的時間重新看下Retorift框架的原始碼,相信從中還會有更多的收穫。
1.Android架構師腦圖;

架構技術詳解,學習路線與資料分享都在 部落格 這篇文章裡《 “寒冬未過”,阿里P9架構分享Android必備技術點,讓你offer拿到手軟!》 (包括自定義控制元件、NDK、架構設計、混合式開發工程師(React native,Weex)、效能優化、完整商業專案開發等);也可直接 加群 找管理員免費領取。
2.全套高階架構視訊;
正在整理一套完整的Android架構視訊,免費分享。目前還在更新中,歡迎關注謝謝支援
(包括java基礎與原理,自定義控制元件、NDK、架構設計、混合式開發(Flutter,Weex)、效能優化、完整商業專案開發等技術體系)

image