preface

Normally I don’t like to write articles about analyzing source code, pipelined-style analysis is boring, but reading the Retrofit source code made me want to change my mind

In general, there are two benefits to reading source code:

  • Familiar with code design process, use the process of problems can be more quickly solved. To be honest, this alone doesn’t excite my interest in reading the source code, after all, using one in the right mannergoodThe framework should not have this problem.
  • A good framework must ensure thatEase of use, scalability“, so the author will introduce a lot of thinking to the design, if we can absorb one or two, it is not a spiritual interaction with the author!

Today, I will try to analyze the principle of Retrofit from the perspective of a designer with my understanding. I believe that you should read it carefully and think about it again. When the interviewer asks about Retrofit, your reply may make him/her eyes light up

Tip :Retrofit is based on 2.9.0. Some of the source code may be missing, but this is my intention to filter out useless information and improve readability

directory

  • 1. What is a REST ful API?
  • 2. Why is the request set to (interface + annotation) form?
    • 2.1 Demeter’s Law and the facade model
    • 2.2 Why to design ApiService through facade mode?
  • 3. Dynamic proxies are not tools
    • 3.1 Retrofit building
    • 3.2 What is dynamic Proxy?
    • 3.3 Obtaining the ApiService through Dynamic Proxy
  • 4. What’s the point of ReturnT, ResponseT?
    • 4.1 create HttpServiceMethod
    • 4.2 How Do I Manage the callAdapter and responseConverter?
    • 4.3 Initiating a Request

1. What is a REST ful API?

REST FUL API: When using THE HTTP protocol for data transfer, we should follow the HTTP rules, including request method, resource type, Uri format, etc..

Not long ago, I saw a friend in the group raise a question: “We need to add the Body in the GET request as required by the backend, but the Body in the GET request in Retrofit will report an error, how to solve it?” For a time the discussion is very lively, there are let the Body into the Header, there are custom interceptors, some people directly encourage to change the source code… But isn’t the nature of the problem back end first? You can’t arrest the one who got into a fight.

In this case, let the wrong party fix it, because everyone knows that GET has no Body. Otherwise, your code is easily confused once someone else takes over.

Retrofit does a very good job of being compliant with the REST ful API, and notifying you directly if you don’t conform to the specification, forcing your code to be compliant. So your company is using the REST ful API and Retrofit is a great choice for you

2. Why is the request set to (interface + annotation) form?

This section is pre-knowledge

2.1 Demeter’s Law and the facade model

  • Demeter’s law: also known asPrinciple of least knowingThat is, to reduce unnecessary dependencies between modules, that is, to reduce the coupling between modules.
  • Facade mode: based onDemeter's lawAn extended formDesign patternsIs designed to be complexModules/systemsAccess point control is more single. For example: now want to do a get photo function, priority, was obtained from the local cache without the cache from network access to add to the local cache, if do not do any processing, that each to get a picture to write caching logic, again write error may be higher, the more actually the caller just wanted to get a picture, specific how to obtain he doesn’t need to care about. In this case, you can use the facade pattern to encapsulate the cache function, exposing only one entry to get the picture, which makes it easier and more secure for the caller to use. So is functional programmingFacade patternThe product of

2.2 Why to design ApiService through facade mode?

Making a request with Retrofit is a rough process:

interface ApiService {
    /** * Get home page data */
    @GET("/article/list/{page}/json")
    suspend fun getHomeList(@Path("page") pageNo: Int)
    : ApiResponse<ArticleBean>
}

/ / build Retrofit
val retrofit = Retrofit.Builder().build()

// Create an ApiService instance
val apiService =retrofit.create(ApiService::class.java)

// Make a call request (this is a call request that is automatically initiated by suspend and can be returned by Java)
apiService.getHomeList(1)

Copy the code

You then create an instance of the ApiService type via Retrofit and invoke the corresponding method to initiate the request. At first glance, it may seem ordinary, but Retrofit actually filters out a lot of useless information with this mode (the facade mode)

