Annotations in Retrofit and how to customize interface method annotations


Interface method and method parameter annotation

One of the biggest features of the Retrofit framework is the use of annotations to describe HTTP requests :

  • Support for URL parameter substitution and query parameters
  • Convert an object into a request body (e.g. JSON, Protocol Buffers)
  • Multipart request body and file upload

Annotations are divided into method annotations and method parameter annotations, which mark how a request will be processed.

1. Method annotation

1.1 Annotation describes the request method

Annotation name express meaning
@GET GET request
@POST POST request
@DELETE DELETE request
@HEAD HEAD request
@OPTIONS OPTIONS request
@PATCH PATCH request
example

1. Notes@GET

    @GET("/users/query/all")
    Call<List<User>> getUsers();

2. Comments @POST:

    @POST("/users/query")
    @FormUrlEncoded
    Call<User> getUser(@Field("id") String id);

1.2 Annotations describe the request content type, return result and request header

Annotation name express meaning
@FormUrlEncoded This annotation is used together with the method annotation @POST and the parameter annotation @Field to indicate that the content of the request entity (body part) will be URL-encoded and uploaded, and the Content-Type under the request (POST) is "application/x-www- form-urlencoded ". In other words, the requested parameters are sent to the server in the form of key1=value1&key2=value2…, and the parameters will be URL-encoded.
@Multipart This annotation is used together with the method annotation @POST and the parameter annotation @Part to indicate that the content of the request entity (body part) is uploaded using a form, and the Content-Type under the request (POST) is "multipart/form-data". The data uploaded using the form can be plain text, plain file, or text+file.
@Streaming Convert the return result (ResponseBody) to a stream (bytes).
@Headers Flag request header.
example

1. Notes@FormUrlEncoded

Send POSTrequest, request header Content-Type:application/x-www-form-urlencoded, request entity id=value.

    @POST("/users/query")
    @FormUrlEncoded
    Call<User> getUser(@Field("id") String id);

2. Notes@Multipart

Send POSTrequest, request header Content-Type: multipart/form-data, request entity id=valueand image file.

    @POST("/users/update")
    @Multipart
    Call<ResponseBody> updateUserPortrait(@Part("id") int id, @Part(value = "image", encoding = "8-bit") RequestBody image);

3. Notes@Streaming

    @GET("/users/download")
    @Streaming
    Call<ResponseBody> downloadUserFile(@Query("id") int id);

4. Notes@Headers

    @Headers("Cache-Control: max-age=640000")
    @GET("/users/download")
    @Streaming
    Call<ResponseBody> downloadUserFile(@Query("id") int id);

2. Parameter annotation

Annotation name express meaning
@Body This annotation is used together with the method annotation @POST or @PUT, and the flag parameter is a POJO or RequestBody object, which will eventually directly convert the parameter object into the content of the request entity (body part).
@Field This annotation is used together with the method annotations @POST and @FormUrlEncoded to indicate that the parameter is a key-value pair, that is, the request entity (body part) is passed in the form of key1=value1&key2=value2… or key=value1&key=value2….
@FieldMap Same as the @Field annotation, except that the parameter object is a Map.
@Header The flag parameter is a request header
@HeaderMap The flag parameter is a request header of a Map collection
@Part This annotation is used together with method annotation @POST and parameter annotation @Multipart
@PartMap Same as the @Part annotation, except that the parameter object is a Map, and the type accepted by the Map is <String, RequestBody>
@Path The flag parameter is used to replace the request path
@Query The flag parameter (@Query("key1") String value1) will be appended to the request path (?key1=value1).
@QueryMap Same as the @Query annotation, except that the parameter object is a Map collection, which converts the Map collection into a key-value pair.
@QueryName Similar to the @Query annotation, except that the parameter (@QueryName String filter) will be directly appended to the path (?filter)
@Tag The flag parameter is used to tag this request
@Url The flag parameter is used to replace the request path, baseUrl is ignored

