preface

The OkHttp source code has been shared before (and is very detailed and long), but in real projects Retrofit is often used to do network request work. Retrofit is RESTful and essentially a wrapper around OkHttp. Today we’ll take a closer look at Retrofit’s source code and design ideas with a few questions.

1. Use method

Take a look at the official usage.

public final class SimpleService {
  public static final String API_URL = "https://api.github.com";

  public static class Contributor {
    public final String login;
    public final int contributions;

    public Contributor(String login, int contributions) {
      this.login = login;
      this.contributions = contributions; }}public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
  }

  public static void main(String... args) throws IOException {
    // Create a very simple REST adapter which points the GitHub API.
    Retrofit retrofit =
        new Retrofit.Builder()
            .baseUrl(API_URL)
            .client(new OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS).build())
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    // Create an instance of our GitHub API interface.
    GitHub github = retrofit.create(GitHub.class);

    // Create a call instance for looking up Retrofit contributors.
    Call<List<Contributor>> call = github.contributors("square"."retrofit");

    // Fetch and print a list of the contributors to the library.
    List<Contributor> contributors = call.execute().body();
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + "(" + contributor.contributions + ")"); }}}Copy the code

It can be summarized in three simple steps:

  1. buildretrofitInstance.
  2. buildAPIInterface instance.
  3. Execute the request and parse the response.

2. Process analysis

Let’s analyze its process according to how it is used.

2.1 Building Retrofit instances

You can see from the usage that the builder pattern is used to build instances.

Retrofit retrofit =
    new Retrofit.Builder()
        .baseUrl(API_URL)
        .client(new OkHttpClient().newBuilder().connectTimeout(10. TimeUnit.SECONDS).build()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build();Copy the code

I’m not going to do this, but let’s look at some parameters.

public static final class Builder {
    // The actual request invocation, such as okHttp3.okHttpClient
    private @Nullable okhttp3.Call.Factory callFactory;
    // Basic URL, such as domain name
    private @Nullable HttpUrl baseUrl;
    // List of data converters
    private final List<Converter.Factory> converterFactories = new ArrayList<>();
    // Request a list of adapters
    private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();
Copy the code

2.2 Building AN API Instance

According to the official usage description, we will put our API methods in an interface, and then set the request parameters through annotations. When used, this interface is instantiated through the retrofit.create(Class

) method and then its methods are called. Such as:

public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
}

// Instantiate the API interface
GitHub github = retrofit.create(GitHub.class);
// Call an API in the interface
Call<List<Contributor>> call = github.contributors("square"."retrofit");
Copy the code

Take a look at the source code

public <T> T create(final Class<T> service) {
    // Verify the API service
    validateServiceInterface(service);
    return (T)
        // Dynamic proxy mode is used here, service is the proxy class
        // Why did Todo use dynamic proxies and what are the benefits? How about something else?
        Proxy.newProxyInstance(
            service.getClassLoader(),
            newClass<? >[] {service},new InvocationHandler() {
              private final Object[] emptyArgs = new Object[0];

              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args); } args = args ! =null ? args : emptyArgs;
                Platform platform = Platform.get();
                // If it is not the default method, return a ServiceMethod via the loadServiceMethod() method and call the invoke method
                returnplatform.isDefaultMethod(method) ? platform.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args); }}); }Copy the code

Did two things:

  1. Verify our API interface class.
  2. Use dynamic proxies to instantiate the API interface at run time.
  private void validateServiceInterface(Class
        service) {
    // Service must be interface, otherwise an exception is thrown
    if(! service.isInterface()) {throw new IllegalArgumentException("API declarations must be interfaces."); }... Omit code...// Whether to immediately validate all methods in the API interface, set by the user, default is false
    if (validateEagerly) {
      Platform platform = Platform.get();
      // Iterate over all methods defined in the service
      for (Method method : service.getDeclaredMethods()) {
        // The loadServiceMethod method is executed if the method is not the system default and the method decorator is not static
        if(! platform.isDefaultMethod(method) && ! Modifier.isStatic(method.getModifiers())) {// Load the request method.loadServiceMethod(method); }}}}Copy the code

From this we can also see that our API methods must be in the method interface. If you start validating the interface, it iterates through all its declared methods, filtering out the system default and static methods, and then loadServiceMethod(method) is executed.

To expand:

GetMethods (): Returns all public methods declared by a class or interface and inherited from superclasses and superinterfaces. GetDeclaredMethods (): Returns class declared methods, including public, protected, default (package), but not inherited methods. Therefore, getDeclaredMethods is faster than getMethods, especially in complex classes such as the Activity class.

Ultimately, a ServiceMethod is loaded using the loadServiceMethod(method) method.

See HttpServiceMethod. ParseAnnotations () method, which I simplified, as follows:

HttpServiceMethod.java

  static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {

    // Get the annotation information for the method
    Annotation[] annotations = method.getAnnotations();
    / / adapter type, is to Retrofit addCallAdapterFactory add the type of ().
    Type adapterType;
    // Return type of the method
    adapterType = method.getGenericReturnType();


    // instantiate a CallAdapter object
    CallAdapter<ResponseT, ReturnT> callAdapter =
        createCallAdapter(retrofit, method, adapterType, annotations);
    // Check responseType and throw an exception if it fails
    Type responseType = callAdapter.responseType();

    // Instantiate a Converter object that converts okHttp3. ResponseBody to ResponseT
    Converter<ResponseBody, ResponseT> responseConverter =
        createResponseConverter(retrofit, method, responseType);

    okhttp3.Call.Factory callFactory = retrofit.callFactory;
    // Instead of suspending the kotlin method, return callStuck, which calls callAdapter.adapter
    return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
  }
Copy the code

After the ServiceMethod is instantiated, the invoke method is called.

HttpServiceMethod.java

  @Override
  final @Nullable ReturnT invoke(Object[] args) {
    // Create an OkHttpCall request
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    // Then call the adapt method, CallAdapted overwrites the adapt method, and calls the callAdapter.adapt(call) method
    return adapt(call, args);
  }

  protected abstract @Nullable ReturnT adapt(Call<ResponseT> call, Object[] args);
Copy the code

As you can see from the above code, the Invoke method instantiates a Call request and then invokes an Adapter method. In this case, Adapter is an abstract method, so the implementation of the method depends on its implementation class, CallAdapter. CallAdapter is through here. AddCallAdapterFactory CallAdapter add () method, and according to the platform provided by default DefaultCallAdapterFactory CallAdapter, The Adapter method is executed, and Callis returned.

2.3 Execute the request and parse the response

In the previous step, we instantiated the API interface and adapted the request through a CallAdapter, resulting in a CallObject.

The next step is to execute the Callrequest and finally get the Object we want.

For example, as described in the initial usage method:

// You've got the Call
      
       > object, run the Call and get List
       
      
List<Contributor> contributors = call.execute().body();
Copy the code

Execute executes the synchronous request to get the Response and then the request body.

OkHttpCall.java

  @Override
  public Response<T> execute(a) throws IOException {
    okhttp3.Call call;

    synchronized (this) {
      // Determine if the request has been executed and throw an exception if it has been executed
      if (executed) throw new IllegalStateException("Already executed.");
      executed = true;
      // Get the original request and create okHttp3.call with createRawCall()
      call = getRawCall();
    }

    if (canceled) {
      call.cancel();
    }
    // Execute the request and parse the response, converting okHttp3.response to retrofit2.response
    return parseResponse(call.execute());
  }

  private okhttp3.Call createRawCall(a) throws IOException {
    // Construct the original request
    okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
    if (call == null) {
      throw new NullPointerException("Call.Factory returned null.");
    }
    return call;
  }

  /** * parse the response, which translates okhttp3.response to retrofit2.response */
  Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {... Omit code...try {
      // Convert to the desired type using Converter
      T body = responseConverter.convert(catchingBody);
      return Response.success(body, rawResponse);
    } catch(RuntimeException e) { ... Omit code... }Copy the code

As you can see from the source code, the actual work of the request is done through okhttp. Retrofit is responsible for the request and response conversion, converting retroFIT2. Call to okHttp3. Call. Convert okhttp3.response to retrofit2.response.

3. Why introduce CallAdapter and Converter?

If you’re familiar with okHttp, you know that when we make a request, we convert the request to a Call object using the okHttpClient.newCall (Request) method, and then execute the Call object to get the response.

But Retrofit doesn’t just support calls. It can adapt requests to observables, which can be used in conjunction with RxJava2. This is adapted by CallAdapter work, for example by default DefaultCallAdapterFactory transform the request into the Call < Object >, The RxJava2CallAdapter transforms the request into Observable.

Going back to okHttp, in most business cases we deserialize the response body as an object for easy invocation. Obviously Retrofit has this in mind, so it provides a GsonConverterFactory by default to help us with this step of deserialization. This is done through The Converter, which also allows users to customize.

4. How does the CallAdapter work?

As a request adapter, we split the CallAdapter workflow into three steps: Add, match, and work.

add

The request adapterFactory class can be added using the addCallAdapterFactory(callAdapterFactory) method, which is then saved in the callAdapterFactories list. In addition, the Retrofit will upon the request of the Platform to add default, the adapter, such as: DefaultCallAdapterFactory, etc., also joined the callAdapterFactories list.

matching

Think about it: All added request adapters are stored in the callAdapterFactories list, so how do you match them in the actual request?

In HttpServiceMethod. ParseAnnotations () method, we have to instantiate a CallAdapter object. (Please refer back to 2.2 Building API Instances for details.)

HttpServiceMethod.java

  static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {

    // instantiate a CallAdapter objectCallAdapter<ResponseT, ReturnT> callAdapter = createCallAdapter(retrofit, method, adapterType, annotations); ... omit code// Instead of suspending the kotlin method, return callStuck, which calls callAdapter.adapter
    return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
  }
Copy the code

The matching work is done in the createCallAdapter() method, working its way down to retrofit.nextCallAdapter () :

Retrofit.java

  publicCallAdapter<? ,? > nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {

    int start = callAdapterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
      // Find a matching CallAdapter based on the method return value type and annotation informationCallAdapter<? ,? > adapter = callAdapterFactories.get(i).get(returnType, annotations,this);
      if(adapter ! =null) {
        returnadapter; }} ·· omit code ···// If no matching CallAdapter is found, an exception is thrown
    throw new IllegalArgumentException(builder.toString());
  }
Copy the code

If no matching CallAdapter is found, throw an IllegalArgumentException.

work

Once you find the matching CallAdapter, all that’s left is to see how it works.

As described in the previous matching procedure, after a matching callAdapter is found, a CallStuck object is instantiated from it.

  static final class CallAdapted<ResponseT.ReturnT> extends HttpServiceMethod<ResponseT.ReturnT> {
    private final CallAdapter<ResponseT, ReturnT> callAdapter;

    CallAdapted(
        RequestFactory requestFactory,
        okhttp3.Call.Factory callFactory,
        Converter<ResponseBody, ResponseT> responseConverter,
        CallAdapter<ResponseT, ReturnT> callAdapter) {
      // pass responseConverter to the parent class.
      super(requestFactory, callFactory, responseConverter);
      this.callAdapter = callAdapter;
    }

    @Override
    protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
      returncallAdapter.adapt(call); }}Copy the code

CallAdapted simply inherits HttpServiceMethod and overwrites the Adapt method. In other words, the adapt method of the CallAdapter object that we matched in the previous step will be executed.

Such as matching to the CallAdapter DefaultCallAdapterFactory, finally is the adapt methods of execution, the specific code details here did not show, interested students, please consult.

In addition, I’m showing a case where the Kotlin suspend function is not supported. Of course, even if the Kotlin suspend function is implemented, the adapt method of its subclass is the same process.

5. How does Converter work?

As data converters, we also divide the Converter workflow into three steps: Add, match, and work.

add

The data ConverterFactory class can be added using the addConverterFactory(ConverterFactory) method, which will be saved in the converterFactories list. In addition, Retrofit adds default data converters based on Platform, such as OptionalConverterFactory, to the converterFactories list.

matching

With the above described 4. CallAdapter works, in the same HttpServiceMethod. ParseAnnotations () method, will instantiate an object of the Converter.

Match the job in createResponseConverter () method, step by step, finally to Retrofit. The nextResponseBodyConverter () method:

  public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
      @Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {

    int start = converterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = converterFactories.size(); i < count; i++) {
      // Find a matching Converter by converting the type to the annotation informationConverter<ResponseBody, ? > converter = converterFactories.get(i).responseBodyConverter(type, annotations,this);
      if(converter ! =null) {
        //noinspection unchecked
        return(Converter<ResponseBody, T>) converter; }} ·· omit code ···// If no matching Converter is found, an exception is thrown
    throw new IllegalArgumentException(builder.toString());
  }
Copy the code

To recap, we traverse the converterFactories list to find a matching Converter by converting the type and annotation information, and throw an IllegalArgumentException if none is found.

work

As described above in 4. How the CallAdapter works, we instantiate a Callstuck by finding a matching Converter. The difference is that we pass responseConverter to the parent class, HttpServiceMethod, and when it calls the Invoke method, we instantiate an OkHttpCall object via responseConverter. Finally, the OkHttpCall object is passed to the Adapter method for execution.

When performing the request, eventually OkHttpCall execution parseResponse to parse the response, call responseConverter. Convert () method, transform ResponseBody data type we want.

6. Describe which design patterns are used

Dynamic proxy mode

Retrofit internally uses dynamic proxy + reflection to take the request parameters defined by the user in the interface to build the actual request. I won’t go into details here, but refer back to 2.2 Building an API Instance.

Why use dynamic proxies to get API methods?

I don’t know if you have this question, but why do our API methods need to be defined in interface? Why use dynamic proxy + reflection to retrieve request parameters?

Retrofit is designed in a RESTful style and annotated to define request parameters for API methods and put those API methods into the interface. Since an interface cannot be instantiated, a dynamic proxy is used to instantiate the API interface at run time to get the method’s request parameters.

Further:

Decoupled, isolating the real business from Retrofit. The user only needs to define the request parameters through the annotation method, while the actual request is built internally through Retrofit.

Now, why do I put it in interface? I’m sure you already have the answer.

The strategy pattern

The strategy pattern can be used when there are multiple approaches to the same type of problem, but the specific behavior is different.

For example: request adapters in Retrofit

  • Abstract strategy:CallAdapter.
  • Specific implementation of the strategy:DefaultCallAdapterFactory.get(),RxJava2CallAdapter.

Provide the default request adapter, but also support user customization, in line with the open and closed principle, to achieve good scalability.

Adapter mode

Will help us to build the actual request Retrofit, internal to transform the request into the Call by the default DefaultCallAdapterFactory < Object >, Retrofit also support other platforms at the same time, such as in order to fit RxJava features, Transform the request into Observable.

  • Target:Call<Object>.Observable<Object>.
  • An object that needs to be adapted:OkHttpCall.
  • Adapter:DefaultCallAdapterFactory.get(),RxJava2CallAdapter.

Factory method pattern

Let’s take the Converter for example.

  • Abstract factory:Converter.Factory.
  • Specific factory:GsonConverterFactory,BuiltInConvertersAnd so on.
  • Abstract products:Converter.
  • Specific products:GsonResponseBodyConverter,GsonRequestBodyConverter,ToStringConverterAnd so on.

There is no specific analysis of each class, interested students can consult.

Builder model

The Builder pattern was used when building an instance of Retrofit. The Builder pattern appears quite frequently in open source libraries, and it is common to use the builder pattern because it makes sense to provide a variety of parameters and methods to suit the needs of different users.

7. What pits have you stepped in during use?

One day I changed BaseUrl and found that the server kept returning 404 when I requested the interface. However, when I tried to debug the interface with Postman, I found that the interface was fine, so I guessed that there was something wrong with my code.

The problem turned out to be that the API had been removed when the full URL was concatenated.

Base URL: http://example.com/api/
Endpoint: /foo/bar/ 
Result: http://example.com/foo/bar/
Copy the code

The correct Endpoint is as follows: The Endpoint does not start with a slash.

Base URL: http://example.com/api/ 
Endpoint: foo/bar/ 
Result: http://example.com/api/foo/bar/
Copy the code

conclusion

This article, in the form of a few questions, takes a look at Retrofit’s source code and design ideas, and hopefully you’ll have a better understanding of the source code. Retrofit is essentially just a encapsulation of okHttp. The goal is definitely to make web requests easier to make. It’s amazing how Many design patterns Jake Wharton has used.

That concludes the source code parsing for Retrofit.

This article is a bit different from my previous source code analysis articles. I have deliberately simplified a lot of the code to make it easier for you to read, but I have also commented the code in detail, which you can refer to in my Retrofit detailed code comments.

If you take a closer look at the OkHttp source code analysis, you can check out my other (very detailed and long) OkHttp source code analysis article.

In fact, the biggest purpose of sharing articles is to wait for someone to point out my mistakes. If you find any mistakes, please point them out without reservation and consult with an open mind.

In addition, if you think this article is good and helpful, please give me a like as encouragement, thank you ~ Peace~!