Kotlin 朱涛-12 实战 Retrofit 动态代理 泛型 注解 反射
目录
目录
12 | 实战:网络请求框架 KtHttp
Retrofit 的底层使用了大量的泛型、注解和反射
的技术。
环境
依赖库
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "org.jetbrains.kotlin:kotlin-reflect"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.9.0'
导包
import com.google.gson.Gson
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import java.lang.Exception
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.lang.reflect.Type
数据类 Repo 和 RepoList
data class Repo(var id: Long, var name: String, var private: Boolean)
data class RepoList(var total_count: Long, var items: List<Repo>?)
注解 GET 和 Field
@Target(AnnotationTarget.FUNCTION) // 修饰函数
@Retention(AnnotationRetention.RUNTIME) // 运行时可访问 -- 反射的前提
annotation class GET(val value: String) // 请求方式
@Target(AnnotationTarget.VALUE_PARAMETER) // 修饰参数
@Retention(AnnotationRetention.RUNTIME) // 运行时可访问 -- 反射的前提
annotation class Field(val value: String) // 请求参数
接口 ApiService
interface ApiService {
@GET("/search/repositories")
// https://api.github.com/search/repositories?q=flutter
// https://github.com/search?q=flutter&type=repositories
// 参考 https://github.com/crazyandcoder/awesome-github-api/blob/master/doc
fun search(@Field("q") q: String): RepoList
}
核心逻辑
动态代理 + 类型实化
下面代码的核心逻辑是:利用 Java 的 动态代理
和 Kotlin 的 类型实化
来 创建接口的实例
。
inline fun <reified T> create(): T {
val service: Class<T> = T::class.java // 类型实化 -- 真泛型
println("create: ${service.simpleName}") // ApiService
// the class loader to define the proxy class
val loader: ClassLoader = service.classLoader
// the list of interfaces for the proxy class to implement
val interfaces: Array<Class<*>> = arrayOf(service)
// 创建接口的实例化对象: a proxy instance with the specified invocation handler
val proxyInstance: Any? = Proxy.newProxyInstance(loader, interfaces, handler)
return proxyInstance as T
}
注意上面代码中的方法声明。
正常情况下,泛型参数类型会被擦除
,这就是 Java 的泛型被称为伪泛型
的原因。而在 Kotlin 中,通过使用关键字 inline
和 reified
,就能实现 类型实化(Reified Type),也就是真泛型
。
借助此特性,我们就能在运行时通过 T::class.java
获取泛型的真实类型。
InvocationHandler
下面代码的核心逻辑是:
- 读取 method 当中的所有注解
- 筛选出类型是 GET 的注解
- 解析出它的值并与 baseUrl 拼接
const val baseUrl = "https://api.github.com"
val handler: (proxy: Any, method: Method, args: Array<Any>) -> Any? = newProxyInstance@{ _, method, args ->
println("${method.name} - ${args.contentToString()}") // search - [Kotlin, bqt]
val path: String = method // 获取 method(即 search 方法) 的所有注解
.annotations // 类型是 Array<Annotation>
.filterIsInstance<GET>() // 筛选出类型是 GET 的注解
.takeIf { it.size == 1 } // 筛选出 GET 注解的数量是 1 的注解
?.firstOrNull()
?.value ?: ""
val url = "$baseUrl$path" // 拼接完整的 URL
println("$path - $url") // https://api.github.com/search/repositories
return@newProxyInstance request(url, method, args) // 调用并返回 Lambda
}
上面用到了 Lambda 表达式的返回语法
上述代码大量应用了
反射
、泛型
、注解
和Lambda
表达式,所以上面的代码没那么容易理解。
拼接参数
下面代码的核心逻辑是:借助 Kotlin 的标准库函数,将注解中的 key value 拼接到 GET 请求的 url 中
private fun getRequestUrl(url: String, method: Method, args: Array<Any>): String {
// 这里的 method 指的是 search 方法,args 指的是调用 search 方法时传的参数
// 参数和参数注解的个数不一定相等,因为有些参数可能不需要注解,有些参数可能有多个注解
var requestUrl = url
method.parameterAnnotations // 类型是 Array<Array<Annotation>>
.takeIf { it.size == args.size } // 判断数量是否相等
?.mapIndexed { i, it -> Pair(it, args[i]) } // 映射配对
?.forEach { pair: Pair<Array<Annotation>, Any> ->
val key: String = pair.first
.filterIsInstance<Field>() // 筛选出 Field 类型的注解
.firstOrNull() // 取出第一个,这里它也应该是唯一的
?.value ?: ""
val value: String = pair.second.toString()
val param = "$key=$value"
val connector = if (requestUrl.contains("?")) "&" else "?"
requestUrl += "$connector$param"
}
return requestUrl
}
request: OkHttp 发请求
下面代码的核心逻辑是:借助 OkHttp 发起网络请求,并使用 Gson 解析返回的内容
private val okHttpClient: OkHttpClient by lazy { OkHttpClient() }
private val gson: Gson by lazy { Gson() }
private fun request(url: String, method: Method, args: Array<Any>): Any? {
val requestUrl: String = getRequestUrl(url, method, args)
println(requestUrl) // https://api.github.com/search/repositories?q=Kotlin&xx=bqt
val request: Request = Request.Builder().url(requestUrl).build() // 底层使用 OkHttp
val response: Response = okHttpClient.newCall(request).execute() // 发起网络请求
val type: Type? = method.genericReturnType // 方法的返回值类型
val body: ResponseBody? = response.body
val json: String? = body?.string()
try {
return gson.fromJson<Any?>(json, type)// 使用 Gson 解析
} catch (e: Exception) {
println(json)
}
return null
}
使用与总结
fun main() {
val api = create<ApiService>() // 创建接口的实例
val repoList: RepoList = api.search("Kotlin","bqt") // 获取接口数据
val repo: Repo? = repoList.items?.get(0)
println("${repoList.total_count} - ${repoList.items?.size}") // 265378 - 30
println(repo.toString()) // Repo(id=3432266, name=kotlin, private=false)
}
通过这样的方式,我们就不必在代码当中去实现每一个接口,符合条件的任意接口和方法,都可以通过简单的两行代码获取接口数据。
可以发现,使用动态代理实现网络请求的灵活性非常好。只要我们定义的 Service 接口拥有对应的注解,我们就可以通过注解与反射
,将这些信息拼凑在一起。
2016-07-29
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/5718742.html