[動態代理三部曲:下] - 從動態代理,看Retrofit的源碼實現

前言

關于動態代理的系列文章,到此便進入了最后的“一出好戲”。前倆篇內容分別展開了:從源碼上,了解JDK實現動態代理的原理;以及從動態代理切入,學會看class文件結構的含義。

如果還沒有看過這倆篇文章的小伙伴,可以看一看呦(前倆篇是一個小伙伴總結的,這一篇由我來續上。至于他會不會結合動態代理捋一捋Java中的AOP,那就看他了,emmmmmm~)

[動態代理三部曲:中] - 從動態代理,看Class文件結構定義

[動態代理三部曲:上] - 動態代理是如何"坑掉了"我4500塊錢

不扯這些沒用的直接開整!

上源碼

構建Retrofit對象

毫無疑問,分析源碼要先從使用凡是入手。對于我們正常的Retrofit套路,我們會先構建一個接口,這里我們使用一個post請求(這個接口已經不能用了,很久沒有倒騰我的服務器了~):

public interface RetrofitApi {String URL = "https://www.ohonor.xyz/";@POST("retrofitPost")@FormUrlEncodedCall<ResponseBody> postRetrofit(@Field("username") String username, @Field("password") String password);
}

然后,我們會通過Builder構建一個Retrofit:

Retrofit retrofit = new Retrofit.Builder().baseUrl(RetrofitApi.URL).addConverterFactory(ScalarsConverterFactory.create()).build();

對于構建Retrofit來說,從外部看就是通過Builder模式去構建。但是細節之處,并非如此,讓我們看一下baseUrl的內部實現。

public static @Nullable HttpUrl parse(String url) {Builder builder = new Builder();Builder.ParseResult result = builder.parse(null, url);return result == Builder.ParseResult.SUCCESS ? builder.build() : null;}

內部很簡單,通過builder.parase()的返回值來判斷是否應該去調用build()方法。因此很明顯,大量的邏輯是在parse()方法之中處理的。讓我們進去一睹芳澤:

此方法內容非常的長,本質就是對url進行準確性的校驗。這里我截取了一些較為關鍵的內容。

//這里是對HTTP請求類型的判斷,是http還是https,并且記錄一個下標pos。
if (input.regionMatches(true, pos, "https:", 0, 6)) {this.scheme = "https";pos += "https:".length();
} else if (input.regionMatches(true, pos, "http:", 0, 5)) {this.scheme = "http";pos += "http:".length();
} else {return ParseResult.UNSUPPORTED_SCHEME; 
}//接下來的內容,代碼過于的長,這里就不貼出來啦。主要內容就是對我們url常見的分隔符進行解碼。
//比如@和%40的相愛先殺。大家有興趣的話,可以自行查看一下源碼

url構建之前有一個比較經典的校驗過程:"baseUrl must end in /: " + baseUrl。這個異常大家都不陌生吧?~baseUrl必須以/結尾。這里的過程,大家有興趣可以自己看一下呦,原理是通過切割“/”字符串,來判斷是不是以“/”結尾。這里切的url并非是咱們的baseUrl,而是構建完畢的url。因為篇幅原因,這里就不貼代碼了。

動態代理部分

讓我們進入下一個過程,動態代理開始的地方。構建了Retrofit對象直接,我們就開始生成我們的接口對象啦,點進入之后,我們就能看到,屬于的動態代理的方法。還是熟悉的配方,熟悉的味道:

RetrofitApi retrofitApi = retrofit.create(RetrofitApi.class);public <T> T create(final Class<T> 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, @Nullable Object[] args)throws Throwable {// 如果該方法是來自Object的方法,則遵循正常調用。(正常來說,咱們也不會傳一個Object進來)if (method.getDeclaringClass() == Object.class) {return method.invoke(this, args);}//判斷是否是默認方法,這是1.8新增的內容。下文簡單展開一些:if (platform.isDefaultMethod(method)) {return platform.invokeDefaultMethod(method, service, proxy, args);}//這里才是我們重點關注的地方:ServiceMethod<Object, Object> serviceMethod =(ServiceMethod<Object, Object>) loadServiceMethod(method);OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);return serviceMethod.callAdapter.adapt(okHttpCall);}});}

默認方法: 是JDK1.8增加的接口中的內容。其關鍵字為default。(如果感興趣這個新特性,小伙伴們可以自行了解~)

官網解釋:如果此方法是默認方法,則返回true; 否則返回false。 默認方法:即在在接口類型中,聲明的具有主體的非靜態方法(有具體實現的)。(Returns true if this method is a default method; returns false otherwise. A default method is a public non-abstract instance method, that is, a non-static method with a body, declared in an interface type.)

倆種類型判斷結束,讓我們重點看一下:ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);這行代碼做了什么。我們點進去loadSerivceMethod()方法。

ServiceMethod<?, ?> loadServiceMethod(Method method) {ServiceMethod<?, ?> result = serviceMethodCache.get(method);if (result != null) return result;synchronized (serviceMethodCache) {result = serviceMethodCache.get(method);if (result == null) {result = new ServiceMethod.Builder<>(this, method).build();serviceMethodCache.put(method, result);}}return result;
}

很明顯,這里做了一次緩存。如果沒有ServiceMethod對象,那么就通過Builder的方式去構建這個對象。那么Buidler的過程是什么樣子的呢?

build()方法相對比較的長,這里我們看一些比較關鍵的地方。

關鍵點1:

拿到方法上的所有注解,然后遍歷:

for (Annotation annotation : methodAnnotations) {parseMethodAnnotation(annotation);
}

parseMethodAnnotation()方法:

private void parseMethodAnnotation(Annotation annotation) {if (annotation instanceof DELETE) {parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);} else if (annotation instanceof GET) {parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);} else if (annotation instanceof HEAD) {parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);if (!Void.class.equals(responseType)) {throw methodError("HEAD method must use Void as response type.");}} else if (annotation instanceof PATCH) {parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);} else if (annotation instanceof POST) {parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);} //省略一些注解類型
}

parseHttpMethodAndPath()方法中,主要做了一件事情:通過傳進來的注解對應的value去判斷是否有?,如果有,那么?后邊不能包含{}(通過正則表達式實現),否則拋異常。如果沒有拋異常,那么通過正則切割{},存到一個Set之中,后續進行處理,也就是和參數中的Path注解的內容進行替換。(下文會涉及替換過程)

關鍵點2:

遍歷過所有方法上的注解后,接下來就是參數注解了。

參數類型校驗:

到達這里,第一步進行的操作,是判斷參數類型。如果參數類型是TypeVariable(類型變量:T、V...)、WildcardType (通配符;?)則直接拋異常:

Type parameterType = parameterTypes[p];if (Utils.hasUnresolvableType(parameterType)) {throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",parameterType);
}static boolean hasUnresolvableType(Type type) {if (type instanceof Class<?>) {return false;}//省略遞歸遍歷的過程if (type instanceof GenericArrayType) {return hasUnresolvableType(((GenericArrayType) type).getGenericComponentType());}if (type instanceof TypeVariable) {return true;}if (type instanceof WildcardType) {return true;}
}

參數類型完畢后,便進入參數注解類型的判斷。

參數注解類型校驗:

正式校驗參數注解類型的時候,會先判斷是否有不含注解的參數,這里就會直接拋異常(也就是我們為什么不能在參數中傳不用注解修飾參數報錯的原因):

Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
if (parameterAnnotations == null) {throw parameterError(p, "No Retrofit annotation found.");
}

接下來便是校驗參數注解類型。不過,這一部分實在沒辦法貼出來了,核心的判斷方法大概有400行。為啥這么多?因為參數注解類型太多了,每一種都有自己的規則,所以判斷內容很多。如果小伙伴有感興趣的,可以自行去ServiceMethod類中的parseParameterAnnotation()方法查看。

請求接口Api類中,注解使用的異常。基本都是在這里處理的。如果小伙伴們遇到什么奇怪的異常,不妨不著急去百度/Google;讓我們看看源碼是怎么說的~~

Path替換{}的內容

這里我們解決一個疑問:那就是我們最開始處理url的時候,通過正則切割{},我們都知道,這里會通過Path注解去替換。那么這里就讓我們看一看Retrofit是如何處理Path類型的注解的。

else if (annotation instanceof Path) {//省略部分內容Path path = (Path) annotation;String name = path.value();validatePathName(p, name);Converter<?, String> converter = retrofit.stringConverter(type, annotations);return new ParameterHandler.Path<>(name, converter, path.encoded());
}

這里我們能看到,想進行接下來的操作。必然和Converter這個類有著密不可分的關系。

public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotations) {// 省略判空及緩存取值操作。return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
}

