Basic use and principle of Retrofit

1. Basic use of Retrofit

1.1. Create interfaces

public interface Github {

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

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

        public Contributor(String login, int contributions) {
            this.login = login;
            this.contributions = contributions; }}}Copy the code

1.2. Build instance to initiate request

// Build a RetroFit instance. Added a Gson parsing class
Retrofit retrofit = new Retrofit.Builder()
     .baseUrl(API_URL)
     .addConverterFactory(GsonConverterFactory.create())
     .build();
// Use Retrofit to produce instances that can initiate requests.
Github github = retrofit.create(Github.class);
// Initiate a request
Call<List<Github.Contributor>> call = github.contributors("square"."retrofit");
List<Github.Contributor> contributors = call.execute().body();
for (Github.Contributor contributor : contributors) {
     System.out.println(contributor.login + "(" + contributor.contributions + ")");
}

Copy the code

It is important to note that the purpose of Retrofit is to produce instances that can initiate requests !!!!! Instead of Retrofit initiating the request

1.3. Use Rxjava

public interface Github {
	// Returns the RxJava Observable
    @GET("/repos/{owner}/{repo}")
    Observable<Repository> repo(
            @Path("owner") String owner,
            @Path("repo") String repo);
            
    class Repository {
        public final int id;
        public final String name;
        public final String html_url;

        public Repository(int id, String name, String html_url) {
            this.id = id;
            this.name = name;
            this.html_url = html_url;
        }

        @Override
        public String toString(a) {
            return "Repository{" +
                    "id=" + id +
                    ", name='" + name + '\' ' +
                    ", html_url='" + html_url + '\' ' +
                    '} '; }}}/ / call, note added RxJava2CallAdapterFactory addCallAdapterFactory
