Hi, I’m Halliday, and I’m going to take a look at some of the technical frameworks that go from tree trunk to twig. This article will introduce you to the open source project Retrofit.

The passage is about 2800 words and takes about 8 minutes to read.

Retrofit source code is based on the latest version 2.9.0

prepare

Retrofit makes web calls as simple as a RESTful design style, such as:

interface WanApi {
    // Use annotations to mark the network request mode get, POST, parameter path, query, etc
    @GET("article/list/{page}/json")
    Call<WanArticleBean> articleList(@Path("page") int page);
}
Copy the code

For another example, the Spring Boot framework on the back end eliminates a lot of configuration by using the convention over configuration idea. It also applies this style to the RestController network interface.

@RestController
public class ActivityController {
    @Autowired
    private ActivityService activityService;

    // Use annotations to mark the network request mode and incoming parameters
    @GetMapping("/goods")
    public ResultEntity queryGoods(@RequestParam("page") int page) {
        returnactivityService.queryGoods(page); }}Copy the code

Retrofit’s underlying network implementation is based on OKHTTP, and it doesn’t have many classes of its own. The core point is the dynamic proxy. The proxy pattern simply provides a proxy for an object that enhances or controls access to it. Let’s first look at static and dynamic proxies

Static agent

The agent is completed at compile time

  • Source level: manually write proxy class, APT generation proxy class
  • Bytecode level: bytecode generation at compile time,

Chestnuts, for example,

interfaceTo make money{
    void makeMoney(int income);
}

classSmall meatimplementsTo make money{ / / the delegate class
    @Override
    public void makeMoney(int income) {
        System.out.println("Let's do it. Make a profit."+ income); }}classagentimplementsTo make money{ / / the proxy classMake money XXR;publicAgent (to make money XXR) {this.xxr = xxr;
    }

    @Override
    public void makeMoney(int income) {
        if (income < 1000 _0000) { // Control access
            System.out.println("Only" + income + "Go back and wait for the announcement.");
        } else{ xxr.makeMoney(income); }}}public static void main(String[] args) {make money XXR =newSmall meat (); Make money JJR =newAgent (XXR); jjr.makeMoney(100 _0000); // Output: only 1000000, go back and wait for notification
    jjr.makeMoney(1000 _0000); // Output: start shooting, earn 10000000
}
Copy the code

Why do the proxy and delegate classes implement the same interface? To ensure that the internal structure of the proxy class is as consistent as possible with that of the delegate class, so that all operations on the proxy class can be transferred to the delegate class, and the proxy class only focuses on enhancement and control.

A dynamic proxy

Bytecode is generated at runtime, such as proxy. newProxyInstance and CGLIB

Proxy.newProxyInstance is provided by Java and can only be used as a Proxy for an interface.

CGLIB is more powerful and can also proxy for ordinary classes. The underlying layer is based on ASM (ASM uses a saX-like parser for progressive scanning to improve performance).

Chestnuts, for example,

classWorking standardimplements InvocationHandler {XXR to make money;publicCooperation criteria (making money XXR) {this.xxr = xxr;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int income = (int) args[0];
        if (income < 1000 _0000) { // Control access
            System.out.println("Only" + income + "Go back and wait for the announcement.");
            return null;
        } else {
            returnmethod.invoke(xxr, args); }}}public static void main(String[] args) {make money XXR =newSmall meat (); Cooperation standard =newCooperation Standards (XXR);$Proxy0 extends Proxy implements money. $Proxy0 extends Proxy implements money
    // Then reflect to create an example bd, that is, a temporary business expansionMake money bd = (make money) proxy.newProxyInstance (make money.class.getClassLoader (),newClass[]{make money. Class}, standard);// Call makeMoney and internally forward to the invoke cooperative standard
    bd.makeMoney(100 _0000);
    bd.makeMoney(1000 _0000);
}
Copy the code

Instead of creating a specific agent class (such as a broker or brokerage firm) in advance to implement the money-making interface, dynamic proxies first draw up a cooperation standard (InvocationHandler), then create the agent class $Proxy0 (bytecode) at runtime, and then reflect to create the example business extension. It’s more flexible.