example

1. Notes @Body:

    @POST("/users/upload")
    Call<ResponseBody> uploadUserLog(@Body RequestBody body);

    //创建RequestBody
    RequestBody body = RequestBody.create(MediaType.parse("Content-Type: application/json;charset=UTF-8"), "Your json log");
    UserService#uploadUserLog(body);

2. Annotations @Filedand@FiledMap

Notes @Filed:

   @POST("/users/query")
    @FormUrlEncoded
    Call<User> getUser(@Field("id") String id);

Notes @FieldMap:

    @POST("/users/query")
    @FormUrlEncoded
    Call<User> getUser(@FieldMap Map<String, String> filedMap);


    Map<String,String> filedMap = new HashMap<>();
    filedMap.put("id", "123");
    filedMap.put("age", "25");
    UserService#getUser(filedMap);

2. Annotations @Headerand@HeaderMap

Notes @Header:

    @POST("/users/query")
    @FormUrlEncoded
    Call<User> getUser(@Field("id") String id, @Header("Accept-Language") String lang);

Notes @HeaderMap:

    @POST("/users/query")
    @FormUrlEncoded
    Call<User> getUser(@Field("id") String id, @HeaderMap Map<String, String> headerMap);


    Map<String,String> headerMap = new HashMap<>();
    headerMap.put("Accept-Charset", "utf-8");
    headerMap.put("Accept", "text/plain");
    UserService#getUser("123", headerMap);

2. Annotations @Partand@PartMap

Notes @Part:

    @POST("/users/update")
    @Multipart
    Call<ResponseBody> updateUserPortrait(@Part("id") int id, @Part(value = "image", encoding = "8-bit") RequestBody image);

Notes @PartMap:

    @POST("/users/upload/feedback")
    @Multipart
    Call<ResponseBody> uploadUserFeedback( @Part("file") RequestBody file, @PartMap Map<String, RequestBody> params);


    Map<String, RequestBody> partMap = new HashMap<>();
    partMap.put("deviceInfo", RequestBody.create(MediaType.parse("text/plain"), "deviceInfo"));
    partMap.put("content", RequestBody.create(MediaType.parse("text/plain"), "content"));
    File file = new File("test.png");
    RequestBody requestBody = RequestBody.create(MultipartBody.FORM, file);
    UserService#uploadUserFeedback(requestBody, partMap);

3. Notes @Path:

    @GET("/users/{name}")
    Call<List<User>> getUserByName(@Path("name") String name);

4. Annotations @Queryand@QueryMap

Notes @Query:

    @GET("/users/query/friends")
    Call<List<User>> getUserFriends(@Query("page") int page);

Notes @QueryMap:

    @GET("/users/query/friends/filter")
    Call<List<User>> getUserFriends(@QueryMap Map<String, String> params);

     Map<String, String> queryMap = new HashMap<>();
     queryMap.put("age", "23");
     queryMap.put("sex", "male");
     UserService#.getUserFriends(queryMap);

5. Comments @QueryName:

    @GET("/users/query/friends/filter")
    Call<List<User>> getUserFriendsByFileter(@QueryName String filter);

6. Notes @Url:

    @GET("/users/config")
    Call<ResponseBody> getUserConfig(@Url String url);

Annotation parsing process

The annotation analysis is carried out in the method of ServiceMethodthe static inner class in the class, the method annotation is resolved by calling the method, and the parameter annotation is resolved by calling the method.BuilderbuildparseMethodAnnotation(annotation)parseParameter

Regarding the specific parsing steps, we first use Retrofitto create a service interface:

        Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com/").build();
        UserService service = retrofit.create(UserService.class);

The creation of the service interface uses a Java dynamic proxy , specifically look at the methods Retrofitin the class create:

  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public 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);
            }
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