Tips: We all know that Retrofit is nothing more than a wrapper around OkHttp.

If you use OkHttp directly, there is a lot of tedious work to do when constructing the Request, and the worst thing is that the Request can be constructed in multiple places (ViewModel, Repository…). The more scattered the writing, the harder it is to check for errors. Retrofit, on the other hand, uses annotations to attach all the necessary information required by the Request to the method (which is also abstract, so as to remove all unnecessary information as much as possible), and as a user, only needs to invoke the corresponding method to implement the Request. As for how to parse, construct, and initiate a request, Retrofit does that internally, and the caller doesn’t want or need to know,

So Retrofit, through the facade pattern, shields the caller from useless information, exposing only one entry point, and allowing the caller to focus more on business development. Room and GreenDao also use this mode

3. Dynamic proxies are not tools

Read a lot of Retrofit related articles, all like to throw up the dynamic proxy, about why don’t mention a word, do Retrofit dynamic proxy like a tool (framework), but it is just a product of the agent model level of thought. This summary will help you dispel any misconceptions by looking at the nature of dynamic proxies through Retrofit

3.1 Retrofit building

Retrofit is built as follows:

Retrofit.Builder()
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .baseUrl(ApiConstants.BASE_URL)
    .build()
Copy the code

In the typical builder pattern, you can configure OkHttp, Gson, RxJava, etc., and finally do the build with build(). Follow the build() code:

#Retrofit.class

public Retrofit build() {

        //1.CallAdapter factory set
        List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
        callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

        //2
        List<Converter.Factory> converterFactories =
                new ArrayList<>(
                        1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
        converterFactories.add(new BuiltInConverters());
        converterFactories.addAll(this.converterFactories);
        converterFactories.addAll(platform.defaultConverterFactories());

        return new Retrofit(
              callFactory,
                baseUrl,
                unmodifiableList(converterFactories),
                unmodifiableList(callAdapterFactories),
                callbackExecutor,
                validateEagerly);
    }
Copy the code

Inject some necessary information into the Retrofit and create the return. The two sets in notes 1 and 2 are very important, so I’m going to give you a hint here and we’ll come back to them later

3.2 What is dynamic Proxy?

What is the agency model?

The concept of agency mode is very simple. For example, if A wants to do something, he can ask B to do it for him. What are the benefits of this? Here is an example. Requirement: Each CRUD of the local database must be reported

The simplest way to do this is to do a separate recording for each CRUD, as follows

// The business layer method test1
fun test1{
    // Insert the database
    dao.insert()
    / / report
    post()
}
// The business layer method test2
fun test2(a){
    // Update the database
    dao.update()
    / / report
    post()
}
Copy the code

There’s a problem with this approach:

  • reportThe operation itself has nothing to do with the specific businessreportChanges may affect the business, which may cause unforeseen problems

In the face of the above problems, the agent mode can be perfectly avoided. The modified code is as follows:

class DaoProxy() {// Insert the database
    fun insert(a){
        dao.insert()
        / / report
        post()
    }

    // Update the database
    fun update(a){
        dao.update()
        / / report
        post()
    }
}

// The business layer method test1
fun test1{
    // Insert the database
    daoProxy.insert()
}
// The business layer method test2
fun test2(a){
    // Update the database
    daoProxy.update()
}
Copy the code

DaoProxy = DaoProxy; DaoProxy = DaoProxy; DaoProxy = DaoProxy; DaoProxy = DaoProxy; DaoProxy = DaoProxy; The actual use of the proxy pattern should be based on the interface rather than the implementation of the programming idea, but the article focuses on teaching the idea, the specification may be deficient

At this point, there is another problem, each CRUD will manually do a report operation, this is obviously template code, how to solve? Let’s look at dynamic proxies:

What is dynamic proxy?

Dynamic proxies in Java do some additional operations on the target object at run time through reflection, as follows:

class DaoProxy() {
    // Create a proxy class
    fun createProxy(a): Any {
        / / create a dao
        val proxyAny = Dao()
        val interfaces = proxyAny.javaClass.interfaces
        val handler = ProxyHandler(proxyAny)
        return Proxy.newProxyInstance(proxyAny::class.java.classLoader, interfaces, handler)
    }

    // The proxy class
    class ProxyHandler(private val proxyObject:Any): InvocationHandler {
        P1 is the target class method and P2 is the target class parameter. Invoke is executed when any method of the proxyObject is called
        override fun invoke(p0: Any, p1: Method, p2: Array<out Any>): Any {
            // Execute Dao methods (CRUD)
            val result = p1.invoke(proxyObject,p2)
            / / report
            post()
            return result
        }
    }
}
// The specification should use interface-based programming instead of implementation programming. If you want to replace the Dao, programming through the interface can improve scalability
val dao:Dao = DaoProxy().createProxy() as Dao
dao.insert()
dao.update()
Copy the code

Proxy is a class used to create dynamic proxies in the JDK. InvocationHandler is a delegate class. The internal Invoke (Proxy) method will be called when any method of the target class (Dao) is called.

Dynamic proxies have the same core idea as static proxies, except that a dynamic proxy can be used to create an aspect (InvocationHandler#invoke) dynamically by reflection at run time to eliminate boilerplate code. For those of you who like to think, the proxy pattern conforms to the idea of aspect Oriented programming (AOP), and the proxy class is the aspect

3.3 Obtaining the ApiService through Dynamic Proxy

You can create an ApiService with retrofit.create(), as mentioned in section 2.2.

#Retrofit.class

public <T> T create(final Class<T> service) {
        / / first place
        validateServiceInterface(service);
        return(T) Proxy.newProxyInstance( service.getClassLoader(), new Class<? >[] {service}, new InvocationHandler() {/ / in the second place
                            @Override
                            public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                                    throws Throwable {
                                ...
                                returnplatform.isDefaultMethod(method) ? platform.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args); }}); }Copy the code

Create () can be roughly divided into two parts:

  • The first part isvalidateServiceInterface()Content, used for verificationApiServiceLegitimacy, relatively simple, not much description, interested students can check.
  • The second part isinvoke()Through the3.2You can see in the section that this is a proxy method that can be calledApiService, where the parametermethodandargsOn behalf ofApiServiceCorresponding methods and parameters. One of the return valuesisDefaultMethodIf the default method of Java8 is executed directly, after all, we only need the proxyApiServiceMethod can be. After a lot of winnowing and winnowing the task finally falls toloadServiceMethod, it is also aRetrofitIn the core of a method, let’s follow it
#Retrofit.classServiceMethod<? > loadServiceMethod(Method method) { ServiceMethod<? > result = serviceMethodCache.get(method);
    if(result ! =null) return result;
    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method); serviceMethodCache.put(method, result); }}return result;
  }

Copy the code

This is basically a common caching operation for the ServiceMethod. The purpose of this operation is to improve the efficiency of creating a ServiceMethod. After all, creating a ServiceMethod requires a lot of reflection. Creating a ServiceMethod object is implemented using its static method parseAnnotations. Follow this method:

#ServiceMethod.class

  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
        / / the first stepRequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method); Type returnType = method.getGenericReturnType(); ./ / the second step
        return HttpServiceMethod.parseAnnotations(retrofit,
                method, requestFactory);
    }
Copy the code

The first step:

Using the RequestFactory’s parseAnnotations() to parse the annotations from the Method (ApiService’s method), the code is simple and no longer pasted. Note that this step simply parses the annotations and stores them in the RequestFactory factory. The request information is then assembled through the RequestFactory when requested.

The second step:

Using HttpServiceMethod’s parseAnnotations to create a ServiceMethod is a lengthy and informative method that I’ll describe in more detail in the next section, but you only need to know what it does. So I’m going to get a little bit of a loop here, but let me just give you a little bit of a sense of why HttpServiceMethod is a subclass of ServiceMethod, so Retrofit dynamic proxy loadServiceMethod is an Object of type HttpServiceMethod, Finally, take a look at its invoke() method.