Retrofit retrofit = new Retrofit.Builder()
     	.baseUrl(API_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .build();

Github github = retrofit.create(Github.class);

Observable<Github.Repository> observable =  github.repo("square"."retrofit");
/ / Rxjava ShuYong
observable.subscribe(repository ->
            System.out.println(repository)
);
Copy the code

1.4. Add custom ConvertFactory

public interface Github {
	// Add a parameter of type Date
    @GET("/repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> contributors(
            @Path("owner") String owner,
            @Path("repo") String repo,
            @Query("current") Date now);
}           

/ / call
public class Demo {

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

    public static void main(String[] args) throws IOException {

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(API_URL)
                .client(new OkHttpClient.Builder()
                        .addInterceptor(newHttpLoggingInterceptor(System.out::println).setLevel(HttpLoggingInterceptor.Level.BODY)) .build()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build();  Github github = retrofit.create(Github.class); Call<List<Github.Contributor>> call = github.contributors("square"."retrofit".new Date());
        List<Github.Contributor> contributors = call.execute().body();
        for (Github.Contributor contributor : contributors) {
            System.out.println(contributor.login + "(" + contributor.contributions + ")"); }}}Copy the code

View the log stopped, the request is: the api.github.com/repos/squar…

Date format: Wed%20Dec%2009%2023%3A44%3A54%20CST%202020

// The first step is to convert a Date to a String
public class DateConverter implements Converter<Date.String> {
    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss");
    @Nullable
    @Override
    public String convert(Date value) throws IOException {
        return SIMPLE_DATE_FORMAT.format(value);
    }
	
    // The second step is to create a Factory and return the DateConverter. Note that this is the DateConverter returned in stringConverter
    //Factory returns three types of Converter for requestBody,responseBody, and other parameter conversions
    // Use stringConverter to convert other parameters
     /**
     * Returns a {@link Converter} for converting {@code type} to a {@link String}, or null if
     * {@code type} cannot be handled by this factory. This is used to create converters for types
     * specified by {@link Field @Field}, {@link FieldMap @FieldMap} values,
     * {@link Header @Header}, {@link HeaderMap @HeaderMap}, {@link Path @Path* {},@link Query @Query}, and {@link QueryMap @QueryMap} values.
     */
    public static class DateConverterFactory extends Factory {
        @Nullable
        @Override
        publicConverter<? , String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {if(type == Date.class){
                return new DateConverter();
            }
            return super.stringConverter(type, annotations, retrofit);
        }

        public static Factory create(a){
            return newDateConverterFactory(); }}}Copy the code

The way to use it is simple, when you build Retrofit, addConvertFactory, plus this Converter

    Retrofit retrofit = new Builder()
          .baseUrl(API_URL)
            .client(new OkHttpClient.Builder()
                    .addInterceptor(new HttpLoggingInterceptor(System.out::println).setLevel(Level.BODY))
                    .build())
        .addConverterFactory(GsonConverterFactory.create())
            .addConverterFactory(DateConverterFactory.create())
        .build();

Copy the code

Effect: api.github.com/repos/squar…

1.5. Customize the CallAdapterFactory

A CallAdapter, as the name suggests, is a transformation of a Call object, a conventionally defined Retrofit interface that returns a Call object. This could be Call or Call<Response>. Suppose we want to support Deffered or Deffered<Response> that returns a Kotlin coroutine. How do we do that?

class DeferredCallAdapterFactory private constructor() :CallAdapter.Factory(a){
    companion object {
        @JvmStatic
        fun create(a) = DeferredCallAdapterFactory()
    }

    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
        if(getRawType(returnType) ! = Deferred::class.java){return null
        }
        if(returnType ! is ParameterizedType){throw IllegalStateException("Deferred return type must be parameterized" + " as Deferred<Foo> or Deferred<? extends Foo>")
        }
        val innerType = getParameterUpperBound(0, returnType)
        if(getRawType(innerType) ! = Response::class.java){return BodyDeferredAdapter<Any>(innerType)
        }
        if(innerType ! is ParameterizedType){throw IllegalStateException("Response must be parameterized" + " as Response<Foo> or Response<? extends Foo>")
        }
        val responseType = getParameterUpperBound(0, innerType)
        return ResponseDeferredAdapter<Any>(responseType)
    }
}

/ / support Deffered < Response < R > >
class ResponseDeferredAdapter<R> (val responseType: Type) :CallAdapter<R.Deferred<Response<R>>>{
    override fun responseType(a) = responseType

    override fun adapt(call: Call<R>): Deferred<Response<R>> {
        val completableDeferred = CompletableDeferred<Response<R>>()
        call.enqueue(object: Callback<R>{
            override fun onResponse(call: Call<R>, response: Response<R>) {
                completableDeferred.complete(response)
            }

            override fun onFailure(call: Call<R>, t: Throwable) {
                completableDeferred.completeExceptionally(t)
            }
        })
        return completableDeferred
    }
}

/ / support Deffered < R >
class BodyDeferredAdapter<R> (val responseType: Type) :CallAdapter<R.Deferred<R>>{
    override fun responseType(a) = responseType

    override fun adapt(call: Call<R>): Deferred<R> {
        val completableDeferred = CompletableDeferred<R>()
        call.enqueue(object: Callback<R>{
            override fun onResponse(call: Call<R>, response: Response<R>) { response.body()? .let(completableDeferred::complete) ? :completableDeferred.completeExceptionally(NullPointerException()) }override fun onFailure(call: Call<R>, t: Throwable) {
                completableDeferred.completeExceptionally(t)
            }
        })
        return completableDeferred
    }
}
Copy the code

use

interface GitHubKt {
    @GET("/repos/{owner}/{repo}/contributors")
    fun contributors(
            @Path("owner") owner: String.@Path("repo") repo: String): Deferred<List<Contributor>>

    class Contributor(val login: String, val contributions: Int)}private const val API_URL = "https://api.github.com"

suspend fun main(a) {
    // Create a very simple REST adapter which points the GitHub API.
    val retrofit = Retrofit.Builder()
            .baseUrl(API_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(DeferredCallAdapterFactory.create())
            .build()

    // Create an instance of our GitHub API interface.
    val github = retrofit.create(GitHubKt::class.java)

    // Create a call instance for looking up Retrofit contributors.
    val call = github.contributors("square"."retrofit")

    // Fetch and print a list of the contributors to the library.
    GlobalScope.launch {
        val contributors = call.await()
        contributors.forEach { contributor ->
            println("" "${contributor.login} (${contributor.contributions}"" ")
        }
    }.join()
}

Copy the code

2. How is the implementation class of the interface generated

// The Github interface Retrofit creates instances
GitHub github = retrofit.create(GitHub.class);

// Check Retrofit's source code
// Create an instance of the principle, using Java dynamic proxy
public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    // Java dynamic proxy is used here
    return (T) Proxy.newProxyInstance(service.getClassLoader(), newClass<? >[] { service },new InvocationHandler() {
          private final Platform platform = Platform.get();
          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.
            // Methods inherited from Object do not require proxies, such as toString/equals
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            // the default method is available since java8. The interface allows methods with a body, which does not require a proxy
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            // The rest of the custom interface travel proxy
            returnloadServiceMethod(method).invoke(args ! =null? args : emptyArgs); }}); }Copy the code

Java dynamic proxy principle

3.1 Implementation of proxy.newproxyInstance (

public static Object newProxyInstance(ClassLoader loader, Class
       [] interfaces, InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        finalClass<? >[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();
        if(sm ! =null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /* * Look up or generate the designated proxy class. */
         // Pass the ClassLoader and the interface to generate the ClassClass<? > cl = getProxyClass0(loader, intfs);/* * Invoke its constructor with the designated invocation handler. */
        try {
            if(sm ! =null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
			// Get the Class constructor. Note that the argument type here is InvocationHandler
            finalConstructor<? > cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;
            if(! Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run(a) {
                        cons.setAccessible(true);
                        return null; }}); }// Construct an instance and pass in the InvocationHandler instance
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw newInternalError(t.toString(), t); }}catch (NoSuchMethodException e) {
            throw newInternalError(e.toString(), e); }}Copy the code

3.2 how to generate a Class from getProxyClass0

    private staticClass<? > getProxyClass0(ClassLoader loader, Class<? >... interfaces) {if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        // The proxyClassCache is just a cache, actually generated in the ProxyClassFactory class
        WeakCache<>(new KeyFactory(), new ProxyClassFactory()); // proxyClassCache is proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
        return proxyClassCache.get(loader, interfaces);
    }
Copy the code

3.3. Check the ProxyClassFactory class code

Note: WeakCache obtains instances through the Apply method, so check the Apply method of ProxyClassFactory

publicClass<? > apply(ClassLoader loader, Class<? >[] interfaces) {/** * The following display shows loading interfaces based on their names and checking that they are indeed interfaces and not classes. If they are classes, an exception is thrown */Map<Class<? >, Boolean> interfaceSet =new IdentityHashMap<>(interfaces.length);
            for(Class<? > intf : interfaces) {/* * Verify that the class loader resolves the name of this * interface to the same Class object. */Class<? > interfaceClass =null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if(interfaceClass ! = intf) {throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                /* * The class that represents the class object actually represents an * interface. */
                if(! interfaceClass.isInterface()) {throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                * Verify that this interface is not a duplicate. */
                if(interfaceSet.put(interfaceClass, Boolean.TRUE) ! =null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }

            String proxyPkg = null;     // package to define proxy class in
    		// Dynamic proxies generate classes that are Public Final by default
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            /* * Record the package of a non-public proxy interface so that the * proxy class will be defined in the same package. Verify that * all non-public proxy interfaces are in the same package. * If more than one interface is not public and does not belong to the same package name, throw an exception ~, because there must be interfaces that cannot access */
            for(Class<? > intf : interfaces) {int flags = intf.getModifiers();
                if(! Modifier.isPublic(flags)) { accessFlags = Modifier.FINAL; String name = intf.getName();int n = name.lastIndexOf('. ');
                    String pkg = ((n == -1)?"" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if(! pkg.equals(proxyPkg)) {throw new IllegalArgumentException(
                            "non-public interfaces from different packages"); }}}// If both interfaces are public, the default package name com.sun.proxy will be used
            if (proxyPkg == null) {
                // if no non-public proxy interfaces, use com.sun.proxy package
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            /* * nextUniqueNumber is a static member, Choose a name for the proxy class to generate. */
            long num = nextUniqueNumber.getAndIncrement();
    		$ProxyN increments N by 1
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            Generate the specified proxy class. */ Generate the specified proxy class
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                // Load the class according to the bytecode
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /* * A ClassFormatError here means that (barring bugs in the * proxy class generation code) there was some other * invalid aspect of the arguments supplied to the proxy * class creation (such as virtual machine limitations * exceeded). * /
                throw newIllegalArgumentException(e.toString()); }}}Copy the code

Summary steps:

1. Load the interface based on the name of the interface passed in the parameter

2, verify that interface is an interface and not a class

Verify and generate the package name of the class to be generated based on whether the interface is public. Rules:

If only one of the generated classes is not public, the package name of the generated class is the package name of the class.

If more than one interface is not public and does not belong to the same package name, raise an exception ~, because some interface must not be accessible

3.3. If the interfaces are all public, use the default package name com.sun.proxy

$proxyN, where N is incremented by 1 for each generated class

. 5, using ProxyGenerator generateProxyClass, according to the name of the class implementing an interface, generate bytecode modifier

6, load bytecode, return class, this step to see the source code is a native method, temporarily not delve into.

3.4, analysis ProxyGenerator. GenerateProxyClass generated class content

3.4.1 The source code of ProxyGenerator here is very long and will not be analyzed here. Explore by generating a class and analyzing its contents

    //IHello interface code
public interface IHello {
    void hello(a);
}
    
// Generate class test code as follows:
    public static void main(String[] args) {
        byte[] helloClassBytes = ProxyGenerator.generateProxyClass("MyProxy".new Class[]{IHello.class}, Modifier.PUBLIC | Modifier.FINAL);
        FilesKt.writeBytes(new File("MyProxy.class"), helloClassBytes);
    }
Copy the code

3.4.2. Analyze myproxy.class

public final class MyProxy extends Proxy implements IHello {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
	
    // The InvacationHandler constructor takes InvacationHandler as an argument
    public MyProxy(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw newUndeclaredThrowableException(var4); }}public final void hello(a) throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw newUndeclaredThrowableException(var3); }}public final String toString(a) throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw newUndeclaredThrowableException(var3); }}public final int hashCode(a) throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw newUndeclaredThrowableException(var3); }}// Assign values to method m1,m2,m3, m0, etc
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.breeze.test.IHello").getMethod("hello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw newNoClassDefFoundError(var3.getMessage()); }}}Copy the code

And you can see that all of them actually go h.I nvoke, so what is H? The Proxy class

    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
Copy the code

H is the InvacationHandler passed in by proxy. newProxyInstance. All methods are executed through the Invoke method of the InvacationHandler.

// Proxy is the generated instance, method is the proxy method, and args is the passthrough parameter
Invoke (proxy,args); invoke(proxy,args); Calling the proxy method creates an endless loop
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
Copy the code

3.5. Dynamic agent expansion

As you can see from the above, Java’s built-in dynamic proxy can only proxy interfaces and will throw an exception if you want to generate a proxy for a class. This can be done using CgLib. The API is actually similar to Java’s dynamic proxy. The principle is the same: generate bytecode and load it.

4. How does Retrofit assemble requests

When Retrofit is used, request parameters are passed through annotations. How does Retrofit parse annotations

Take the following code as an example

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

From the above code, there are two types of annotations, method annotations and parameter annotations

4.1 Source code analysis

GitHub github = retrofit.create(GitHub.class);
// Recalling the section on how the interface's implementation class is generated, what does the create method end up doing
returnloadServiceMethod(method).invoke(args ! =null ? args : emptyArgs);
// The loadServiceMethod method is listed belowServiceMethod<? > loadServiceMethod(Method method) {// Note that serviceMethodCache is of type ConcurrentHashMap, which is thread-safeServiceMethod<? > result = serviceMethodCache.get(method);if(result ! =null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
      	// Parse the annotations here
        result = ServiceMethod.parseAnnotations(this, method); serviceMethodCache.put(method, result); }}return result;
  }
Copy the code

View the ServiceMethod source code

abstract class ServiceMethod<T> {
  // Parse the parameters, assemble the request parameters
  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);

    Type returnType = method.getGenericReturnType();
    if (Utils.hasUnresolvableType(returnType)) {
      throw methodError(method,
          "Method return type must not include a type variable or wildcard: %s", returnType);
    }
    if (returnType == void.class) {
      throw methodError(method, "Service methods cannot return void.");
    }
	// Parse the parameters and assemble the request
    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
  }

  abstract @Nullable T invoke(Object[] args);
}

Copy the code

ServiceMethod deals with two things:

1. Assembly request parameters

2. Assembly request

4.2. Assembly request parameters

// Create a RequestFactory
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
    return new Builder(retrofit, method).build();
  }

// Focus on the build method
RequestFactory build(a) {
    // Parse method annotations, corresponding to parse request types GET/POST/...
	for (Annotation annotation : methodAnnotations) {
    	parseMethodAnnotation(annotation);
	}
    // Omit some code here
   
   // Parses method parameters and annotations, encapsulated in ParameterHandler
   int parameterCount = parameterAnnotationsArray.length;
   parameterHandlers = newParameterHandler<? >[parameterCount];for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) { parameterHandlers[p] = parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter); }}Copy the code

4.3 Assembly request instance

Taking the default Call as an example,

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), newClass<? >[] { service },new InvocationHandler() {
          private final Platform platform = Platform.get();
          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);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            returnloadServiceMethod(method).invoke(args ! =null? args : emptyArgs); }}); }Copy the code

The create method finally executes the Invoke method and looks at the source code, which is the HttpServiceMethod class

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

If it is the default adapter, which is still the Call object, and then calls enQueue to initiate the request, the business is done.

At this point, the analysis verifies that Retrofit does not actually initiate requests, only produces instances that can initiate requests!!