我們可以看到,第一次一定是沒有Converter對象的。點進INSTANCE之后我們會發現這里構建了一個ToStringConverter類。初始化之后,再讓我們回到Path類型中的判斷里。最終我們會return一個return new ParameterHandler.Path<>(name, converter, path.encoded());很明顯這是一個內部類。其實它是一個封裝類。對應封裝了所有注解對應的java類。用于在請求網絡的時候統一管理。而這個類只需要重寫了apply方法。

static final class Path<T> extends ParameterHandler<T> {//省略構造方法@Override void apply(RequestBuilder builder, @Nullable T value) throws IOException {//省略拋異常。我們Path替換{}的過程就在下面這個方法中。builder.addPathParam(name, valueConverter.convert(value), encoded);}
}void addPathParam(String name, String value, boolean encoded) {//省略拋異常,看到replace應該很清楚了吧。relativeUrl = relativeUrl.replace("{" + name + "}", canonicalizeForPath(value, encoded));}

當然,執行replace勢必要引起apply方法的調用。很顯然目前在動態代理的這個過程中,我們沒有辦法看到apply被調用。因此現在先按住不表,讓我們先把動態代理部分整完。

newProxyInstance的return

我們上面看了,校驗接口方法的參數類型/參數注解類型。這個邏輯過后,就是調用build,構建ServiceMethod。

public ServiceMethod build() {// 省略上訴的檢驗過程return new ServiceMethod<>(this);
}

構建完了ServiceMethod之后,讓我們再把目光轉移到Retrofit.create()中newProxyInstance的最后一點內容:

ServiceMethod<Object, Object> serviceMethod =(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

走到這,就通過動態代理構建出了我們接口方法中的Call對象。從這三行代碼中,我們很明顯看不出來貓膩,讓我們走進OkHttpCall中:

final class OkHttpCall<T> implements Call<T>

這其中重寫了Call中我們常用的方法,比如:enqueue()。內部是轉發給okhttp3.Call(OkHttp)去處理真正的網絡請求。
接下來讓我們重點看一下return的serviceMethod.callAdapter.adapt(okHttpCall)方法。這里callAdapter的初始化就不展開,默認的是DefaultCallAdapterFactory:

這里我們因為沒有設置適配的Adapter,比如:RxJava的。

final class DefaultCallAdapterFactory extends CallAdapter.Factory {//省略構造方法@Overridepublic CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {//省略判空final Type responseType = Utils.getCallResponseType(returnType);return new CallAdapter<Object, Call<?>>() {@Override public Type responseType() {return responseType;}@Override public Call<Object> adapt(Call<Object> call) {return call;}};}
}

看到這個類,我們就可以明確,這了返回的Call實際上就是我們動態代理中傳遞的OkHttpCall。

return serviceMethod.callAdapter.adapt(okHttpCall);
有了它,我們就可以執行我們想執行的網絡請求的方法了。

那么此時我們就可以這么做了:

Call<ResponseBody> call = retrofitApi.postRetrofit(username,password);
call.enqueue(....);

動態代理部分接近尾聲

走到這里,動態代理部分就結束了。不過我們還有一些問題沒有看到結果。最簡單的,上面所說的apply方式是誰調用的?其實這個問題很好解答。

我們通過上面的梳理,可以明確動態代理的部分僅僅是為了構建我們的接口類,而真正的調用并非在此。因此我們可以推斷出apply的調用時機應該是正在去請求網絡的時候。

因為本篇的主題是梳理Retrofit中動態代理的部分。所以關于真正請求的部分,就簡單的進行總結下見諒了,各位

我們知道,我們正真請求網絡是調用了Call中的方法:

public interface Call extends Cloneable {Request request();Response execute() throws IOException;void enqueue(Callback responseCallback);
}

那么Call的實現類是怎么被創建出來的呢?其中,上文我們已經看到,在newProxyInstance方法中return的時候,初始化的OkHttpCall。既然知道了Call的實現類是什么,那么我們就取其中比較有代表性的方法,來展開apply被調用的過程。

這里我們展開enqueue()方法做代表吧:

@Override public void enqueue(final Callback<T> callback) {checkNotNull(callback, "callback == null");okhttp3.Call call;Throwable failure;//省略判空,同步等操作call = rawCall = createRawCall();//省略真正發起請求的過程。
}private okhttp3.Call createRawCall() throws IOException {//apply就在此方法中被調用Request request = serviceMethod.toRequest(args);okhttp3.Call call = serviceMethod.callFactory.newCall(request);//省略拋異常return call;
}Request toRequest(@Nullable Object... args) throws IOException {//省略無關的代碼for (int p = 0; p < argumentCount; p++) {//到此我們的apply就被調用了。handlers[p].apply(requestBuilder, args[p]);}return requestBuilder.build();
}

在這我們就很清晰的看到apply方法被調用~

總結

我們的Retrofit,通過動態代理,構建我們所需要的接口方法,其中校驗我們的接口方法的注解,參數類型,參數注解類型;構建ServiceMethod對象,最終通過OkHttpCall,return出我們所需要的Call類型對象。
有了Call,我們就可以開始網絡請求,當然網絡請求的過程,在OkHttpCall中是被轉發給OkHttp框架中的okhttp3.Call去執行的。

到此,從動態代理,看Retrofit的源碼實現就結束了。這篇文章重點是去分析Retrofit中的動態代理的思路,所以在網絡請求的源碼過程并沒有過多的涉獵。有機會的話,在Retrofit的源碼實現中去總結吧。

在看源碼的過程中,最大的感慨是框架設計上的巧妙。自己最近在重構公司的相機庫,越來越感覺整體設計的重要性!唉,好難。


這里是一個應屆生/初程序員公眾號~~歡迎圍觀

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,已經我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

img_39e544ceb47ae92ede4c13f05f18ed09.png
個人公眾號:IT面試填坑小分隊

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/253727.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/253727.shtml
英文地址,請注明出處:http://en.pswp.cn/news/253727.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Ti的DM368系列芯片的所有PDF資料匯總

http://www.ti.com/sc/docs/psheets/man_dsp.htm

劉浩(專業打劫三十年)20155307的預備作業02:

我的技能&#xff1f;比大多數人好&#xff1f;經驗是什么&#xff1f;與老師的經驗的共同之處&#xff1f; 我的技能之一就是單詞翻譯王——其實看了婁老師的學習經驗之后便有些自慚形穢了&#xff0c;我目前的單詞量是7300,扇貝上測的&#xff0c;而且測試時是嚴格的“不會就…

數字后端——電源規劃

電源規劃是給整個芯片的供電設計出一個均勻的網絡&#xff0c;它是芯片物理設計中非常關鍵的一部分。電源規劃在芯片布圖規劃后或在布圖規劃過程中交叉完成,它貫穿于整個設計中&#xff0c;需要在芯片設計的不同階段對電源的供電網絡進行分析并根據要求進行修改。&#xff0c;主…

逆向project實戰--Acid burn

0x00 序言 這是第二次破解 crackme 小程序&#xff0c;感覺明顯比第一次熟練。破解過程非常順利&#xff0c;差點兒是分分鐘就能夠找到正確的 serial&#xff0c;可是我們的目標是破解計算過程。以下將具體介紹。 0x01 初次執行 剛開始拿到 crackme 先執行程序。看看有哪些明顯…

PyCharm使用技巧(六):Regullar Expressions的使用

2019獨角獸企業重金招聘Python工程師標準>>> PyCharm v2018.2最新版本下載 使用正則表達式查找和替換文件中的文本 示例代碼 使用正則表達式查找和替換字符串 假設您想用擴展標記<title> </title>替換元素&#xff08;title&#xff09;中的屬性&#x…

內核中_init,_exit中的作用

__init&#xff0c; __initdata等屬性標志&#xff0c;是要把這種屬性的代碼放入目標文件的.init.text節&#xff0c;數據放入.init.data節──這一過程是通過編譯內核時為相關目標平臺提供了xxx.lds鏈接腳本來指導ld完成的。 對編譯成module的代碼和數據來說&#xff0c;當模…

jQuery筆記總結

來源于&#xff1a;http://blog.poetries.top/2016/10/20/review-jQuery/ http://www.jianshu.com/p/f8e3936b34c9 首先&#xff0c;來了解一下jQuery學習的整體思路 第一節 jQuery初步認知 jQuery概述 JQuery概念 javascript概念 基于Js語言的API和語法組織邏輯&#xff0c;通…

芯片生產流程

每個半導體產品的制造都需要數百個工藝&#xff0c;泛林集團將整個制造過程分為八個步驟&#xff1a;晶圓加工-氧化-光刻-刻蝕-薄膜沉積-互連-測試-封裝。 一、晶圓加工 所有半導體工藝都始于一粒沙子&#xff01;因為沙子所含的硅是生產晶圓所需要的原材料。晶圓是將硅(Si)或砷…

GRE Sub math 報名

Step1 注冊ETS帳號 Step2 登錄帳號&#xff0c;點擊Register/Find Test Centers, Dates Step3 按照提示查詢考場 如果沒有結果而是出現了如下提示&#xff0c;意味著這個地方沒有考位了&#xff0c;需要選擇其他地方的考位 Step 4 接下來就和GRE general test的過程一樣了&…

????platform_device_系列函數及其設備注冊的作用

platform_device_系列函數&#xff0c;實際上是注冊了一個叫platform的虛擬總線。使用約定是如果一個不屬于任何總線的設備&#xff0c;例如藍牙&#xff0c;串口等設備&#xff0c;都需要掛在這個虛擬總線上。 driver/base/platform.c //platform設備聲明 struct device pla…

示例解讀 Python 2 和 Python 3 之間的主要差異

開發四年只會寫業務代碼&#xff0c;分布式高并發都不會還做程序員&#xff1f; 每門編程語言在發布更新之后&#xff0c;主要版本之間都會發生很大的變化。 在本文中&#xff0c;Vinodh Kumar 通過示例解釋了 Python 2 和 Python 3 之間的一些重大差異&#xff0c;以幫助說明…

數字后端——布局

由于I / O單元和模塊的布放已經在布圖規劃時完成&#xff0c;因此布局的剩余任務主要是對標準單元的布局。布局方案在布圖規劃時就已經做了決定&#xff0c;要么選擇展平式布局&#xff0c;要么就是層次化布局。 一、布局目標 布局的目標也即布局內容實施之后所要達到的預期值…

python基礎 函數 (四)

一 函數基本 def func1():print("hello world")return 1, "hello", ("wo", "ai"), ["ni", "da"], {"you": "xi"} # return 可以返回任意# 結果&#xff1a; (1, hello, (wo, ai), [ni, da…

c#注釋

c#的注釋分為&#xff1a;這里不能不說一下什么是注釋。 注釋本身不會執行&#xff0c;只是說明性文字&#xff0c;只供程序員閱讀。 注釋又分為&#xff1a;單行注釋&#xff0c;多行注釋&#xff0c;文檔注釋。 單行注釋&#xff1a;//開始 多行注釋&#xff1a;/*開始&#…

嵌入式linux字符設備驅動

1. 我們需要先調用register_chrdev_region()或 alloc_chrdev_region()來向系統申請設備號int register_chrdev_region( dev_t first, unsigned int count, char *name ); //函數通過已知的設備號first來注冊字符設備區域。 int alloc_chrdev_region( dev_t *dev, unsigned int…

數字后端——時鐘樹綜合

在數字集成電路設計中&#xff0c;時鐘信號是數據傳輸的基準&#xff0c;它對于同步數字系統的功能、性能和穩定性起決定性作用&#xff0c;所以時鐘信號的特性及其分配網絡尤被人們關注。時鐘信號通常是整個芯片中有最大扇出、通過最長距離、以最高速度運行的信號。時鐘信號必…

52次課(mysql用戶管理、常用sql語句、 mysql數據庫備份恢復)

MySQL創建用戶以及授權 默認用戶是root用戶&#xff0c;不可能所有人員都用root用戶&#xff0c;創建用戶防止誤刪除&#xff0c;因為mysql里邊有多個庫每個庫里有很多表&#xff0c;所以需要給單獨的用戶做一些授權我只需要它對某一個數據庫有權限&#xff0c;或者說對某個數據…

線程池的種類,區別和使用場景

newCachedThreadPool&#xff1a; 底層&#xff1a;返回ThreadPoolExecutor實例&#xff0c;corePoolSize為0&#xff1b;maximumPoolSize為Integer.MAX_VALUE&#xff1b;keepAliveTime為60L&#xff1b;unit為TimeUnit.SECONDS&#xff1b;workQueue為SynchronousQueue(同步隊…

20145225 《信息安全系統設計基礎》第14周學習總結

第九章 虛擬存儲器 虛擬存儲器是計算機系統最重要的概念之一&#xff0c;它是對主存的一個抽象 三個重要能力&#xff1a; 它將主存看成是一個存儲在磁盤上的地址空間的高速緩存&#xff0c;在主存中只保存活動區域&#xff0c;并根據需要在磁盤和主存之間來回傳送數據&#xf…

數字后端——布線

布線是繼布局和時鐘樹綜合之后的重要物理實施任務&#xff0c;其內容是將分布在芯片核內的模塊、標準單元和輸入輸出接口單元( I /O pad&#xff09;按邏輯關系進行互連&#xff0c;其要求是百分之百地完成它們之間的所有邏輯信號的互連&#xff0c;并為滿足各種約束條件進行優…