Retrofit注解详细探究
在Retrofit使用入门一文中,了解了Retrofit的基本使用。现在继续深挖Retrofit的核心:注解,Retrofit通过注解的方式将HTTP网络请求转换成Java接口。
当前使用的Retrofit版本是2.9.0,这个版本共有25个注解。按照注解作用的地方可以分为两类:一类是方法注解(METHOD),作用在方法上。一类是参数注解(PARAMETER),用在方法的参数中。
1、方法注解
Retrofit用在方法上的注解共有12个:@DELETE、@GET、@HEAD、@OPTIONS、@PATCH、@POST、@PUT、@HTTP;@FormUrlEncoded、@Multipart、@Streaming、@Headers。
1.1、网络请求方法注解
在RESTful 统一接口一文,了解了HTPP网络请求的几种统一接口。Retrofit中的网络请求方法注解分别对应这几种HTTP请求方法。
1.1.1、@GET
@GET注解应用在方法上,表示该方法对应的HTTP网络请求是GET。
interface GetCall {
@GET("/info")
fun getCall(): Call<ResponseBody>
}返回类型Call<T>的泛型可以为原始响应类型ResponseBody或者空Void。
interface GetCall {
@GET("/info")
fun getCall(): Call<Void>
}T也可以是定义的Json实体类等其它类型,GsonConverter会将网络响应转换成为设置的返回结果类型T。
interface GetCall {
@GET("/info")
fun getCall(): Call<Bean>
}@GET注解不可与@FormUrlEncoded和@Multipart标记注解同时作用在方法上。因为GET不携带请求体,后面的@HEAD、@OPTIONS等注解也是一样的。
从源码可以发现,在RequestFactory类中的parseMethodAnnotation(Annotation annotation)方法解析注解,只允许PATCH、POST、PUT三个网络请求方法可以携带RequestBody请求体,至于@HTTP注解则看具体情况。
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);
} 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());
}
...
}在build()构造Request请求的时候会结合前面解析出的请求方法和RequestBody进行判断,很多时候开发中遇到的异常是从这个方法中抛出的。
RequestFactory build() {
...
if (httpMethod == null) {
throw methodError(method, "HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}
if (!hasBody) {
if (isMultipart) {
throw methodError(method,
"Multipart can only be specified on HTTP methods with request body (e.g., @POST).");}
if (isFormEncoded) {
throw methodError(method,
"FormUrlEncoded can only be specified on HTTP methods with request body (e.g., @POST).");}
}
...
if (relativeUrl == null && !gotUrl) {
throw methodError(method, "Missing either @%s URL or @Url parameter.", httpMethod);
}
if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
throw methodError(method, "Non-body HTTP method cannot contain @Body.");
}
if (isFormEncoded && !gotField) {
throw methodError(method, "Form-encoded method must contain at least one @Field.");
}
if (isMultipart && !gotPart) {
throw methodError(method, "Multipart method must contain at least one @Part.");
}
return new RequestFactory(this);
}扯远了,回来继续讲解注解。
1.1.2、@POST
@POST注解用来向服务端发送数据,可以携带RequestBody。
interface PostMessage {
@FormUrlEncoded
@POST("/report")
fun postMessage(@Query("name") name: String): Call<Boolean>
}1.1.3、@DELETE
@DELETE注解的方法不能携带RequestBody。返回值具体业务而定,可以是简单的Boolean(前提是转换器能自行转换),或者返回其它类型的数据通知删除结果。
interface DeleteService {
@DELETE("/storage/{fileName}")
fun deleteComments(@Path("fileName") fileName: String): Call<Boolean>
}涉及到的参数注解第2节会讲到。基本的HTTP请求方法,再结合Retrofit中其它参数注解完成丰富的API设计。
1.1.4、PUT
@PUT请求方法使用请求中的内容创建或者替换目标资源。
interface PutService {
@PUT("/table/info")
fun putInfo(): Call<Result>
}PUT与POST方法的区别在于,PUT方法是幂等的:调用一次与连续调用多次是等价的(即没有副作用)。而连续调用多次POST方法可能会有副作用,比如将一个订单重复提交多次。
1.1.5、PATCH
@PATCH用于对资源进行部分修改。
interface PatchService {
@FormUrlEncoded
@PATCH("/data/_table")
fun updateInfo(@FieldMap set: Map<String, String>): Call<String>
}1.1.6、HEAD
@HEAD注解用来请求资源的头部信息, 并且这些信息头与GET方法请求时返回的一致.。
interface HeadCall {
@HEAD("/")
fun getHead(): Call<Void>
}HEAD请求不会返回任何Body,因此Call<T>中的泛型必须为Void空。Kotlin中的Unit空类型暂时并不被Retrofit库支持。
interface HeadCall {
@HEAD("/")
fun getHead(): Call<Unit>
}从Retrofit源码中的注释,可以看到Retrofit开发团队也在计划准备支持一下Kotlin的空类型Unit。
// TODO support Unit for Kotlin?
if (requestFactory.httpMethod.equals("HEAD") && !Void.class.equals(responseType)) {
throw methodError(method, "HEAD method must use Void as response type.");
}使用HEAD请求方法的一个场景是在下载一个大文件前先获取header头部少量信息,知道文件大小再决定是否要下载, 以此可以节约带宽资源。这个做法有点像解析Bitmap,先用inSampleSize获取图片的大小,再压缩采样加载Bitmap。
{
cache-control=[no-store, no-cache, must-revalidate],
connection=[keep-alive],
content-encoding=[gzip],
content-type=[text/html; charset=utf-8],
date=[Tue, 30 Mar 2021 12:32:59 GMT],
expires=[Thu, 19 Nov 1981 08:52:00 GMT],
pragma=[no-cache], server=[nginx],
set-cookie=[bbs_sid=ll7d8fb8okit2c6ctqnfif85pb; expires=Thu,
08-Jul-2021 12:32:59 GMT; Max-Age=8640000; HttpOnly,
cookie_test=tMWidLYc5m6_2BXD53ZuiXdvh5C7s5_2BLL9_2BthuaOhZE8_2B5afYo;
expires=Wed, 31-Mar-2021 12:32:59 GMT; Max-Age=86400],
vary=[Accept-Encoding],
x-powered-by=[PHP/7.3.9]
}1.1.7、OPTIONS
@OPTIONS注解的方法用于获取目的服务端所支持的通信选项。不能包含RequestBody,且返回类型为Void/Unit空。
interface OPTIONS_Options {
@OPTIONS("/")
fun getOptions(): Call<Void>
}举个例子:
val retrofit = retrofit.newBuilder()
.baseUrl("http://example.org/")
.build()
val options: OPTIONS_Options = retrofit.create(OPTIONS_Options::class.java)
val call = options.getOptions()
call.enqueue(object : Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
Log.d(TAG,"options onResponse ${response.headers().toMultimap().toString()}")
}
override fun onFailure(call: Call<Void>, t: Throwable) {
}
})响应报文包含一个 allow 首部字段,该字段的值表明了服务器支持的所有 HTTP 方法:[OPTIONS, GET, HEAD, POST]。测试链接:http://example.org/。
{
allow=[OPTIONS, GET, HEAD, POST],
cache-control=[max-age=604800],
content-length=[0],
content-type=[text/html; charset=UTF-8],
date=[Wed, 31 Mar 2021 03:29:39 GMT],
expires=[Wed, 07 Apr 2021 03:29:39 GMT],
server=[EOS (vny/0452)]
}1.1.8、HTTP
@HTTP注解用来替换@GET、@POST、@DELETE、@PUT、@HEAD等注解,用来自定义用户请求,使用@HTTP注解可以拓展网络请求功能。
interface HttpService {
@HTTP(method = "GET",path = "/search",hasBody = false)
fun commonHttpMethod(@Query("keywords") keyword: String): Call<MusicBean>
}@HTTP注解需要设置method、path、hasBody。必须要设置请求方法method,默认无RequestBody。在Retrofit内部构造请求Request的时候会用到开发者自定义HTTP中设置的这三个属性。
private void parseMethodAnnotation(Annotation annotation) {
...
else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
}
...
}1.2、其它方法注解
以上8个是基本的HTTP网络请求方法注解,另外还有4个方法注解,用来标记RequestBody类型、Response响应类型以及添加请求头Headers。
1.2.1、@FormUrlEncoded
@FormUrlEncoded注解只能用在有RequestBody的网络请求方法上,比如使用@POST、@PUT、@PATCH注解的请求方法。
RequestFactory build() {
...
if (!hasBody) {
...
if (isFormEncoded) {
throw methodError(
method,
"FormUrlEncoded can only be specified on HTTP methods with "
+ "request body (e.g., @POST).");
}
}
...
return new RequestFactory(this);
}@FormUrlEncoded注解的方法必须至少包含一个@Field注解参数,关于@Field注解详见2.2节。
RequestFactory build() {
...
if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
throw methodError(method, "Non-body HTTP method cannot contain @Body.");
}
if (isFormEncoded && !gotField) {
throw methodError(method, "Form-encoded method must contain at least one @Field.");
}
if (isMultipart && !gotPart) {
throw methodError(method, "Multipart method must contain at least one @Part.");
}
return new RequestFactory(this);
}举个例子,提交评论的接口设计如下:
interface PostService {
@FormUrlEncoded
@POST("/user/comment")
fun postComments(@Field("comments") comments: String): Call<String>
}1.2.2、Multipart
和@FormUrlEncoded一样,@Multipart注解只能用在有请求体的网络请求方法参数中。否则会抛出异常:Multipart can only be specified on HTTP methods with request body。
RequestFactory build() {
...
if (!hasBody) {
if (isMultipart) {
throw methodError(
method,
"Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
}
...
}
...
return new RequestFactory(this);
}同样的,@Multipart注解的方法也必须至少包含一个@Part注解的参数。否则抛出异常:Multipart method must contain at least one @Part。关于@Part注解详见2.6节。
if (isMultipart && !gotPart) {
throw methodError(method, "Multipart method must contain at least one @Part.");
}举个例子:上传文件的接口设计:
interface PostFileService {
@Multipart
@POST("/")
fun uploadFile(@Part filePart: MultipartBody.Part): Call<String>
}文件上传部分逻辑代码实例:
//从待上传文件创建RequestBody
val requestBody = File(imagePath).asRequestBody("image/png".toMediaType())
val head: Headers = Headers.Builder().add("content-lenght", "128kb").build()
//构造携带文件请求体的MultipartBody.Part
val part: MultipartBody.Part = MultipartBody.Part.create(head, requestBody)
//通过接口上传文件
val call = uploadService.uploadFile(part)1.2.3、Streaming
@Streaming注解的网络请求,响应数据以OKHttp中原始ResponseBody的形式返回。对于没有@Streaming注解的方法,则先将响应数据全部获取到内存中处理(比如使用转换器将原始响应转换成合适的类型)。@Streaming注解适合大文件下载场景。
interface BigFileDownloadService {
@Streaming
@GET("/{path}")
fun downloadBigFile(@Path("path") file: String): Call<ResponseBody>
}使用@Steaming注解实现大文件下载示例,文件为Qt安装包,约3.2Gb:
/**
* https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/qt/5.12/5.12.0/qt-opensource-mac-x64-5.12.0.dmg
* https://mirrors.tuna.tsinghua.edu.cn/
* qt/official_releases/qt/5.12/5.12.0/qt-opensource-mac-x64-5.12.0.dmg
*/
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://mirrors.tuna.tsinghua.edu.cn/")
.build()
val downloadService: BigFileDownloadService =
retrofit.create(BigFileDownloadService::class.java)
val filePath = "qt/official_releases/qt/5.12/5.12.0/qt-opensource-mac-x64-5.12.0.dmg"
val call = downloadService.downloadBigFile(filePath)
call.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
//从Response流中处理大文件下载
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
}
})1.2.4、Headers
通常还会在请求头中添加一些特殊的消息,Retrofit提供两种添加Headers信息的方式。第一种就是通过给方法添加@Headers注解,向Headers中添加静态无法修改的消息报头。另一种方法通过方法注解动态增加请求头信息,详见2.4节和2.5节。
interface GetCall {
@Headers("Cache-Control: max-age=640000", "cache-control: no-cache")
@GET("/popular")
fun getPopular(): Call<Bean>
}2、参数注解
作用在参数上的注解有:@Body、@Field、@FieldMap、@Header、@HeaderMap、@Part、@PartMap、@Path、@Query、@QueryMap、@QueryName、@Tag、@Url。
在接口中定义的请求方法如果是有参方法,参数必须用上面的参数注解。否则RequestFactory解析interface中方法的参数时,调用parseParameter(...)方法会校验报错:No Retrofit annotation found。
2.1、@Body
@Body注解用于非表单请求体。有一些注意坑点,开发者在使用过程中可能会踩到。
interface GetCall {
@POST("/search")
fun postCall(@Body key: String): Call<Void>
}只能作用在有RequestBody的网络请求方法上,不能用于GET、HEAD等没有请求体的网络请求方法中。否则会抛出Non-body HTTP method cannot contain @Body.异常。
RequestFactory build() {
...
if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
throw methodError(method, "Non-body HTTP method cannot contain @Body.");
}
...
return new RequestFactory(this);
}@Body注解不能作用在已经被@FormUrlEncoded和@Multipart注解过的方法的参数中。否则会抛出@Body parameters cannot be used with form or multi-part encoding.异常。
private ParameterHandler<?> parseParameterAnnotation(...) {
...
if (isFormEncoded || isMultipart) {
throw parameterError(
method, p, "@Body parameters cannot be used with form or multi-part encoding.");
}
...
}2.2、@Field
@Field注解用于向表单提交字段。
interface Post_interface {
@FormUrlEncoded
@POST("/name")
fun getInfo(@Field("name") name: String): Call<String>
}只能用在@FormUrlEncoded注解的方法的参数中。否则会抛出异常:@Field parameters can only be used with form encoding。
@Nullable
private ParameterHandler<?> parseParameterAnnotation(
int p, Type type, Annotation[] annotations, Annotation annotation) {
...
else if (annotation instanceof Field) {
validateResolvableType(p, type);
if (!isFormEncoded) {
throw parameterError(method, p, "@Field parameters can only be used with form encoding.");
}
...
return new ParameterHandler.Tag<>(tagType);
}
return null; // Not a Retrofit annotation.
}2.3、@FieldMap
@FieldMap同上面的@Field注解一样,用于@FormUrlEncoded注解的表单请求提交字段。
interface PostMap {
@FormUrlEncoded
@POST("/info")
fun getInfo(@FieldMap info: Map<String, String>): Call<String>
}@FieldMap注解的参数必须是Map类型,否则会抛出异常:@FieldMap parameter type must be Map.
private ParameterHandler<?> parseParameterAnnotation(
int p, Type type, Annotation[] annotations, Annotation annotation) {
...
else if (annotation instanceof FieldMap) {
validateResolvableType(p, type);
if (!isFormEncoded) {
throw parameterError(
method, p, "@FieldMap parameters can only be used with form encoding.");
}
Class<?> rawParameterType = Utils.getRawType(type);
if (!Map.class.isAssignableFrom(rawParameterType)) {
throw parameterError(method, p, "@FieldMap parameter type must be Map.");
}
...
}2.4、@Header
@Header参数注解用于给请求添加一个请求头。传入的参数值如果为null,则忽略。可以为List集合,每一个非空项都被添加到请求头中。
interface GetInfoService {
@GET("/info")
fun getInfo(@Header("Accept-Language") language: String): Call<String>
}请求头中的相同name的value不会互相覆盖,是可以重复的。这一点“得益”于OkHttp中Headers头部信息的存储方式实现,详见OKHttp之Headers请求头。
2.5、@HeaderMap
@HeaderMap和@Header注解一样用来添加请求头,能够批量添加信息头。
interface GetInfoService {
@GET("/info")
fun getInfo(@HeaderMap map: Map<String, String>): Call<String>
}@HeaderMap注解的参数必须是Map,且键类型必须是String。这些在源码中都会校验,如果不符合就会抛出异常:
private ParameterHandler<?> parseParameterAnnotation(...) {
...
else if (annotation instanceof HeaderMap) {
...
Class<?> rawParameterType = Utils.getRawType(type);
if (!Map.class.isAssignableFrom(rawParameterType)) {
throw parameterError(method, p, "@HeaderMap parameter type must be Map.");
}
...
if (String.class != keyType) {
throw parameterError(method, p, "@HeaderMap keys must be of type String: " + keyType);
}
...
}2.6、@Part
@Part参数注解表示multi-part多块请求的一部分。参数类型可以是:MultipartBody.Part、RequestBody或者其它类型。
①如果类型为MultipartBody.Part,则内容将直接使用。
②如果类型为RequestBody,则该值将直接与其内容类型一起使用。
③其类型不是以上两种而是其它对象类型,将通过使用Converter转换为适当的表示形式。
interface PostPartService {
@Multipart
@POST("/upload")
fun getInfo(@Part("file") request: RequestBody): Call<String>
}@Part注解只能和@Multipart注解一起使用,否则会抛出异常:@Part parameters can only be used with multipart encoding。还记得:在1.2.2节提到Multipart注解只能在有RequestBody的HTTP方法上使用,比如使用POST、PUT、PATCH注解的请求方法。
@Part注解的参数类型如果是MultipartBody.Part,则不能给注解传入参数值。但@Part注解的参数类型如果不是MultipartBody.Part,而是RequestBody或者其它类型,则必须给@Part注解传入name值。否则又会抛出异常:@Part annotation must supply a name or use MultipartBody.Part parameter type。
private ParameterHandler<?> parseParameterAnnotation(...) {
...
else if (annotation instanceof Part) {
validateResolvableType(p, type);
if (!isMultipart) {
throw parameterError(
method, p, "@Part parameters can only be used with multipart encoding.");
}
Part part = (Part) annotation;
...
if (!MultipartBody.Part.class.isAssignableFrom(Utils.getRawType(iterableType))) {
throw parameterError(
method,
p,
"@Part annotation must supply a name or use MultipartBody.Part parameter type.");
}
...
}2.7、@PartMap
@PartMap注解和上一节中的@Part注解作用相似,支持多块上传。所以适合文件分块、多文件上传的业务场景。
interface PostMultiFile {
@Multipart
@POST("/upload")
fun uploadFile(@PartMap key: Map<String, String>): Call<Void>
}@PartMap注解的参数类型必须是Map,否则会抛出异常:@PartMap parameter type must be Map。在源码RequestFactory类中的parseParameterAnnotation()方法中解析校验各个参数注解。
...
if (!Map.class.isAssignableFrom(rawParameterType)) {
throw parameterError(method, p, "@PartMap parameter type must be Map.");
}
...@PartMap注解的Map映射参数中的键类型必须是String类型,否则会抛出异常:@PartMap keys must be of type String。
if (String.class != keyType) {
throw parameterError(method, p, "@PartMap keys must be of type String: " + keyType);
}2.8、@Path
@Path参数注解用于URL地址的缺省值。
interface PostMultiFile {
@POST("/upload/{path}")
fun uploadFile(@Path("path") path: String): Call<Void>
}缺省{path}的命名必须符合正则:[a-zA-Z][a-zA-Z0-9_-]*。否则在validatePathName(int p, String name)方法中校验path不通过会抛出异常:@Path parameter name must match \{([a-zA-Z][a-zA-Z0-9_-]*)\}。
// Upper and lower characters, digits, underscores, and hyphens, starting with a character.
private static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
private static final Pattern PARAM_NAME_REGEX = Pattern.compile(PARAM);
...
private void validatePathName(int p, String name) {
if (!PARAM_NAME_REGEX.matcher(name).matches()) {
throw parameterError(
method,
p,
"@Path parameter name must match %s. Found: %s",
PARAM_URL_REGEX.pattern(),
name);
}
// Verify URL replacement name is actually present in the URL path.
if (!relativeUrlParamNames.contains(name)) {
throw parameterError(method, p, "URL \"%s\" does not contain \"{%s}\".", relativeUrl, name);
}
}@Path注解必须要传入和URL中{path}一样的名称,否则会找不到替换的缺省路径会报异常。比如反例:
interface PostMultiFile {
@POST("/upload/{path1}")
fun uploadFile(@Path("path2") path: String): Call<Void>
}上面这样的API设计就不符合规范,报错: URL "/upload/{path1}" does not contain "{path2}"。
2.9、@Query
@Query注解的参数会作为查询参数附加到URL。@Query用法简单,问题不大。但是因为参数值直接附加到URL上,所以安全性较低,敏感数据最好用POST方式并且加密。
interface QueryService {
@POST("/info")
fun queryName(@Query("name") name: String): Call<Void>
}上面这个例子访问的服务端路径是/info?name=name。@Query一般用来进行简单的查询,必须要指定注解的value值。
2.10、@QueryMap
@QueryMap参数注解和上面的@Query一样,都是用于查询。借助Map映射可以携带更多的信息。
interface QueryService {
@GET("/info")
fun queryName(@QueryMap map: Map<String, String>): Call<Void>
}@QueryMap注解的参数类型必须是Map映射,同时映射的键必须是String类型。否则会抛出异常:@QueryMap parameter type must be Map. 或 @QueryMap keys must be of type String: class。
2.11、@QueryName
@QueryName注解的参数会附加到不需要值的查询URL上。参数名称默认开启URL编码。
interface QueryService {
@GET("/info")
fun queryName(@QueryName(encoded = true) map: String): Call<Void>
}2.12、@Tag
@Tag注解使用类型作为键,将参数实例添加为请求标记,不允许重复的Tag标签类型。
interface QueryService {
@GET("/info")
fun queryName(@Tag type: String): Call<Void>
}标记的参数值如果为null,将从请求中省略。 传递参数化类型(例如List<String>)将使用原始类型(即List.class)作为键。
2.13、@Url
@Url注解的参数将作为请求路径URL,请求方法注解无需再设置URL。
interface QueryService {
@GET
fun queryUrl(@Url url: String): Call<Void>
}25种注解要熟练掌握,各注解之间的依赖和互斥也要非常清楚。

