This is the fourth day of my participation in the More text Challenge. For details, see more text Challenge

Originally intended to go to the disco today, but found that if go to the disco, more challenge will be broken more! Alas!

Today nonsense not to say, to a learning for use, hand a simple version of Retrofit! Use knowledge points, reflections, annotations plus dynamic proxies and builder patterns! Especially dynamic proxy, if you understand the principle, this article will look easier, these knowledge points, the previous articles have been written, but are too theoretical, so today to a more comprehensive actual combat!

For those of you unfamiliar with reflection and dynamic proxies, read Java reflection and dynamic proxies in the JDK

For those unfamiliar with annotations, read the advanced Java feature annotations

1. Use of Retrofit

Retrofit is not a web request framework, it’s just a framework that encapsulates OKHttp. Using Retrofit just makes it easier for us to use OKHttp, so we need to learn about it first, or at least know how to use it

1.1. Declare the Service interface

public interface WeatherApi {

    @POST("/v3/weather/weatherInfo")
    @FormUrlEncoded
    Call<ResponseBody> postWeather(@Field("city") String city, @Field("key") String key);


    @GET("/v3/weather/weatherInfo")
    Call<ResponseBody> getWeather(@Query("city") String city, @Query("key") String key);
}
Copy the code

Get a proxy object for the Service interface via Retrofit

  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Retrofit retrofit = new Retrofit.Builder().baseUrl("https://restapi.amap.com")
               .build();

        weatherApi = retrofit.create(WeatherApi.class);

     
    }

Copy the code

The proxy object executes the corresponding method to obtain a Call, and then executes the Http request through the Call

   public void get(View view) {
        Call<ResponseBody> call = weatherApi.getWeather("110101"."ae6c53e2186f33bbf240a12d80672d1b");
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()){
                    ResponseBody body = response.body();
                    try {
                        String string = body.string();
                        Log.i(TAG, "onResponse get: " + string);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally{ body.close(); }}}@Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {}}); }Copy the code

Retrofit uses dynamic proxy annotation and reflection to integrate the url parameters and methods of the request into a call and return the call. Here’s a simple version

2. Implementation of Retrofit Lite

2.1 Implementation of Retrofit builder

To implement his own Retrofit, he uses the Builder pattern. The Builder pattern separates the construction of a complex object from its representation, so that the user does not have to know the details of its internal composition.

public class MyRetrofit {

    final Map<Method, ServiceMethod> serviceMethodCache = new ConcurrentHashMap<>();
    final Call.Factory callFactory;
    final HttpUrl baseUrl;

    MyRetrofit(Call.Factory callFactory, HttpUrl baseUrl) {
        this.callFactory = callFactory;
        this.baseUrl = baseUrl;
    }

   
    /** * The builder pattern, which separates the construction of a complex object from its representation, allows the consumer to avoid knowing the details of its internal composition. * /
    public static final class Builder {
        private HttpUrl baseUrl;
        //Okhttp->OkhttClient
        private okhttp3.Call.Factory callFactory;  //null


        public Builder callFactory(okhttp3.Call.Factory factory) {
            this.callFactory = factory;
            return this;
        }

        public Builder baseUrl(String baseUrl) {
            this.baseUrl = HttpUrl.get(baseUrl);
            return this;
        }

        public MyRetrofit build(a) {
            if (baseUrl == null) {
                throw new IllegalStateException("Base URL required.");
            }
            okhttp3.Call.Factory callFactory = this.callFactory;
            if (callFactory == null) {
                callFactory = new OkHttpClient();
            }

            return newMyRetrofit(callFactory, baseUrl); }}}Copy the code

Builder pattern, is don’t need to pay close attention to internal details, we can only set the parameters, we care about so we must be in the build method in judging parameter properties of can’t be empty, if he set the default value is null, so at the time of use, even if we don’t set anything, can be normal use, without null pointer

With our Retrofit, we have to put him to work. We have to use it to generate our proxies

Create a proxy object for the Service using Retrofit

public <T> T create(final Class<T> service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // Parse all the annotation information on this method
                        ServiceMethod serviceMethod = loadServiceMethod(method);
                        //args:
                        returnserviceMethod.invoke(args); }}); }Copy the code

Our proxy object is generated

So when the proxy object executes the method, it’s going to go through the InvocationHandler callback, which is going to call back the network request method that we’re going to execute, and we’re going to get all the annotation information for all the interfaces of the method and all the parameter annotation information for the method, so let’s just parse that code

2.3. ServiceMethod Parses the annotation information of the Service interface method

This step is to add a request cache. When we execute the same method many times, our request parameter key and request type are fixed, only the request parameter value is different, so we need to make a cache to store the requested method information.

    private ServiceMethod loadServiceMethod(Method method) {
        // Leave the lock unlocked to avoid the synchronized performance penalty
        ServiceMethod result = serviceMethodCache.get(method);
        if(result ! =null) return result;
        // Multithreaded to avoid repeated parsing,
        synchronized (serviceMethodCache) {
            result = serviceMethodCache.get(method);
            if (result == null) {
                result = new ServiceMethod.Builder(this, method).build(); serviceMethodCache.put(method, result); }}return result;
    }
Copy the code

After adding the cache, we write the build method of the Service Builder

     public ServiceMethod build(a) {

            /** * 1 Parse the annotation on the method, only handle POST and GET */
            for (Annotation methodAnnotation : methodAnnotations) {
                if (methodAnnotation instanceof POST) {
                    // Record the current request mode
                    this.httpMethod = "POST";
                    // Record the path of the request URL
                    this.relativeUrl = ((POST) methodAnnotation).value();
                    // Whether there is a request body
                    this.hasBody = true;
                } else if (methodAnnotation instanceof GET) {
                    this.httpMethod = "GET";
                    this.relativeUrl = ((GET) methodAnnotation).value();
                    this.hasBody = false; }}/** * 2 parse method parameters */
            int length = parameterAnnotations.length;
            parameterHandler = new ParameterHandler[length];
            for (int i = 0; i < length; i++) {
                // All the annotations on a parameter
                Annotation[] annotations = parameterAnnotations[i];
                // Process each annotation on the parameter
                for (Annotation annotation : annotations) {
                    // Todo can add a judgment: if the httpMethod is a GET request and is now parsed. field annotation, it can prompt the user to use the Query annotation
                    if (annotation instanceof Field) {
                        // Get the value in the annotation: the key of the request parameter
                        String value = ((Field) annotation).value();
                        parameterHandler[i] = new ParameterHandler.FiledParameterHandler(value);
                    } else if (annotation instanceof Query) {
                        String value = ((Query) annotation).value();
                        parameterHandler[i] = newParameterHandler.QueryParameterHandler(value); }}}return new ServiceMethod(this);
        }
