Article Directory
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 POST
request, 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 POST
request, request header Content-Type: multipart/form-data
, request entity id=value
and 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 @Filed
and@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 @Header
and@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 @Part
and@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 @Query
and@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 ServiceMethod
the 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.Builder
build
parseMethodAnnotation(annotation)
parseParameter
Regarding the specific parsing steps, we first use Retrofit
to 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 Retrofit
in 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() , InvocationHandler
the callback method in the interface invoke
is called when the method of the target interface (UserService) is called, and invoke
the 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 invoke
the method and return Call<List<User>>
.
In invoke
the method, first determine whether the executed method is Object
a method declared in the class:
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
Like hashCode
, equals
etc. are Object
the 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);
}
Platform
The main distinction is Android or Java. Finally ServiceMethod
, create OkHttpCall
an object, and return the corresponding interface method return value through the method CallAdapter
in 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 loadServiceMethod
the method, first check whether the method
corresponding object ServiceMethod
already 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 ServiceMethod
the static inner class in the class accepts and objects, and finally calls the method to create the object:Builder
Retrofit
Method
build()
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 ServiceMethod
the static inner class in the class. Look again at the first line of code in the method:Builder
build
build
callAdapter = createCallAdapter(); //创建一个 CallAdapter 对象
createCallAdapter()
method creates an CallAdapter<T, R>
object . CallAdapter<R, T>
It is an interface whose function is to R
convert the response type of the request result into a custom type T
. CallAdapter
Objects are CallAdapter.Factory
created through classes. It is added by the method when Retrofit
the 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 Retrofit
method callAdapter
returns an CallAdapter<T, R>
object. This callAdapter
method 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 annotations
this 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 nextCallAdapter
the method, the object is obtained through the method adapterFactories
in the traversal :CallAdapter.Factory
get
CallAdapter<?, ?>
public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
Retrofit retrofit);
get
Type
There 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 .Retrofit
Annotation[]
CallAdapter.Factory
Retrofit.Builder
#addCallAdapterFactory(Factory)
Let's look at the default Retrofit Retrofit.Builder#build Retrofit
provided 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 platform
following defaultCallAdapterFactory
method:
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.Factory
its 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 get
the method, get the custom interface method annotation, and then parse it. The request interface is encrypted above. In get
the method, only the request entity of this request interface needs to be encrypted, and then the request entity needs to be encrypted in OKHttpClient
the 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.Factory
in its method for analysis. get
Note that interface method parameter annotations cannot be customized here.