According to the previous Java dynamic proxy Proxy.newProxyInstance() , InvocationHandlerthe callback method in the interface invokeis called when the method of the target interface (UserService) is called, and invokethe return value of the method is the return value of the target interface (UserService) method. As in the example above @GET:

    @GET("/users/query/all")
    Call<List<User>> getUsers();

    Call<List<User>> call =  service.getUsers();
    call.enqueue(callback);

When the UserService calls getUsers()the method, it will execute invokethe method and return Call<List<User>>.

In invokethe method, first determine whether the executed method is Objecta method declared in the class:

            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }

Like hashCode, equalsetc. are Objectthe methods inside. The second is to judge whether the executed method is the default method declared in the target interface (UserService) (Java 8.0 or later, the default method can be declared in the interface):

            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }

PlatformThe main distinction is Android or Java. Finally ServiceMethod, create OkHttpCallan object, and return the corresponding interface method return value through the method CallAdapterin the object :adapt

            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);

In loadServiceMethodthe method, first check whether the methodcorresponding object ServiceMethodalready exists, if it exists, return it directly, if not, create one and put it in the cache:

  ServiceMethod<?, ?> loadServiceMethod(Method method) {
    ServiceMethod<?, ?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = new ServiceMethod.Builder<>(this, method).build();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

The constructor of ServiceMethodthe static inner class in the class accepts and objects, and finally calls the method to create the object:BuilderRetrofitMethodbuild()ServiceMethod

    Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      this.method = method;
      this.methodAnnotations = method.getAnnotations();
      this.parameterTypes = method.getGenericParameterTypes();
      this.parameterAnnotationsArray = method.getParameterAnnotations();
    }

    public ServiceMethod build() {
      callAdapter = createCallAdapter();
      responseType = callAdapter.responseType();
      
      ...省略部分代码
      
      responseConverter = createResponseConverter();

      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation); //解析方法注解
      }

      ...省略部分代码

      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];
        if (Utils.hasUnresolvableType(parameterType)) {
          throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
              parameterType);
        }

        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
        if (parameterAnnotations == null) {
          throw parameterError(p, "No Retrofit annotation found.");
        }

        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); //解析方法参数注解
      }

      ...省略部分代码

      return new ServiceMethod<>(this);
    }

parseMethodAnnotation(annotation)The main analysis method annotations in the method:

    private void parseMethodAnnotation(Annotation annotation) {
      if (annotation instanceof DELETE) {
        parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
      } else if (annotation instanceof GET) {
        parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
      } else if (annotation instanceof HEAD) {
        parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
        if (!Void.class.equals(responseType)) {
          throw methodError("HEAD method must use Void as response type.");
        }
      } else if (annotation instanceof PATCH) {
        parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
      } else if (annotation instanceof POST) {
        parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
      } else if (annotation instanceof PUT) {
        parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
      } else if (annotation instanceof OPTIONS) {
        parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
      } else if (annotation instanceof HTTP) {
        HTTP http = (HTTP) annotation;
        parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
      } else if (annotation instanceof retrofit2.http.Headers) {
        String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
        if (headersToParse.length == 0) {
          throw methodError("@Headers annotation is empty.");
        }
        headers = parseHeaders(headersToParse);
      } else if (annotation instanceof Multipart) {
        if (isFormEncoded) {
          throw methodError("Only one encoding annotation is allowed.");
        }
        isMultipart = true;
      } else if (annotation instanceof FormUrlEncoded) {
        if (isMultipart) {
          throw methodError("Only one encoding annotation is allowed.");
        }
        isFormEncoded = true;
      }
    }

parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations)The method mainly analyzes method parameter annotations:

    private ParameterHandler<?> parseParameter(
        int p, Type parameterType, Annotation[] annotations) {
      ParameterHandler<?> result = null;
      for (Annotation annotation : annotations) {
        ParameterHandler<?> annotationAction = parseParameterAnnotation(
            p, parameterType, annotations, annotation);

        if (annotationAction == null) {
          continue;
        }

        if (result != null) {
          throw parameterError(p, "Multiple Retrofit annotations found, only one allowed.");
        }

        result = annotationAction;
      }

      if (result == null) {
        throw parameterError(p, "No Retrofit annotation found.");
      }

      return result;
    }

    private ParameterHandler<?> parseParameterAnnotation(
        int p, Type type, Annotation[] annotations, Annotation annotation) {
      if (annotation instanceof Url) {
        ...省略部分代码

      } else if (annotation instanceof Path) {
        ...省略部分代码

      } else if (annotation instanceof QueryName) {
        ...省略部分代码
        
      } else if (annotation instanceof QueryMap) {
        ...省略部分代码
        
      } else if (annotation instanceof Header) {
        ...省略部分代码
       
      } else if (annotation instanceof HeaderMap) {
        ...省略部分代码
       
      } else if (annotation instanceof Field) {
        ...省略部分代码
         
      } else if (annotation instanceof FieldMap) {
        ...省略部分代码
        
      } else if (annotation instanceof Part) {
        ...省略部分代码
      
      } else if (annotation instanceof PartMap) {
        ...省略部分代码
        
      } else if (annotation instanceof Body) {
        ...省略部分代码
        
      }
    }

The specific analysis details of the annotation will not be analyzed and introduced here. If you are interested, you can read this part of the code carefully.


How to customize interface method annotations

A key question for custom interface method annotations is: how to parse and when to parse this method annotation?

From the above annotation analysis process, we know that the annotation analysis is performed in the method of ServiceMethodthe static inner class in the class. Look again at the first line of code in the method:Builderbuildbuild

    callAdapter = createCallAdapter(); //创建一个 CallAdapter 对象

createCallAdapter()method creates an CallAdapter<T, R>object . CallAdapter<R, T>It is an interface whose function is to Rconvert the response type of the request result into a custom type T. CallAdapterObjects are CallAdapter.Factorycreated through classes. It is added by the method when Retrofitthe object is created :Retrofit.Builder
#addCallAdapterFactory(Factory)

        Retrofit retrofit = Retrofit.Builder()
                .baseUrl("https://api.example.com/")
                .client(okHttpClient)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(createGsonConverter()))
                .build();
        UserService service = retrofit.create(UserService.class);

Look at the method again createCallAdapter:

  private CallAdapter<T, R> createCallAdapter() {
      Type returnType = method.getGenericReturnType();//得到方法返回值类型
      if (Utils.hasUnresolvableType(returnType)) {
        throw methodError(
            "Method return type must not include a type variable or wildcard: %s", returnType);
      }
      if (returnType == void.class) {
        throw methodError("Service methods cannot return void.");
      }
      Annotation[] annotations = method.getAnnotations();//获得方法注解
      try {
        //noinspection unchecked
        return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations);
      } catch (RuntimeException e) { // Wide exception range because factories are user code.
        throw methodError(e, "Unable to create call adapter for %s", returnType);
      }
    }

At the end of the method, the passed Retrofitmethod callAdapterreturns an CallAdapter<T, R>object. This callAdaptermethod accepts two parameters, which are the method return value type Type returnType = method.getGenericReturnType();and method annotation Annotation[] annotations = method.getAnnotations();. If you want to customize the annotation, you must find a way to get annotationsthis parameter.

Look at the method again callAdapter:

  final List<CallAdapter.Factory> adapterFactories;
   
  public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
    return nextCallAdapter(null, returnType, annotations);
  }

  public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
      Annotation[] annotations) {
      
    ...省略部分代码

    int start = adapterFactories.indexOf(skipPast) + 1;//默认会添加一个`CallAdapter<?, ?>`对象
    for (int i = start, count = adapterFactories.size(); i < count; i++) {
      CallAdapter<?, ?> adapter = adapterFactories.get(i).get(returnType, annotations, this);
      if (adapter != null) {
        return adapter;
      }
    }

    ...省略部分代码
  }

