12 | 实战:网络请求框架 KtHttp
在 Java 和 Kotlin 领域,有许多出色的网络请求框架,比如 OkHttp、Retrofit、Fuel。Retrofit 的底层使用了大量的泛型、注解和反射的技术。
- 1.0 版本,用 Java
命令式风格,实现同步式的 GET 网络请求 - 2.0 版本,用
函数式风格重构代码
Java 命令式风格
需要引入的依赖:
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.8.9'
数据类 RepoList 和 Repo
data class RepoList(var count: Int?, var items: List<Repo>?, var msg: String?)
data class Repo(
var added_stars: String?,
var avatars: List<String>?,
var desc: String?,
var forks: String?,
var lang: String?,
var repo: String?,
var repo_link: String?,
var stars: String?
)
网络请求接口 ApiService
interface ApiService {
@GET("/repo")
fun repos(@Field("lang") lang: String, @Field("since") since: String): RepoList
}
@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) // 请求参数
动态代理 Proxy
这里是利用了 Java 的动态代理来创建接口的实例。
Object newProxyInstance(ClassLoader l, Class<?>[] s, InvocationHandler h)
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
}
具体逻辑如下:
fun <T> create(service: Class<T>): T {
val loader: ClassLoader = service.classLoader
val interfaces = arrayOf<Class<*>>(service) // 对象 service 所有实现的接口
val any: Any? = Proxy.newProxyInstance(loader, interfaces) { proxy, method, args ->
// 调用 Proxy.newProxyInstance 就可以创建接口的实例化对象,且此方法符合 SAM 转换
println("${service.simpleName} - ${proxy.javaClass.simpleName} " +
"- ${method.name} - ${Arrays.toString(args)}")
// ApiService - $Proxy0 - repos - [Kotlin, weekly]
val annotations = method.annotations // 这里的 method 指的是 repos 方法
for (annotation in annotations) {
if (annotation is GET) { // 遍历所有的注解,找到 GET 注解
val value = annotation.value // 获取 GET 注解中的值
val url = "https://trendings.herokuapp.com$value" // 拼接完整的 URL
return@newProxyInstance request(url, method, args) // 调用并返回
}
}
return@newProxyInstance null // 返回 Lambda,而非 create() 函数
}
@Suppress("UNCHECKED_CAST")
return any as T // 不能保证返回值类型,这里是将其强制转换成了 T 类型
}
Lambda 表达式的返回语法:上面代码中的 return@newProxyInstance 代表返回 Lambda,而直接的 return,代表了返回 create() 这个函数。
由于
动态代理大量应用了反射,加之业务代码中还牵涉到泛型、注解和Lambda表达式,所以上面的代码可能没那么容易理解。
网络请求 OkHttp Gson
private fun request(path: String, method: Method, args: Array<Any>): Any? {
// 这里的 method 指的是 repos 方法,args 指的是调用 repos 方法时传的参数
val annotations = method.parameterAnnotations // 取出方法参数中的所有注解
if (annotations.size != args.size) return null // 参数和参数注解的个数相同
// 这两个值并不一定相等,因为有些参数可能不需要注解,有些参数可能有多个注解
var url = path // 拼接参数
for (indice in annotations.indices) {
for (parameterAnnotation in annotations[indice]) {
if (parameterAnnotation is Field) { // 筛选出 Field 注解
val key = parameterAnnotation.value
val value = args[indice].toString()
val param = "$key=$value"
url += if (url.contains("?")) "&$param" else "?$param"
}
}
}
val request = Request.Builder().url(url).build() // 底层使用 OkHttp
val response = OkHttpClient().newCall(request).execute() // 发起网络请求
val type = method.genericReturnType // 方法的返回值类型
val body: ResponseBody? = response.body
val json = body?.string()
return Gson().fromJson(json, type) // 使用 Gson 解析
}
使用与总结
通过这样的方式,我们就不必在代码当中去实现每一个接口,符合条件的任意接口和方法,都可以通过下面简单的两行代码获取接口数据。
fun main() {
val api: ApiService = create(ApiService::class.java) // 通过动态代理创建接口的实例
val data: RepoList = api.repos(lang = "Kotlin", since = "weekly") // 获取接口数据
println("${data.count} - ${data.msg} \n${Gson().toJson(data.items)}")
}
如果定义了另一个接口,这时候,我们的主体逻辑不需要做任何的改动,直接像下面这样调用即可:
interface GitHubService {
@GET("/search")
fun search(@Field("id") id: String): User
}
val api: GitHubService = create(GitHubService::class.java) // 换成另外一个接口
val data: User = api.search(id = "JetBrains") //换成另外一个方法,传相应的参数
可以发现,使用动态代理实现网络请求的灵活性非常好。只要我们定义的 Service 接口拥有对应的注解,我们就可以通过注解与反射,将这些信息拼凑在一起。
实际上,我们的 KtHttp,就是将 URL 的信息存储在了注解当中(比如 lang 和 since),而实际的参数值,是在函数调用的时候传进来的(比如 Kotlin 和 weekly)。我们通过泛型、注解、反射的结合,将这些信息集到一起,完成整个 URL 的拼接,最后才通过 OkHttp 完成的网络请求。
函数式风格重构
by lazy 懒加载委托
private var okHttpClient: OkHttpClient = OkHttpClient()
private var gson: Gson = Gson()
// 支持懒加载
private val okHttpClient by lazy { OkHttpClient() }
private val gson by lazy { Gson() }
类型实化 -- 真泛型
在下面的代码中,create() 会接收一个 Class<T> 类型的参数。其实,针对这样的情况,我们完全可以省略掉这个参数。
fun <T> create(service: Class<T>): T {
return Proxy.newProxyInstance(
service.classLoader,
arrayOf<Class<*>>(service)
) { ... }
}
正常情况下,泛型参数类型会被擦除,这就是 Java 的泛型被称为伪泛型的原因。而通过使用 inline 和 reified 这两个关键字,我们就能实现 类型实化(Reified Type),也就是真泛型。进一步,我们就可以在下面代码注释 ①、② 的地方,使用 T::class.java 来得到 Class 对象。
inline fun <reified T> create(): T { // 省略参数 Class<T>,添加关键字 inline 和 reified
return Proxy.newProxyInstance(
T::class.java.classLoader, // 使用 T::class.java 来得到 Class 对象
arrayOf(T::class.java) // 使用 T::class.java 来得到 Class 对象
) { ... }
}
使用标准库函数优化 create
在 create() 方法中,我们需要读取 method 当中的 GET 注解,解析出它的值,然后与 baseURL 拼接。这里完全可以借助 Kotlin 的标准库函数来实现:
inline fun <reified T> create(): T {
return Proxy.newProxyInstance(
T::class.java.classLoader,
arrayOf(T::class.java)
) { proxy, method, args ->
return@newProxyInstance method.annotations // 获取 method 的所有注解
.filterIsInstance<GET>() // 筛选出 GET 注解
.takeIf { it.size == 1 } // 筛选出 GET 注解的数量是 1 的注解
?.let { request("$baseUrl${it[0].value}", method, args) } // 判空后请求
} as T
}
使用标准库函数优化 request
下面同样借助 Kotlin 的标准库函数,对 request() 方法进行重构。
fun request(url: String, method: Method, args: Array<Any>): Any? = method
.parameterAnnotations // 获取方法当中所有的参数注解
.takeIf { it.size == args.size } // 判断数量是否相等
?.mapIndexed { index, it -> Pair(it, args[index]) } // 映射配对
?.fold(url, ::parseUrl) // 函数引用,fold 是高阶函数版的 for 循环
?.let { Request.Builder().url(it).build() } // 构建 Request 对象
?.let { okHttpClient.newCall(it).execute().body?.string() } // 网络请求
?.let { gson.fromJson(it, method.genericReturnType) } // Gson 解析
上面的代码中,我们引用了一个实现 URL 拼接的 parseUrl() 函数:
private fun parseUrl(acc: String, pair: Pair<Array<Annotation>, Any>) =
pair.first
.filterIsInstance<Field>() // 筛选出 Field 类型的注解
.first() // 取出第一个 Field 注解,这里它也应该是唯一的
.let { field -> // 拼接参数
val param = "${field.value}=${pair.second}"
if (acc.contains("?")) "$acc&$param" else "$acc?$param"
}
至此,我们 2.0 版本的代码就完成了。
小结
- 在 1.0 版本的代码中,我们灵活利用了 动态代理、泛型、注解、反射 这几个技术,实现了 KtHttp 的基础功能
- 动态代理:我们通过 ApiImpl 这个类,模拟了它动态生成的 Proxy 类
- 泛型:我们将其用在了动态代理的 create() 方法上,后面我们还使用了类型实化的技术
- 注解:我们自定义了两个注解
-
反射:我们通过反射分析了 repos() 方法
- 从方法注解中取出了
注解的值,用于拼接网络请求的地址 - 还取出了方法的
返回值类型,用于 Gson 数据解析
- 从方法注解中取出了
- 在 2.0 版本的代码中,我们以
函数式的思维重写了 KtHttp 的内部逻辑,并且大量使用了 Kotlin 标准库里的高阶函数,进一步提升了代码的可读性
相比起前面实战课,这一次我们的函数式范式的代码,实现起来没有那么得流畅。因为 Kotlin 提供了强大的集合操作符,这就让 Kotlin 十分擅长集合操作的场景。而对于注解、反射相关的场景,函数式的编程范式就没那么擅长了。
2016-07-29