Copy the code

ParameterHandler is used to store the keys of the parameters of our Service method in an order. When we execute invOK, we can add valueh to the keys of the parameters

public abstract class ParameterHandler {

    abstract void apply(ServiceMethod serviceMethod, String value);


    static class QueryParameterHandler extends ParameterHandler {
        String key;

        public QueryParameterHandler(String key) {
            this.key = key;
        }

        / / serviceMethod: callback
        @Override
        void apply(ServiceMethod serviceMethod, String value) { serviceMethod.addQueryParameter(key,value); }}static class FiledParameterHandler extends ParameterHandler {
        String key;

        public FiledParameterHandler(String key) {
            this.key = key;
        }

        @Override
        void apply(ServiceMethod serviceMethod, String value) { serviceMethod.addFiledParameter(key,value); }}}Copy the code

Now our service interface method annotation is parsed and we go to Retrofit to create a proxy with return Servicemethod. invoke(args); This step, this step is to throw our information to Okhttp and return a Call

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // Parse all the annotation information on this method
                        ServiceMethod serviceMethod = loadServiceMethod(method);
                        //args:
                        return serviceMethod.invoke(args);
                    }
                   
Copy the code

Why do we pass the parameter args to invoke when executing invoke

Write a serviceMethod invoke method that returns a call

 public Object invoke(Object[] args) {
        /** * 1 Address and parameters to process the request */
        for (int i = 0; i < parameterHandler.length; i++) {
            ParameterHandler handlers = parameterHandler[i];
            // Handler has already recorded the key, and now gives the corresponding value
            handlers.apply(this, args[i].toString());
        }

        // Get the final request address
        HttpUrl url;
        if (urlBuilder == null) {
            urlBuilder = baseUrl.newBuilder(relativeUrl);
        }
        url = urlBuilder.build();

        / / request body
        FormBody formBody = null;
        if(formBuild ! =null) {
            formBody = formBuild.build();
        }

        Request request = new Request.Builder().url(url).method(httpMethod, formBody).build();
        return callFactory.newCall(request);
    }

    // Get request, spell k-V in URL
    public void addQueryParameter(String key, String value) {
        if (urlBuilder == null) {
            urlBuilder = baseUrl.newBuilder(relativeUrl);
        }
        urlBuilder.addQueryParameter(key, value);
    }

    //Post puts the k-v into the request body
    public void addFiledParameter(String key, String value) {
        formBuild.add(key, value);
    }
Copy the code

So we’re done producing and returning call, and we’re done with the simple version of Retrofit! Then use Call to execute the enqueue method!

3, summarize

Retrofit uses dynamic proxy to create a proxy object called serviceAPI for the Service interface. ServiceAPI calls the requested method. It executes a callback to the InvocationHandler that passes the method executed by the current proxy object, and then our ServiceMethod interprets the method information by reflecting on the passed method, saving the key of the annotation. To complete the Retrofit function, return a call to the ServiceMethod invoke, and then add the value of the HTTP request to the key.