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