18、网络请求注解:Retrofit风格的注解实现、动态代理与反射结合
说实话,Retrofit 这个库,咱们 Android 开发几乎天天用。但你有没有想过,它底层到底是怎么把「一个接口方法」变成「一个完整的网络请求」的?
我个人习惯把这种技术叫做「声明式网络请求」。你只需要写个接口,加几个注解,剩下的脏活累活,框架全帮你干了。今天我们就来手撕一下这个机制的核心——注解 + 动态代理 + 反射。
18.1 Retrofit 的核心思想
先看一个典型的 Retrofit 用法:
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): User
}
你写了个接口,加了个 @GET 注解,然后 Retrofit 就能帮你发 HTTP 请求了。这背后其实就三步:
- 解析注解:从方法上拿到
@GET、@Path等注解信息。 - 动态代理:在运行时生成接口的实现类。
- 反射调用:把方法调用转换成网络请求。
嗯,这里要注意,Retrofit 并没有在编译期生成代码,它用的是运行时的动态代理。这一点和很多人的直觉不一样。
核心结论:Retrofit 的注解处理发生在运行时,通过动态代理拦截方法调用,再通过反射读取注解信息,最终拼装成 OkHttp 的 Request。
18.2 自定义网络请求注解
我们先定义一套自己的注解。我建议你按照 Retrofit 的风格来,但可以简化一些。
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GET(val value: String)
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Path(val value: String)
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Query(val value: String)
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Body
这里我用了 RUNTIME 保留策略,因为我们要在运行时通过反射读取它们。如果你用 SOURCE 或 CLASS,运行时是拿不到的。
小提示:注解的 @Retention 一定要选对。我见过不少新手把 RUNTIME 写成 SOURCE,结果反射拿不到注解,排查了半天。
18.3 动态代理:接口的「替身」
动态代理是 Java 反射包里的一个机制。它可以在运行时动态创建一个接口的实现类。说白了,你定义一个接口,不用写实现类,代理对象会帮你拦截所有方法调用。
看代码:
fun <T> create(serviceClass: Class<T>): T {
return Proxy.newProxyInstance(
serviceClass.classLoader,
arrayOf(serviceClass)
) { proxy, method, args ->
// 这里就是拦截点
handleMethod(method, args)
} as T
}
这个 handleMethod 就是核心。它要做三件事:
- 读取方法上的
@GET注解,拿到 URL 路径。 - 读取参数上的
@Path、@Query、@Body注解,拿到参数值。 - 拼装成 HTTP 请求,返回结果。
18.4 反射读取注解信息
接下来就是反射上场了。我们来看 handleMethod 的具体实现:
private fun handleMethod(method: Method, args: Array<Any?>): Any? {
// 1. 获取方法上的 GET 注解
val getAnnotation = method.getAnnotation(GET::class.java)
?: throw IllegalArgumentException("缺少 @GET 注解")
var urlPath = getAnnotation.value
val parameterAnnotations = method.parameterAnnotations
val parameterTypes = method.parameterTypes
// 2. 遍历参数,处理 @Path、@Query、@Body
val queryParams = mutableMapOf<String, String>()
var body: Any? = null
for (i in args.indices) {
val annotations = parameterAnnotations[i]
for (annotation in annotations) {
when (annotation) {
is Path -> {
val placeholder = "{${annotation.value}}"
urlPath = urlPath.replace(placeholder, args[i].toString())
}
is Query -> {
queryParams[annotation.value] = args[i].toString()
}
is Body -> {
body = args[i]
}
}
}
}
// 3. 拼装请求(这里简化了,实际要拼接 baseUrl)
println("请求路径: $urlPath")
println("查询参数: $queryParams")
println("请求体: $body")
// 4. 返回模拟结果(实际会走 OkHttp)
return "模拟响应"
}
这里有个细节:method.parameterAnnotations 返回的是一个二维数组。第一维是参数索引,第二维是该参数上的所有注解。为什么是二维?因为一个参数上可以加多个注解嘛。
注意:反射读取注解时,getAnnotation 和 parameterAnnotations 的性能开销都不小。如果你在循环中频繁调用,建议做缓存。我曾在项目中遇到过因为反射调用过多导致卡顿的问题,后来用 Map<Method, MethodInfo> 缓存了解析结果,性能提升很明显。
18.5 完整示例:跑起来看看
我们把上面几块拼起来,写个完整的 demo:
interface ApiService {
@GET("users/{id}/posts")
fun getPosts(
@Path("id") userId: Int,
@Query("page") page: Int
): String
}
fun main() {
val service = create(ApiService::class.java)
val result = service.getPosts(123, 2)
println(result)
}
运行结果:
请求路径: users/123/posts
查询参数: {page=2}
请求体: null
模拟响应
你看,@Path("id") 把 123 替换进了 URL,@Query("page") 把 2 变成了查询参数。整个过程没有写一行实现代码,全靠注解 + 动态代理 + 反射。
18.6 知识体系图
下面这张图帮你理清整个流程:
18.7 避坑指南
最后分享几个我踩过的坑:
- 注解保留策略:一定要用
RUNTIME,否则反射拿不到。我曾经在代码审查时发现有人用了SOURCE,问他为什么,他说「我忘了」。 - 参数顺序:反射拿到的参数注解数组,顺序必须和接口定义一致。如果你在接口里调换了参数位置,反射解析时就会乱掉。
- 性能问题:动态代理本身开销不大,但反射读取注解是耗时的。建议在第一次调用时解析并缓存结果,后续直接复用。
- 泛型擦除:如果你要解析返回类型的泛型(比如
List<User>),需要用ParameterizedType来处理。Retrofit 在这方面做了很多工作,我们这里只是演示核心原理,所以简化了。
好了,这一章的内容就到这里。你想想看,其实 Retrofit 的核心并不神秘,就是注解 + 动态代理 + 反射这三板斧。掌握了这些,你甚至可以自己写一个轻量级的网络请求框架。