Implementation of Retrofit Custom Request Parameter Annotation

  • 2021-10-25 07:44:29
  • OfStack

Preface

At present, only GET and POST are used in our project. For GET request, the parameters of the request will be spliced in Url; For an POST request, we can submit some parameter information via Body or a form.

How to use in Retrofit

Let's first look at how these two requests are declared in Retrofit:

GET Request


@GET("transporter/info")
Flowable<Transporter> getTransporterInfo(@Query("uid") long id);

We use @ Query annotations to declare query parameters, and every parameter needs to be marked with @ Query annotations

POST Request


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);

In the Post request, we mark the object that needs to be passed to the server with the @ Body annotation

Can the declaration of Post request parameters be more intuitive

The above two conventional request methods are very common, so there is nothing special to explain.

Once the team discussed a problem, all our requests were declared in different interfaces, such as the official example:


public interface GitHubService {
 @GET("users/{user}/repos")
 Call<List<Repo>> listRepos(@Path("user") String user);
}

If it is an GET request, we can intuitively see the parameters of the request through @ Query annotation, but if it is an POST request, we can only see the specific parameters where the upper layer calls, so can the parameter declaration of POST request be as intuitive as GET request 1?

@ Field notes

Look at the code first, about the use of @ Field annotations:


@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

With the @ Field annotation, we will submit the data as a form (first_name = XXX & last_name = yyy).

Problems based on conventions

It seems that @ Field annotation can meet our needs, but unfortunately we agreed with API that the format of data transmission requested by POST is JSON format before, so obviously we can't use this annotation

Processing Flow of Retrofit Parameter Annotation

At this time, I wonder if I can imitate the @ Field annotation, implement an annotation by myself, and finally pass the parameters to API in the format of JSON. Before that, let's take a look at how the requested parameters are handled in Retrofit:

Constructor of Builder in ServiceMethod


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

We focus on three attributes:

Annotation on methodAnnotations method, Annotation [] type parameterTypes parameter type, Type [] type parameterAnnotationsArray parameter annotation, Annotation [] [] type

In the constructor, we mainly assign values to these five attributes.

build Method of Builder Constructor

Next, let's look at what happens during the creation of an ServiceMethod object through the build method:


// Part of the code is omitted ...

public ServiceMethod build() {
 //1.  Comments on parsing methods 
 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];

 Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
 //2.  Through a loop for each 1 Parameter creation 1 Parameter processor 
 parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
 }
 return new ServiceMethod<>(this);
}

Annotation on parsing method parseMethodAnnotation


if (annotation instanceof GET) {
 parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
}else if (annotation instanceof POST) {
 parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} 

I omitted most of the code. The whole code is actually to judge the type of method annotation, and then continue to parse the method path. We only pay attention to the 1 branch of POST:


private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
 this.httpMethod = httpMethod;
 this.hasBody = hasBody;
 // Get the relative URL path and existing query string, if present.
 // ...
}

It can be seen that this method call chain actually determines the value of httpMethod (request mode: POST), hasBody (whether it contains Body body) and other information

Create a parameter handler

Create 1 ParameterHandler for every 1 parameter in the loop body:


private ParameterHandler<?> parseParameter(
 int p, Type parameterType, Annotation[] annotations) {
 ParameterHandler<?> result = null;
 for (Annotation annotation : annotations) {
 ParameterHandler<?> annotationAction = parseParameterAnnotation(
 p, parameterType, annotations, annotation);
 }
 //  Omit part of the code ...
 return result;
}

You can see that the parseParameterAnnotation method is then called inside the method to return 1 parameter handler:

Handling of @ Field annotations


else if (annotation instanceof Field) {
 Field field = (Field) annotation;
 String name = field.value();
 boolean encoded = field.encoded();

 gotField = true;
 Converter<?, String> converter = retrofit.stringConverter(type, annotations);
 return new ParameterHandler.Field<>(name, converter, encoded);

}
Gets the value of the annotation, that is, the parameter name Select the appropriate Converter according to the parameter type Returns 1 Field object, which is the handler for the @ Field annotation

