前言
關于動態代理的系列文章,到此便進入了最后的“一出好戲”。前倆篇內容分別展開了:從源碼上,了解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的源碼實現中去總結吧。
在看源碼的過程中,最大的感慨是框架設計上的巧妙。自己最近在重構公司的相機庫,越來越感覺整體設計的重要性!唉,好難。
這里是一個應屆生/初程序員公眾號~~歡迎圍觀
我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,已經我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~