Once you’ve learned about dynamic proxies, you’re ready to start your Retrofit journey

The trunk

Simple to use

Introducing dependencies,

implementation 'com. Squareup. Okhttp3: okhttp: 3.14.9'
implementation 'com. Squareup. Retrofit2: retrofit: 2.9.0'
implementation 'com. Squareup. Retrofit2: converter - gson: 2.9.0'
implementation 'com. Google. Code. Gson: gson: 2.8.6'
Copy the code

Define the interface WanApi,

interface WanApi {
    // Use annotations to mark the type of the network request get with the parameter PATH
    @GET("article/list/{page}/json")
    Call<WanArticleBean> articleList(@Path("page") int page);
}
Copy the code

Make a request,

class RetrofitActivity extends AppCompatActivity {
    final String SERVER = "https://www.xxx.com/";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(SERVER) // Specify the server address
            .addConverterFactory(GsonConverterFactory.create()) // Deserialize the data into entities with gson
            .build();
        // The runtime generates a class (bytecode) that implements the WanApi interface, and reflects the creation examples
        WanApi wanApi = retrofit.create(WanApi.class);
        // Get the Retrofit call, which encapsulates the OKHTTP call
        Call<WanArticleBean> call = wanApi.articleList(0);
        // Ask to join the team
        call.enqueue(new Callback<WanArticleBean>() {
            @Override
            public void onResponse(Call<WanArticleBean> call, Response<WanArticleBean> response) {
                // Get the data entity
                WanArticleBean bean = response.body();
                // Unlike okHTTP, Retrofit already uses a Handler to help us switch back to the main thread
                mBinding.tvResult.setText("" + bean.getData().getDatas().size());
            }

            @Override
            public void onFailure(Call<WanArticleBean> call, Throwable t) {}}); }}Copy the code

Realize the principle of

Since Retrofit is based on OKHTTP, hardy has done an analysis of the network process in the okHTTP series, so this article ignores the network implementation and only focuses on some processing of Retrofit itself. The construction of Retrofit objects is a simple Builder mode, we directly look at create.