ParameterHandler.Field


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
0

The parameter name and parameter value of @ Filed tag are added to FromBody by apply method

Handling of @ Body annotations


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
1 Select the appropriate Converter gotBody is labeled true Returns 1 Body object, which is the handler for the @ Body annotation

ParameterHandler.Body


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
2

Convert the @ Body declared object to RequestBody through Converter, and then set the assignment to the body object

When is the apply method called

Let's look at OkHttpCall's synchronous request execute method:


// Omit part of the code ...
@Override
public Response<T> execute() throws IOException {
 okhttp3.Call call;

 synchronized (this) {
 call = rawCall;
 if (call == null) {
 try {
 call = rawCall = createRawCall();
 } catch (IOException | RuntimeException | Error e) { throwIfFatal(e); // Do not assign a fatal error to creationFailure.
 creationFailure = e;
  throw e;
 }
 }
 return parseResponse(call.execute());
}

Inside the method, we use the createRawCall method to create an call object, and inside the createRawCall method, we call the serviceMethod.toRequest(args); Method to create an Request object:


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
4

It can be seen that the apply method of the parameter processor corresponding to each parameter is executed in the for loop, and the corresponding attributes in RequestBuilder are assigned values. Finally, an Request object is constructed by the build method. There is another important step in the build method: confirm the source of our final Body object, whether it comes from the object declared by @ Body annotation or other


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
5

Custom POST request parameter annotation @ BodyQuery

According to the above process, if you want to customize one parameter annotation, the following changes are involved:

Added class @ BodyQuery parameter annotation Added class BodyQuery to handle parameters declared by @ BodyQuery The parseParameterAnnotation method in ServiceMethod adds a processing branch to @ BodyQuery RequestBuilder class, boolean value hasBodyQuery is added to indicate whether @ BodyQuery annotation is used, and an Map object hasBodyQuery is used to store the parameters of @ BodyQuery tag

@ BodyQuery notes


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
6

There is no special, copy @ Query annotated code

BodyQuery Annotation Processor


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
7

We did two things in the apply method

Imitate the processing of Field and get the parameter value of @ BodyQuery tag Add key-value pairs to 1 Map

//  In  RequestBuilder  Added methods in 
void addBodyQueryParams(String name, String value) {
 bodyQueryMaps.put(name, value);
}

New branch processing for @ BodyQuery


@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);
9

I will briefly judge the parameterized type, and we can see that the processing here is basically the same as the branch processing for @ Field, except that the returned ParameterHandler object type is different

RequestBuilder

We said before that in RequestBuilder#build() The most important point in the method is to determine whether the value of body comes from @ Body, a form or another object. Here, we need to add one new source, that is, the parameter value declared by our @ BodyQuery annotation:


RequestBody body = this.body;
if (body == null) {
 // Try to pull from one of the builders.
 if (formBuilder != null) {
 body = formBuilder.build();
 } else if (multipartBuilder != null) {
 body = multipartBuilder.build();
 } else if (hasBodyQuery) {
 body = RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), JSON.toJSONBytes(this.bodyQueryMaps));

 } else if (hasBody) {
 // Body is absent, make an empty body.
 body = RequestBody.create(null, new byte[0]);
 }
}

In the hasBodyQuery branch, we convert bodyQueryMaps to an JSON string and construct an RequestBody object to assign to body.

Finally

Take an example to see the use of @ BodyQuery annotation under 1:


@Test
public void simpleBodyQuery(){
 class Example{
 @POST("/foo")
 Call<ResponseBody> method(@BodyQuery("A") String foo,@BodyQuery("B") String ping){
  return null;
 }
 }
 Request request = buildRequest(Example.class,"hello","world");
 assertBody(request.body(), "{\"A\":\"hello\",\"B\":\"world\"}");
}

Since Retrofit does not provide permission to modify and extend these classes, this is only an extension of ideas, and I only extended a new set of annotation types along with the processing of ParameterHandler in Retrofit.

Summarize


Related articles: