ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Retrofit 接口声明完全指南:从请求方法注解到 Kotlin 协程的声明式 HTTP API 定义

Retrofit 接口声明完全指南:从请求方法注解到 Kotlin 协程的声明式 HTTP API 定义 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 method2.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?sortdesc)需要注意的是查询字符串部分不允许出现{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/) CallResponseBody customEndpoint(); // 带请求体的 DELETE HTTP(method DELETE, path remove/, hasBody true) CallResponseBody 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 manipulation3.1 替换块与Path请求 URL 可以通过**替换块replacement block**和参数实现动态更新。替换块是由字母数字字符组成、被{和}包裹的字符串对应的参数必须用相同名称的Path注解标记GET(group/{id}/users) CallListUser 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) CallListUser groupList(Path(id) int groupId, Query(sort) String sort);Query.java 的源码揭示了几个关键行为参数值通过Retrofit#stringConverter或默认的toString()转成字符串再做 URL 编码null值会被直接忽略foo.friends(null)得到的 URL 是/friends而不是/friends?groupnull参数类型为List或数组时每个非 null 元素都会生成一个同名查询参数例如Query(group) String... groups传入(coworker, bowling)会得到/friends?groupcoworkergroupbowlingencoded true时不做编码适合传入已经编码好的值如foobar。在 RequestFactory.java 中Query的解析会根据参数原始类型自动区分普通类型、Iterable与数组三种情况分别包装出对应的 handler。另外QueryName注解可以只提供查询参数名值为 null常用于生成?flag这类无值的查询项。3.3 复杂查询参数组合QueryMap当查询参数数量不定、需要动态组合时可以传入一个MapGET(group/{id}/users) CallListUser groupList(Path(id) int groupId, QueryMap MapString, String options);ParameterHandler.QueryMap 的实现要求QueryMap参数必须是Map且键必须为String类型Map 本身为 null、包含 null 键、null 值或值经转换后为 null都会抛出带参数位置的精确错误信息如 Query map contained null key.。3.4 动态指定完整相对 URLUrl当 URL 在运行时才能完全确定时可以用Url参数替换注解中的静态相对 URLGET CallResponseBody list(Url String url);RequestFactory.java 对Url有一套严格的约束规则类型必须是okhttp3.HttpUrl、String、java.net.URI或android.net.UriUrl不能与GET(...)等带 URL 的注解共用也不能与Path、Query、QueryName、QueryMap混用这些注解必须出现在Url之前。四、请求体Request body使用Body注解将一个对象指定为 HTTP 请求体POST(users/new) CallUser 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.。五、表单编码与 multipart5.1 表单编码FormUrlEncodedField当方法带有FormUrlEncoded注解时请求体将以application/x-www-form-urlencoded形式发送。每个键值对使用Field注解指定字段名并提供值FormUrlEncoded POST(user/edit) CallUser updateUser(Field(first_name) String first, Field(last_name) String last);Field.java 的文档给出了非常直观的例子调用foo.example(Bob Smith, President)会生成请求体nameBobSmithoccupationPresident而Field(name) String... names传入两个名字会得到nameBobSmithnameJaneDoe——List 和数组同样会展开为多个同名字段null值被忽略。Field也有encoded属性默认false表示字段名和值都需要做表单编码。源码层面的校验RequestFactory.javaField只能用于表单编码的方法反过来表单编码方法必须至少包含一个FieldForm-encoded method must contain at least one Field.。FieldMap则允许通过MapString, ?动态传入整组表单字段其键必须为String且 Map 中不能有 null 键或 null 值。5.2 MultipartMultipartPart当方法带有Multipart注解时请求体将采用multipart/form-data编码。每个 part 用Part注解声明Multipart PUT(user/photo) CallUser updateUser(Part(photo) RequestBody photo, Part(description) RequestBody description);从 Part.java 的文档看Part参数有三种处理方式类型为okhttp3.MultipartBody.Partpart 内容被直接使用注解中必须省略名称Part MultipartBody.Part part类型为okhttp3.RequestBody值直接作为 part注解中提供名称如Part(photo) RequestBody photoPart的encoding()属性默认binary指定 part 的Content-Transfer-Encoding其他对象类型由Retrofit的 Converter 转换为合适的表示注解中提供名称。也就是说multipart 的每个 part 既可以使用 Retrofit 注册的 Converter 序列化也可以让对象自己实现RequestBody来处理序列化。Part的值为 null 时该 part 会被整体忽略PartMap则支持用MapString, ?动态生成多个 part值为MultipartBody.Part的类型不允许出现在PartMap中应改用Part ListPart。与表单编码对称multipart 方法必须至少包含一个PartMultipart method must contain at least one Part.且Multipart与FormUrlEncoded互斥——同时出现会报 Only one encoding annotation is allowed.。六、请求头操作Header manipulation6.1 静态请求头HeadersHeaders注解为方法设置静态请求头支持单条或多条Headers(Cache-Control: max-age640000) GET(widget/list) CallListWidget widgetList();Headers({ Accept: application/vnd.github.v3.fulljson, User-Agent: Retrofit-Sample-App }) GET(users/{username}) CallUser 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) CallUser getUser(Header(Authorization) String authorization)Header.java 与 ParameterHandler.Header 的行为是值为 null 时该请求头会被省略非 null 时调用toString()或注册的stringConverter取结果作为头值参数为List或数组时每个非 null 元素生成一个同名头与Headers的不覆盖、全保留规则一致。6.3 动态请求头集合HeaderMap与QueryMap类似复杂的请求头组合可以用MapGET(user) CallUser getUser(HeaderMap MapString, String headers)HeaderMap要求键必须是StringMap 及其中键值均不能为 null否则抛出带参数位置的错误。HeaderMap也可以接受okhttp3.Headers类型参数见 RequestFactory.java。6.4 全局请求头OkHttp 拦截器如果某些请求头需要添加到每一个请求上如统一的鉴权、设备信息可以用 OkHttp interceptorOkHttp 拦截器机制在OkHttpClient层面统一注入而不是在每个接口方法上重复声明。这是 Retrofit 官方推荐的做法因为 Retrofit 本身就构建在 OkHttp 之上客户端层面的拦截器对请求头、日志、重试、缓存等都有全局控制能力。七、同步与异步执行Synchronous vs. asynchronousCall实例可以同步或异步执行同步调用call.execute()在当前线程阻塞直到拿到ResponseT异步调用call.enqueue(callback)请求在后台线程执行完成后回调Callback每个Call实例只能使用一次但调用clone()会得到一个新的可用实例例如在重试、并发重复请求时回调线程约定在 Android 上回调会在主线程执行便于直接更新 UI在 JVM 上回调发生在执行 HTTP 请求的那个线程上。这些行为由 Call.java 接口及 OkHttpCall.java 实现承载而 DefaultCallAdapterFactory.java 负责把接口方法声明中的CallT返回类型适配为可执行的调用对象。仓库测试 CallTest.java 对同步、异步、clone 等行为有完整覆盖。八、Kotlin 协程支持Kotlin support8.1 挂起函数直接返回Response接口方法支持 Kotlin 的suspend函数可以直接返回Response对象——Retrofit 会创建并异步执行请求同时挂起当前协程GET(users) suspend fun getUser(): ResponseUser此时调用方无需手动管理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 中有清晰实现CallT.await()内部通过suspendCancellableCoroutine包装enqueue成功响应时continuation.resume(body)非成功响应时resumeWithException(HttpException(response))网络失败时原样抛出异常awaitResponse()则总是返回完整的ResponseT不抛HttpException适合需要自行判断状态码的场景。仓库测试 KotlinSuspendTest.kt 覆盖了suspend fun body(): String、bodyNullable(): String?、response(): ResponseString、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 bodyFormUrlEncoded用于无请求体的方法FormUrlEncoded can only be specified on HTTP methods with request body表单方法没有FieldForm-encoded method must contain at least one Field.Multipart 方法没有PartMultipart method must contain at least one Part.Multipart与FormUrlEncoded同用Only one encoding annotation is allowed.非请求体方法使用BodyNon-body HTTP method cannot contain Body.Path值替换块不在 URL 中URL ... does not contain {name}.URL 查询串中包含{param}URL query string ... must not have replace block.Path参数为 nullPath parameter name value must not be null.Body参数为 nullBody parameter value must not be null.同一参数有多个 Retrofit 注解Multiple Retrofit annotations found, only one allowed.参数没有 Retrofit 注解No Retrofit annotation found.路径穿越./..Path parameters shouldnt perform path traversal (. or ..)Headers格式不是Name: ValueHeaders 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 的CallT风格还是 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进