//Retrofit.java
public <T> T create(final Class<T> service) {
    / / verification
    validateServiceInterface(service);
    return (T)
        // Dynamic proxy
        Proxy.newProxyInstance(
        service.getClassLoader(), // Class loader
        newClass<? >[] {service},// A group of interfaces
        new InvocationHandler() {
            // Determine the Android and JVM platforms and their versions
            private final Platform platform = Platform.get();

            @Override
            public Object invoke(Object proxy, Method method, Object[] args){
                // If the method is an Object method, execute it directly
                if (method.getDeclaringClass() == Object.class) {
                    return method.invoke(this, args);
                }
                //isDefaultMethod: check whether it is the default method supported by Java8 from the beginning
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args); // Let's focus here}}); }Copy the code

Proxy.newProxyInstance dynamic Proxy, run time will generate a class (bytecode) such as $ProxyN, implement the incoming interface WanApi, override the interface method and forward to the InvocationHandler invoke, as follows (pseudo code),

class $ProxyN extends Proxy implements WanApi{
    Call<WanArticleBean> articleList(@Path("page") int page){
        // Forward to invocationHandler
        invocationHandler.invoke(this,method,args); }}Copy the code

Let’s look at the validateServiceInterface validation logic,

//Retrofit.java
private void validateServiceInterface(Class
        service) {
    // Check: WanApi is not an interface, throw an exception...
    // Check: WanApi cannot have generic parameters and cannot implement other interfaces...
    if (validateEagerly) { // This function is disabled by default
        Platform platform = Platform.get();
        for (Method method : service.getDeclaredMethods()) { // Iterate over the WanApi method
            // Not a default method, and not a static method
            if(! platform.isDefaultMethod(method) && ! Modifier.isStatic(method.getModifiers())) {// Load the method ahead of time (check if it works)loadServiceMethod(method); }}}}Copy the code

If validate15, it will check all methods of WanApi at once and load them in. It can be opened in debug mode to detect errors in advance. For example, if @body is set in @get request, such errors will raise an exception:

java.lang.IllegalArgumentException: Non-body HTTP method cannot contain @Body.

loadServiceMethod

LoadServiceMethod (method).invoke(args)

//Retrofit.java
// Cache, using thread-safe ConcurrentHashMap
finalMap<Method, ServiceMethod<? >> serviceMethodCache =newConcurrentHashMap<>(); ServiceMethod<? > loadServiceMethod(Method method) { ServiceMethod<? > result = serviceMethodCache.get(method);//WanApi's articleList method is cached and returns directly
    if(result ! =null) return result;
    synchronized (serviceMethodCache) {
        result = serviceMethodCache.get(method);
        if (result == null) {
            // Parse the articleList annotation, create a ServiceMethod and cache it
            result = ServiceMethod.parseAnnotations(this, method); serviceMethodCache.put(method, result); }}return result;
}
Copy the code

Follow up ServiceMethod parseAnnotations,

//ServiceMethod.java
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    / / 1.
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
    // Check: the articleList method return type cannot use wildcards and void...
    / / 2.
    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}
Copy the code

Look at 1. RequestFactory. ParseAnnotations,

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

class Builder {
    RequestFactory build(a) {
        // Parse method annotations such as GET
        for (Annotation annotation : methodAnnotations) {
            parseMethodAnnotation(annotation);
        }
		// Omit all tests...
        // Parse parameter annotations such as Path
        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);
        }
        // Omit all tests...
        return new RequestFactory(this); }}Copy the code

After get RequestFactory, 2. HttpServiceMethod. ParseAnnotations, HttpServiceMethod responsible for adaptation and conversion process, adjust the call of the interface methods for HTTP calls,

//HttpServiceMethod.java
//ResponseT response type such as WanArticleBean, ReturnT return type such as Call
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {
    // Omit kotlin coroutine logic...
    Annotation[] annotations = method.getAnnotations();
    // Iterate to find the appropriate adapter
    CallAdapter<ResponseT, ReturnT> callAdapter =
        createCallAdapter(retrofit, method, adapterType, annotations);
    // Get the response type, such as WanArticleBean
    Type responseType = callAdapter.responseType();
    // Iterate to find the appropriate converter
    Converter<ResponseBody, ResponseT> responseConverter =
        createResponseConverter(retrofit, method, responseType);
    okhttp3.Call.Factory callFactory = retrofit.callFactory;
    return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
}
Copy the code

So we finally return a CallAdapted, and we see the CallAdapted,

//CallAdapted extends HttpServiceMethod extends ServiceMethod
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) {
        super(requestFactory, callFactory, responseConverter);
        this.callAdapter = callAdapter;
    }

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

So who is this CallAdapter instance? Let’s go back to retrofit.Builder,

//Retrofit.Builder.java
public Retrofit build(a) {
    Executor callbackExecutor = this.callbackExecutor;
    // If the thread pool is not set, set the default MainThreadExecutor for the Android platform (with Handler to switch callback back to the main thread).
    if (callbackExecutor == null) {
        callbackExecutor = platform.defaultCallbackExecutor();
    }
    List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
    Add the default DefaultCallAdapterFactory / /
    callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));
}
Copy the code

To create specific instances of CallAdapter DefaultCallAdapterFactory this factory,

//DefaultCallAdapterFactory.java
publicCallAdapter<? ,? > get(Type returnType, Annotation[] annotations, Retrofit retrofit) {final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);
    // If the SkipCallbackExecutor annotation is specified, there is no need to switch back to the main thread
    final Executor executor =
        Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
        ? null
        : callbackExecutor;
    return newCallAdapter<Object, Call<? > > () {@Override
        public Type responseType(a) {
            return responseType;
        }

        @Override
        public Call<Object> adapt(Call<Object> call) {
            // By default, returns a Call wrapped in the main pool, and its enqueue will use the main pool's execute
            return executor == null ? call : newExecutorCallbackCall<>(executor, call); }}; }Copy the code