In nextCallAdapterthe method, the object is obtained through the method adapterFactoriesin the traversal :CallAdapter.FactorygetCallAdapter<?, ?>

 public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
        Retrofit retrofit);

getTypeThere are three parameters and objects in the method . The object is the method annotation of the interface method. If there is a custom method annotation, then the custom method annotation is insideAnnotation[] . So custom annotation is to implement the interface, and then add it through the method .RetrofitAnnotation[]CallAdapter.FactoryRetrofit.Builder
#addCallAdapterFactory(Factory)

Let's look at the default Retrofit Retrofit.Builder#build Retrofitprovided by the framework :CallAdapter.Factory,它是在在创建对象的时候,通过方法添加的

      Executor callbackExecutor = this.callbackExecutor;
      if (callbackExecutor == null) {
        callbackExecutor = platform.defaultCallbackExecutor();
      }

      // Make a defensive copy of the adapters and add the default Call adapter.
      List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
      adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

Look at the platformfollowing defaultCallAdapterFactorymethod:

  CallAdapter.Factory defaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
    if (callbackExecutor != null) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }
    return DefaultCallAdapterFactory.INSTANCE;
  }

If set Retrofit.Builder#build.callbackExecutor(Executor executor), use it ExecutorCallAdapterFactory, otherwise use it DefaultCallAdapterFactory.

Let's look at the class DefaultCallAdapterFactory:

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory();

  @Override
  public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != Call.class) {
      return null;
    }

    final Type responseType = Utils.getCallResponseType(returnType);
    return new CallAdapter<Object, Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public Call<Object> adapt(Call<Object> call) {
        return call;
      }
    };
  }
}

According to the above code, you know how to create one and parse the custom interface method annotation in CallAdapter.Factoryits method.get

example

Customize an annotation that identifies whether the request interface needs to be encrypted:

/**
 * 加密注解,在接口方法中添加了这个注解,就代表着这个请求接口传输的数据会进行加密
 */
public @interface Encryption {
    
}

Customize one CallAdapter.Factory:

public class EncryptionCallAdapterFactory extends CallAdapter.Factory {
    private static final CallAdapter.Factory INSTANCE = new EncryptionCallAdapterFactory();

    public static CallAdapter.Factory create() {
        return INSTANCE;
    }

    @Override
    public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
        if (getRawType(returnType) != Call.class) {
            return null;
        }
        final Type responseType = Utils.getCallResponseType(returnType);

        String baseUrl = retrofit.baseUrl().url().toString();
        String path = null;//请求路径
        boolean hasEncryption = false;//是否有加密注解Encryption
        for (Annotation annotation : annotations) {
            if (annotation instanceof GET) {
                path = ((GET) annotation).value();
            } else if (annotation instanceof POST) {
                path = ((POST) annotation).value();
            } else if (annotation instanceof Encryption) {
                hasEncryption = true;
            }
        }
        if (path != null && hasEncryption) {
            //do something
        }

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

            @Override
            public Call<Object> adapt(Call<Object> call) {
                return call;
            }
        };
    }
}

In getthe method, get the custom interface method annotation, and then parse it. The request interface is encrypted above. In getthe method, only the request entity of this request interface needs to be encrypted, and then the request entity needs to be encrypted in OKHttpClientthe interceptor .Interceptor

add EncryptionCallAdapterFactory:

        Retrofit retrofit = Retrofit.Builder()
                .baseUrl("https://api.example.com/")
                .client(okHttpClient)
                .addCallAdapterFactory(EncryptionCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(createGsonConverter()))
                .build();
        UserService service = retrofit.create(UserService.class);

To sum up, the custom interface method annotation is mainly used , and the method annotation is obtained CallAdapter.Factoryin its method for analysis. getNote that interface method parameter annotations cannot be customized here.

Guess you like

Origin blog.csdn.net/wangjiang_qianmo/article/details/94344924