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

weixin_33797791發表於2018-08-22

前言

關於動態代理的系列文章,到此便進入了最後的“一出好戲”。前倆篇內容分別展開了:從原始碼上,瞭解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的原始碼實現中去總結吧。

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


這裡是一個應屆生/初程式設計師公眾號~~歡迎圍觀

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

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

相關文章