invoke

Invoke (HttpServiceMethod) (HttpServiceMethod);

//HttpServiceMethod.java
final ReturnT invoke(Object[] args) {
    // Finally see okhttp!
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
}

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

    @Override
    protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
        // Package OkHttpCall as an ExecutorCallbackCall with the previous adapter
        returncallAdapter.adapt(call); }}Copy the code

Then the request team, ExecutorCallbackCall. The enqueue – > OkHttpCall. The enqueue,

//ExecutorCallbackCall.java
void enqueue(final Callback<T> callback) {
    delegate.enqueue(
        new Callback<T>() {
            @Override
            public void onResponse(Call<T> call, final Response<T> response) {
                // Switch the callback back to the main thread
                callbackExecutor.execute(
                    () -> {
                        callback.onResponse(ExecutorCallbackCall.this, response);
                    });
                / /...
            }

            @Override
            public void onFailure(Call<T> call, final Throwable t) {}}); }//OkHttpCall.java
void enqueue(final Callback<T> callback) {
    / / okhttp logic
    okhttp3.Call call;
    call.enqueue(new okhttp3.Callback() {
        void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
            callback.onResponse(OkHttpCall.this, response); }})}Copy the code

You finally get through the process and you go back and you look at the flow chart again, and you see

twigs

CallAdapter

The CallAdapter adapter is used to accommodate the return type. For example, it also supports the use of Rxjava, coroutines,

interface WanApi {
    //Call
    @GET("article/list/{page}/json")
    Call<WanArticleBean> articleList(@Path("page") int page);

    / / Rxjava, need addCallAdapterFactory (RxJavaCallAdapterFactory. The create ())
    @GET("article/list/{page}/json")
    Observable<WanArticleBean> articleListRx(@Path("page") int page);
}
Copy the code

Converter

The Converter Converter is used to convert parameter types, such as formatting a Long timestamp into a string and passing it to the back end.

interface WanApi {
    //Long cur Current time
    @GET("article/list/{page}/json")
    Call<WanArticleBean> articleList(@Path("page") int page, @Query("cur") Long cur);
}

class TimeConverter implements Converter<Long.String> {
    private SimpleDateFormat mFormat = new SimpleDateFormat("yyyy-MM-dd-HHmmss");

    @Override
    public String convert(Long value) throws IOException {
        if (value > 1_000_000_000_000L) {// milliseconds is not very precise
            return mFormat.format(new Date(value));
        }
        returnString.valueOf(value); }}class TimeConverterFactory extends Converter.Factory {

    @Override
    publicConverter<? , String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {if (type == Long.class) {
            // Use a custom TimeConverter
            return new TimeConverter();
        }
        return super.stringConverter(type, annotations, retrofit);
    }

    public static Converter.Factory create(a) {
        return newTimeConverterFactory(); }}/ / then set up line, addConverterFactory (TimeConverterFactory. The create ())
Copy the code

Dynamic url replacement

We pass in an HttpUrl object when we build Retrofit, and the instance is always there, so we can reflect and modify its fields such as host to dynamically replace the server address.

String SERVER = "https://www.xxx.com/";
HttpUrl httpUrl = HttpUrl.get(SERVER);
Retrofit retrofit = new Retrofit.Builder()
    //.baseUrl(SERVER)
    .baseUrl(httpUrl) / / use HttpUrl
    .build();
Copy the code

The end of the

See you next time ~😆

Series of articles:

  • Okhttp for the Series
  • Glide from the Series “Never Forget”

The resources

  • GitHub & Documentation & API
  • Imooc – Hack Retrofit
  • Brief – An architectural perspective on Retrofit’s role, principles, and implications
  • Brief – JAVA dynamic proxy
  • Csdn-cglib (Code Generation Library) details
  • Zhihu – What is the function of Java dynamic proxy?

Welcome to pay attention to the original technology public account: Halliday EI