End

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 中,通过使用关键字 inlinereified,就能实现 类型实化(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

posted @ 2016-07-29 16:00  白乾涛  阅读(6888)  评论(0编辑  收藏  举报