前言
關於動態代理的系列文章,到此便進入了最後的“一出好戲”。前倆篇內容分別展開了:從原始碼上,瞭解JDK實現動態代理的原理;以及從動態代理切入,學會看class檔案結構的含義。
如果還沒有看過這倆篇文章的小夥伴,可以看一看呦~(前倆篇是一個小夥伴總結的,這一篇由我來續上。至於他會不會結合動態代理捋一捋Java中的AOP,那就看他了,emmmmmm~~)
[動態代理三部曲:中] - 從動態代理,看Class檔案結構定義
[動態代理三部曲:上] - 動態代理是如何"坑掉了"我4500塊錢
不扯這些沒用的直接開整!
上原始碼
構建Retrofit物件
毫無疑問,分析原始碼要先從使用凡是入手。對於我們正常的Retrofit套路,我們會先構建一個介面,這裡我們使用一個post請求(這個介面已經不能用了,很久沒有倒騰我的伺服器了~):
public interface RetrofitApi {
String URL = "https://www.ohonor.xyz/";
@POST("retrofitPost")
@FormUrlEncoded
Call<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 {
//省略構造方法
@Override
public 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的原始碼實現中去總結吧。
在看原始碼的過程中,最大的感慨是框架設計上的巧妙。自己最近在重構公司的相機庫,越來越感覺整體設計的重要性!唉,好難。
這裡是一個應屆生/初程式設計師公眾號~~歡迎圍觀
我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,已經我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~