Retrofit 接口声明完全指南:从请求方法注解到 Kotlin 协程的声明式 HTTP API 定义
2026/9/19 18:43:20 网站建设 项目流程

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 方法是否允许请求体
@GETGET
@POSTPOST
@PUTPUT
@PATCHPATCH
@DELETEDELETE
@OPTIONSOPTIONS
@HEADHEAD
@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.HttpUrlStringjava.net.URIandroid.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参数有三种处理方式:

  1. 类型为okhttp3.MultipartBody.Part:part 内容被直接使用,注解中必须省略名称@Part MultipartBody.Part part);
  2. 类型为okhttp3.RequestBody:值直接作为 part,注解中提供名称(如@Part("photo") RequestBody photo),@Partencoding()属性(默认"binary")指定 part 的Content-Transfer-Encoding
  3. 其他对象类型:由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 会抛出包含完整ResponseHttpException(见 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(): StringbodyNullable(): 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 客户端的基石。通过本文可以总结出三条核心规律:

  1. 方法级注解定基调:HTTP 方法 + 相对 URL 由@GET/@POST/@HTTP等决定,@FormUrlEncoded/@Multipart决定编码形态,@Headers提供静态头;
  2. 参数级注解做动态化@Path@Query@QueryMap@Header@HeaderMap@Field@Part@Body@Url让 URL、头、表单与请求体在运行时动态生成;
  3. 解析一次、复用多次:所有注解在 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询