#HttpServiceMethod.class

@Override
  final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }
Copy the code

We create an OkHttpCall instance, which is essentially a series of operations on OkHttp, and I’ll talk about that later. The returned Call object does nothing, but is passed to the Adapter () method and returned. The literal meaning should be an adaptation operation. Here is another foreshadowing to echo the end of 3.1, we will reveal one by one in the next section.

So that’s the end of dynamic proxies, so what problem does it solve?

  • If don’t use the proxy pattern, that about methods in ApiService annotation parsing operations into the business, is bound to once the changes are likely to affect the business, in fact is also against the facade pattern we said earlier and Demeter, by proxy pattern to do a cut operation (AOP) to circumvent the problem can be perfect. It can be seen that the facade model and the agency model complement each other

  • Retrofit has no prior knowledge of the number of ApiService methods, and even if it did, it would inevitably result in a large amount of template code being parsed one by one. This can be solved by introducing dynamic proxies to parse dynamically at run time.

4. What’s the point of ReturnT, ResponseT?

ResponseT, ReturnT is a Retrofit shorthand for the response data type and the return value type

4.1 create HttpServiceMethod

In the previous section, we followed Adapter (), an abstract method whose implementation class is created through parseAnnotations of HttpServiceMethod, and we continue with this:

#HttpServiceMethod.class

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
            Retrofit retrofit, Method method, RequestFactory requestFactory) {
        boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
        boolean continuationWantsResponse = false;
        boolean continuationBodyNullable = false;

        Annotation[] annotations = method.getAnnotations();
        Type adapterType;
        //1. Obtain the adapterType, which defaults to method
        if (isKotlinSuspendFunction) {
            Type[] parameterTypes = method.getGenericParameterTypes();
            Type responseType =
                    Utils.getParameterLowerBound(
                            0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
            if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
                // Unwrap the actual body type from Response<T>.
                responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
                continuationWantsResponse = true;
            } else {
            }
            adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
            annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
        } else {
            adapterType = method.getGenericReturnType();
        }
        / / 2. Create CallAdapter
        CallAdapter<ResponseT, ReturnT> callAdapter =
                createCallAdapter(retrofit, method, adapterType, annotations);
        Type responseType = callAdapter.responseType();
        / / 3. Create responseConverter
        Converter<ResponseBody, ResponseT> responseConverter =
                createResponseConverter(retrofit, method, responseType);

        okhttp3.Call.Factory callFactory = retrofit.callFactory;
        //4. Create an instance of the HttpServiceMethod type
        if(! isKotlinSuspendFunction) {return new HttpServiceMethod.CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
        }
        // Compatible with kotlin Suspend methods
        else if (continuationWantsResponse) {
            //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
            return (HttpServiceMethod<ResponseT, ReturnT>)
                    new HttpServiceMethod.SuspendForResponse<>(
                            requestFactory,
                            callFactory,
                            responseConverter,
                            (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
        } else {
            //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
            return(HttpServiceMethod<ResponseT, ReturnT>) new HttpServiceMethod.SuspendForBody<>( requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter, continuationBodyNullable); }}Copy the code
  • Note 1: GetadapterTypeHere,adapterRefers to theRetrofitBuild time passaddCallAdapterFactory()The type to be added, if yesRxJavatheadapterTypeisObservable. The default ismethodReturn value, and also dokotlin suspendadapter
  • Note 2: Create a callAdapter, skimming it momentarily, described in detail below
  • Note 3: Create a responseConverter and skim it, described below
  • Note 4: Concrete will be created hereHttpServiceMethodType instance. There are three typesCallAdapted,SuspendForResponse,SuspendForBody, the first is the default type, and the latter two are compatible with Kotlin Suspend. The main thing that the interior does is actually very simple, is through the interioradapter()callcallAdapter``adapter(), the specific code is not posted, interested in their own view

4.2 How Do I Manage the callAdapter and responseConverter?

Create Create a callAdapter

#HttpServiceMethod.class

 private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter(
            Retrofit retrofit, Method method, Type returnType, Annotation[] annotations) {
        return(CallAdapter<ResponseT, ReturnT>) retrofit.callAdapter(returnType, annotations); . }Copy the code

Get the callAdapter from retrofit#callAdapter() and continue

#Retrofit.class

publicCallAdapter<? ,? > callAdapter(Type returnType, Annotation[] annotations) {return nextCallAdapter(null, returnType, annotations);
}

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++) {
            // Get the adapter factory using the callAdapterFactories using the returnType, and then get the AdapterCallAdapter<? ,? > adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
            if(adapter ! =null) {
                returnadapter; }}... }Copy the code

Get the Adapter factory using the callAdapterFactories using the returnType, and then get the CallAdapter instance using the factory get(). The callAdapterFactories are initialized in build() at the end of 3.1 and add the default type via platform, or you can add an adapter type such as RxJava via addCallAdapterFactory().

Two design pattern adapters and strategies are used here

Adapter mode

If you want Retrofit to work with RxJava, the normal way to do this is to create an Observable separately from the business and merge it with the Call. The idea of merging an Observable with a Call is not business related. At this point, the adaptor pattern can be introduced to adapt the Call to an Observable, moving the adaptation details from the business layer into the interior of the Retrofit, in compliance with Demeter’s Law

The strategy pattern

By ReturnT to obtain corresponding CallAdapter ReturnT, if is Call < T > extract is DefaultCallAdapterFactory created instance, If it is observables < T > is acquired RxJava2CallAdapterFactory created instance. If you want to add an adapter, you just need to specify the ReturnT, create the corresponding factory and add it through addCallAdapterFactory. Retrofit will automatically find the corresponding CallAdapter through ReturnT, in accordance with the open and closed principle (extension open).

Create responseConverter

Now, the responseConverter is actually a data conversion, and it ADAPTS the ResponseT to the data type that we want, For example, Gson parsing only needs to add the Converter instance created by GsonConverterFactory through addConverterFactory to add and obtain the specific process is basically the same as CallAdapter, interested students can check by themselves

4.3 Initiating a Request

Now that we’ve created everything we need, let’s go back to the HttpServiceMethod invoke, which passes OkHttpCall to adapt execution and returns, HttpServiceMethod implementation class adapter will perform corresponding CallAdapter adapter that we just get the default CallAdapter namely DefaultCallAdapterFactory CallAdapter through the get access to, The code is as follows:

DefaultCallAdapterFactory.class

public @NullableCallAdapter<? ,? >get(
        returnnew CallAdapter<Object, Call<? > > () {@Override
            public Type responseType() {
                return responseType;
            }

            @Override
            public Call<Object> adapt(Call<Object> call) {
                return executor == null? call : new DefaultCallAdapterFactory.ExecutorCallbackCall<>(executor, call); }}; }Copy the code

The inner Adapt (ApiService method) method returns ExecutorCallbackCall, which is an OkHttpCall decorator class. The execute of OkHttpCall can be used to initiate a request as follows:

#OkHttpCall.class

publicResponse<T> execute() throws IOException { okhttp3.Call call; .return parseResponse(call.execute());
    }

Copy the code

OkHttp general operations, and then focus on the parseResponse of onResponse

#OkHttpCall.classResponse<T> parseResponse(okhttp3.Response rawResponse) throws IOException { ... T body = responseConverter.convert(catchingBody); .return Response.success(body, rawResponse);
    }
Copy the code

The responseConverter makes an adaptation to the Body, and if addConverterFactory adds GsonConvert then the parsing will take place there

So far, the analysis of all Retrofit processes has been completed

From what has been discussed above

  • Retrofit construes code from a paradigm level through the REST ful API
  • Designing ApiService through the facade model allows developers to focus more on the business
  • Dynamic proxies simply take the functional code out of the business and solve the boilerplate code problem
  • ReturnT and ResponseT introduce adapter patterns to make the results more flexible