Retrofit 接口声明完全指南:从请求方法注解到 Kotlin 协程的声明式 HTTP API 定义
【免费下载链接】retrofitA type-safe HTTP client for Android and the JVM项目地址: https://gitcode.com/gh_mirrors/re/retrofit
本指南以仓库文档 declarations.md 为核心骨架,系统讲解 Retrofit 中如何通过接口方法及参数上的注解声明 HTTP 请求的处理方式——从请求方法、URL 拼接、请求体、表单与 multipart 编码、请求头,到同步/异步执行与 Kotlin 挂起函数支持。文章同时深入 RequestFactory.java 与 ParameterHandler.java 等源码,揭示每一条注解背后的解析逻辑与校验规则。读完本文,你将能够熟练编写类型安全的 Retrofit 服务接口,并理解其底层工作机理。
一、总览:注解如何描述一次请求
Retrofit 的核心理念是:接口方法及其参数上的注解,决定了这个请求将被如何处理。你不需要手写 URL 拼接、编码、表单序列化等样板代码——只需在接口上声明意图,Retrofit 在运行时通过反射解析这些注解,构造出完整的okhttp3.Request。
这一过程发生在 RequestFactory.java 的Builder.build()中:它遍历方法注解(如@GET、@Headers、@FormUrlEncoded、@Multipart),再逐个遍历参数注解(如@Path、@Query、@Body),为每个参数生成对应的ParameterHandler。之后的每次调用,这些 handler 会把实参写入RequestBuilder,最终拼装出真正的 HTTP 请求。值得注意的是,注解解析只发生一次并缓存复用——这也是 Retrofit 能够高效运行的关键设计。
后续小节将按照原文档的脉络,逐一讲解各类注解的用法与底层实现。
二、请求方法注解(Request method)
2.1 八种内建注解
每个接口方法都必须有一个 HTTP 注解,用于提供请求方法(HTTP method)和相对 URL。Retrofit 内置八种注解:
| 注解 | HTTP 方法 | 是否允许请求体 |
|---|---|---|
@GET | GET | 否 |
@POST | POST | 是 |
@PUT | PUT | 是 |
@PATCH | PATCH | 是 |
@DELETE | DELETE | 否 |
@OPTIONS | OPTIONS | 否 |
@HEAD | HEAD | 否 |
@HTTP | 自定义(由method属性指定) | 由hasBody指定 |
相对 URL 直接写在注解的value属性中:
@GET("users/list")value也可以留空(此时必须配合@Url参数,见 3.4 节)。从 GET.java 的源码可以看到,该注解只含一个String value() default ""属性,指向的既可以是相对路径,也可以是绝对路径,甚至完整 URL,最终会与Retrofit.Builder#baseUrl组合解析出完整的端点地址。
2.2 在相对 URL 上直接指定静态查询参数
可以在相对 URL 中直接写死查询参数:
@GET("users/list?sort=desc")需要注意的是,查询字符串部分不允许出现{param}形式的替换块。源码 RequestFactory.java 会先以?切分 URL,再用正则\{([a-zA-Z][a-zA-Z0-9_-]*)\}检查查询串,一旦发现替换块就抛出错误并提示"对于动态查询参数请使用@Query"。
2.3 自定义 HTTP 方法:@HTTP
当内置注解不够用时,@HTTP允许你声明任意 HTTP 动词,甚至让 DELETE 携带请求体。这在 HTTP.java 的文档注释中有示例:
interface Service { @HTTP(method = "CUSTOM", path = "custom/endpoint/") Call<ResponseBody> customEndpoint(); // 带请求体的 DELETE @HTTP(method = "DELETE", path = "remove/", hasBody = true) Call<ResponseBody> deleteObject(@Body RequestBody object); }在 RequestFactory.java 中,@HTTP的三个属性分别对应解析出的 HTTP 方法、相对路径与hasBody标志。若一个方法上同时出现两个 HTTP 方法注解,解析器会直接报错:"Only one HTTP method is allowed.";若方法没有任何 HTTP 注解,则会报"HTTP method annotation is required (e.g., @GET, @POST, etc.)"。
三、URL 操作(URL manipulation)
3.1 替换块与@Path
请求 URL 可以通过**替换块(replacement block)**和参数实现动态更新。替换块是由字母数字字符组成、被{和}包裹的字符串;对应的参数必须用相同名称的@Path注解标记:
@GET("group/{id}/users") Call<List<User>> groupList(@Path("id") int groupId);从 Path.java 可以看到,@Path有两个属性:
String value():URL 中的替换块名称,必须与 URL 中{name}完全一致;boolean encoded() default false:标记参数值是否已经做过 URL 编码,默认会再次编码。
关于编码行为,Path的文档给出了精确的对照:默认情况下值会被 URL 编码——传入"John%Doe"会得到/user/John%25Doe;而设置encoded = true后原样透传,得到/user/John%Doe。
在运行时,替换发生在 RequestBuilder.addPathParam:先用relativeUrl.replace("{" + name + "}", replacement)做字符串替换,然后立刻用PATH_TRAVERSAL正则(匹配.、..及其百分号编码形式%2e)检查结果,一旦出现路径穿越(path traversal)就抛出 IllegalArgumentException,防止诸如DELETE /account/book/{isbn}/被..篡改成DELETE /account/这类安全隐患。
同时 ParameterHandler.Path 明确规定:@Path参数不允许为 null,否则抛出 "Path parameter "name" value must not be null."。@Path的命名还必须匹配[a-zA-Z][a-zA-Z0-9_-]*,且 URL 中必须真实存在该替换块,否则报 "URL "..." does not contain "{name}""。
3.2 查询参数:@Query
动态查询参数使用@Query注解:
@GET("group/{id}/users") Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);Query.java 的源码揭示了几个关键行为:
- 参数值通过
Retrofit#stringConverter(或默认的toString())转成字符串,再做 URL 编码; null值会被直接忽略:foo.friends(null)得到的 URL 是/friends而不是/friends?group=null;- 参数类型为
List或数组时,每个非 null 元素都会生成一个同名查询参数,例如@Query("group") String... groups传入("coworker", "bowling")会得到/friends?group=coworker&group=bowling; encoded = true时不做编码,适合传入已经编码好的值(如"foo+bar")。
在 RequestFactory.java 中,@Query的解析会根据参数原始类型自动区分普通类型、Iterable与数组三种情况,分别包装出对应的 handler。另外,@QueryName注解可以只提供查询参数名(值为 null),常用于生成?flag这类无值的查询项。
3.3 复杂查询参数组合:@QueryMap
当查询参数数量不定、需要动态组合时,可以传入一个Map:
@GET("group/{id}/users") Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);ParameterHandler.QueryMap 的实现要求:
@QueryMap参数必须是Map,且键必须为String类型;- Map 本身为 null、包含 null 键、null 值,或值经转换后为 null,都会抛出带参数位置的精确错误信息(如 "Query map contained null key.")。
3.4 动态指定完整相对 URL:@Url
当 URL 在运行时才能完全确定时,可以用@Url参数替换注解中的静态相对 URL:
@GET Call<ResponseBody> list(@Url String url);RequestFactory.java 对@Url有一套严格的约束规则:类型必须是okhttp3.HttpUrl、String、java.net.URI或android.net.Uri;@Url不能与@GET("...")等带 URL 的注解共用,也不能与@Path、@Query、@QueryName、@QueryMap混用(这些注解必须出现在@Url之前)。
四、请求体(Request body)
使用@Body注解将一个对象指定为 HTTP 请求体:
@POST("users/new") Call<User> createUser(@Body User user);根据 ParameterHandler.Body 的实现:
- 对象会交给
Retrofit实例上注册的Converter(如 Gson、Moshi、Jackson 转换器)转换成okhttp3.RequestBody; - 如果没有注册任何 Converter,则只能使用
RequestBody类型作为@Body参数(内置转换器直接透传); @Body参数不允许为 null;- 一个方法中只能有一个
@Body,且@Body不能与表单/ multipart 编码共用("@Body parameters cannot be used with form or multi-part encoding."); - 若 HTTP 方法本身不允许请求体(如
@GET),使用@Body会报 "Non-body HTTP method cannot contain @Body."。
五、表单编码与 multipart
5.1 表单编码:@FormUrlEncoded+@Field
当方法带有@FormUrlEncoded注解时,请求体将以application/x-www-form-urlencoded形式发送。每个键值对使用@Field注解,指定字段名并提供值:
@FormUrlEncoded @POST("user/edit") Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);Field.java 的文档给出了非常直观的例子:调用foo.example("Bob Smith", "President")会生成请求体name=Bob+Smith&occupation=President;而@Field("name") String... names传入两个名字会得到name=Bob+Smith&name=Jane+Doe——List 和数组同样会展开为多个同名字段,null值被忽略。
@Field也有encoded属性,默认false表示字段名和值都需要做表单编码。
源码层面的校验(RequestFactory.java):@Field只能用于表单编码的方法;反过来,表单编码方法必须至少包含一个@Field("Form-encoded method must contain at least one @Field.")。@FieldMap则允许通过Map<String, ?>动态传入整组表单字段,其键必须为String,且 Map 中不能有 null 键或 null 值。
5.2 Multipart:@Multipart+@Part
当方法带有@Multipart注解时,请求体将采用multipart/form-data编码。每个 part 用@Part注解声明:
@Multipart @PUT("user/photo") Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);从 Part.java 的文档看,@Part参数有三种处理方式:
- 类型为
okhttp3.MultipartBody.Part:part 内容被直接使用,注解中必须省略名称(@Part MultipartBody.Part part); - 类型为
okhttp3.RequestBody:值直接作为 part,注解中提供名称(如@Part("photo") RequestBody photo),@Part的encoding()属性(默认"binary")指定 part 的Content-Transfer-Encoding; - 其他对象类型:由
Retrofit的 Converter 转换为合适的表示,注解中提供名称。
也就是说,multipart 的每个 part 既可以使用 Retrofit 注册的 Converter 序列化,也可以让对象自己实现RequestBody来处理序列化。@Part的值为 null 时该 part 会被整体忽略;@PartMap则支持用Map<String, ?>动态生成多个 part(值为MultipartBody.Part的类型不允许出现在@PartMap中,应改用@Part List<Part>)。
与表单编码对称,multipart 方法必须至少包含一个@Part("Multipart method must contain at least one @Part."),且@Multipart与@FormUrlEncoded互斥——同时出现会报 "Only one encoding annotation is allowed."。
六、请求头操作(Header manipulation)
6.1 静态请求头:@Headers
@Headers注解为方法设置静态请求头,支持单条或多条:
@Headers("Cache-Control: max-age=640000") @GET("widget/list") Call<List<Widget>> widgetList();@Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-Sample-App" }) @GET("users/{username}") Call<User> getUser(@Path("username") String username);两点重要行为:
- 同名请求头不会相互覆盖,所有同名头都会包含在请求中("headers do not overwrite each other");
- 在 RequestFactory.parseHeaders 中,每个条目必须符合
"Name: Value"格式(冒号不能缺失或位于首尾),否则报错;若头名是Content-Type,其值会被解析为MediaType并作为请求的 content type。
6.2 动态请求头:@Header
请求头也可以在方法参数上动态更新,使用@Header注解:
@GET("user") Call<User> getUser(@Header("Authorization") String authorization)Header.java 与 ParameterHandler.Header 的行为是:
- 值为 null 时该请求头会被省略;
- 非 null 时调用
toString()(或注册的stringConverter)取结果作为头值; - 参数为
List或数组时,每个非 null 元素生成一个同名头(与@Headers的"不覆盖、全保留"规则一致)。
6.3 动态请求头集合:@HeaderMap
与@QueryMap类似,复杂的请求头组合可以用Map:
@GET("user") Call<User> getUser(@HeaderMap Map<String, String> headers)@HeaderMap要求键必须是String,Map 及其中键值均不能为 null,否则抛出带参数位置的错误。@HeaderMap也可以接受okhttp3.Headers类型参数(见 RequestFactory.java)。
6.4 全局请求头:OkHttp 拦截器
如果某些请求头需要添加到每一个请求上(如统一的鉴权、设备信息),可以用 OkHttp interceptor(OkHttp 拦截器机制)在OkHttpClient层面统一注入,而不是在每个接口方法上重复声明。这是 Retrofit 官方推荐的做法,因为 Retrofit 本身就构建在 OkHttp 之上,客户端层面的拦截器对请求头、日志、重试、缓存等都有全局控制能力。
七、同步与异步执行(Synchronous vs. asynchronous)
Call实例可以同步或异步执行:
- 同步:调用
call.execute(),在当前线程阻塞直到拿到Response<T>; - 异步:调用
call.enqueue(callback),请求在后台线程执行,完成后回调Callback; - 每个
Call实例只能使用一次,但调用clone()会得到一个新的可用实例(例如在重试、并发重复请求时); - 回调线程约定:在 Android 上,回调会在主线程执行(便于直接更新 UI);在 JVM 上,回调发生在执行 HTTP 请求的那个线程上。
这些行为由 Call.java 接口及 OkHttpCall.java 实现承载,而 DefaultCallAdapterFactory.java 负责把接口方法声明中的Call<T>返回类型适配为可执行的调用对象。仓库测试 CallTest.java 对同步、异步、clone 等行为有完整覆盖。
八、Kotlin 协程支持(Kotlin support)
8.1 挂起函数直接返回Response
接口方法支持 Kotlin 的suspend函数,可以直接返回Response对象——Retrofit 会创建并异步执行请求,同时挂起当前协程:
@GET("users") suspend fun getUser(): Response<User>此时调用方无需手动管理Call的创建与回调,代码可以像顺序执行一样书写。从 RequestFactory.java 可以看到实现机理:Kotlin 挂起函数编译后会在参数列表末尾追加一个kotlin.coroutines.Continuation参数,解析器识别到该参数后将方法标记为isKotlinSuspendFunction,并在构造请求时排除它(RequestFactory.create)。
8.2 直接返回响应体与HttpException
挂起函数也可以直接返回响应体:
@GET("users") suspend fun getUser(): User此时若服务端返回非 2XX 状态码,Retrofit 会抛出包含完整Response的HttpException(见 HttpException.java),协程调用方可通过 try/catch 捕获并检查e.response()。
这一语义在 KotlinExtensions.kt 中有清晰实现:Call<T>.await()内部通过suspendCancellableCoroutine包装enqueue,成功响应时continuation.resume(body),非成功响应时resumeWithException(HttpException(response)),网络失败时原样抛出异常;awaitResponse()则总是返回完整的Response<T>(不抛HttpException),适合需要自行判断状态码的场景。仓库测试 KotlinSuspendTest.kt 覆盖了suspend fun body(): String、bodyNullable(): String?、response(): Response<String>、unit()及带@Path参数的挂起方法等多种形态。
九、常见声明错误速查
结合 RequestFactory.java 的校验逻辑,整理接口声明阶段最常见的错误,便于你在编写服务接口时提前规避:
| 错误场景 | 报错信息(节选) |
|---|---|
| 方法缺少 HTTP 方法注解 | "HTTP method annotation is required (e.g., @GET, @POST, etc.)" |
| 方法上出现多个 HTTP 方法注解 | "Only one HTTP method is allowed." |
@Multipart用于无请求体的方法 | "Multipart can only be specified on HTTP methods with request body" |
@FormUrlEncoded用于无请求体的方法 | "FormUrlEncoded can only be specified on HTTP methods with request body" |
表单方法没有@Field | "Form-encoded method must contain at least one @Field." |
Multipart 方法没有@Part | "Multipart method must contain at least one @Part." |
@Multipart与@FormUrlEncoded同用 | "Only one encoding annotation is allowed." |
非请求体方法使用@Body | "Non-body HTTP method cannot contain @Body." |
@Path值(替换块)不在 URL 中 | "URL "..." does not contain "{name}"." |
URL 查询串中包含{param} | "URL query string ... must not have replace block." |
@Path参数为 null | "Path parameter "name" value must not be null." |
@Body参数为 null | "Body parameter value must not be null." |
| 同一参数有多个 Retrofit 注解 | "Multiple Retrofit annotations found, only one allowed." |
| 参数没有 Retrofit 注解 | "No Retrofit annotation found." |
路径穿越(./..) | "@Path parameters shouldn't perform path traversal ('.' or '..')" |
@Headers格式不是"Name: Value" | "@Headers value must be in the form "Name: Value"." |
十、小结
声明式注解是 Retrofit 类型安全 HTTP 客户端的基石。通过本文可以总结出三条核心规律:
- 方法级注解定基调:HTTP 方法 + 相对 URL 由
@GET/@POST/@HTTP等决定,@FormUrlEncoded/@Multipart决定编码形态,@Headers提供静态头; - 参数级注解做动态化:
@Path、@Query、@QueryMap、@Header、@HeaderMap、@Field、@Part、@Body、@Url让 URL、头、表单与请求体在运行时动态生成; - 解析一次、复用多次:所有注解在 RequestFactory.java 中一次性解析为
ParameterHandler链并缓存,运行时只做参数绑定与请求拼装。
无论是纯 Java 的Call<T>风格,还是 Kotlin 协程的suspend fun风格,只要遵循上表所示的声明规则,Retrofit 都能安全、高效地将接口声明转化为真实可执行的 HTTP 请求。如果你需要进一步了解Retrofit实例与baseUrl的构建细节,可以继续阅读 configuration.md。
【免费下载链接】retrofitA type-safe HTTP client for Android and the JVM项目地址: https://gitcode.com/gh_mirrors/re/retrofit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考