Kotlin实现网络请求框架

181 阅读14分钟

思路:使用 动态代理 + 注解 + 反射 实现这个网络请求框架;

运用Kotlin 的委托、泛型、注解、反射这几个高级特性。来写一个 Kotlin 版本的 HTTP 网络请求框架。由于它是纯 Kotlin 开发的,所以把它叫 KtHttp 吧。事实上,在 Java 和 Kotlin 领域,有许多出色的网络请求框架,比如 OkHttp、Retrofit、Fuel。而今天要实现的 KtHttp,它的灵感来自于 Retrofit。之所以选择 Retrofit 作为借鉴的对象,是因为它的底层使用了大量的泛型、注解和反射的技术。

从 0 开始实现这个网络请求框架。代码会分为两个版本:

1.0 版本,用 Java 思维,以最简单直白的方式来实现 KtHttp 的基础功能——同步式的 GET 网络请求;

2.0 版本,用函数式思维来重构代码。

1.0:Java 思维

首先定义两个注解:

image.png

@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)

GET 注解只能用于修饰函数,Field 注解只能用于修饰参数。另外,这两个注解的 Retention 都是 AnnotationRetention.RUNTIME,这意味着这两个注解都是运行时可访问的。

先过一遍1.0的完整代码,后面再一步步解析


//任何支持Get API
interface ApiService {
    @GET("/repo")
    fun repos(
        @Field("lang") lang: String,
        @Field("since") since: String
    ): RepoList
}

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?
)


object KHttpV1 {

    private var okHttpClient: OkHttpClient = OkHttpClient()
    private var gson: Gson = Gson()
    var baseUrl = "https://trendings.herokuapp.com"

    fun <T> create(service: Class<T>): T {
        return Proxy.newProxyInstance(
            service.classLoader,
            arrayOf<Class<*>>(service)
        ) { proxy, method, args ->
            val annotations = method.annotations
            for (annotation in annotations) {
                if (annotation is GET) {
                    val url = baseUrl + annotation.value
                    return@newProxyInstance invoke(url, method, args)
                }
            }
            return@newProxyInstance null
        } as T
    }

    private fun invoke(path: String, method: Method, args: Array<Any>?): Any? {
        if (method.parameterAnnotations.size != args?.size) return null

        var url = path
        val parameterAnnotations = method.parameterAnnotations
        for (i in parameterAnnotations.indices) {
            for (parameterAnnotation in parameterAnnotations[i]) {
                if (parameterAnnotation is Field) {
                    val key = parameterAnnotation.value
                    val value = args[i].toString()
                    if (!url.contains("?")) {
                        url += "?$key=$value"
                    } else {
                        url += "&$key=$value"
                    }
                }
            }
        }
        val request = Request.Builder()
            .url(url)
            .build()
        val response = okHttpClient.newCall(request).execute()

        val genericReturnType = method.genericReturnType
        val body = response.body
        val json = body?.string()
        println(json)
        val result = gson.fromJson<Any?>(json, genericReturnType)

        return result
    }
}

fun main() {
    val api: ApiService = KHttpV1.create(ApiService::class.java)
    val data: RepoList = api.repos(lang = "Kotlin", since = "weekly")
    println(data)
}

class ApiImpl(val h: InvocationHandler) : Proxy(h), ApiService {
    override fun repos(lang: String, since: String): RepoList {
        val method: Method = ::repos.javaMethod!!
        val args = arrayOf(lang, since)
        return h.invoke(this, method, args) as RepoList
    }

}
    

其中RepoList、Repo是接受服务器返回内容的数据类;ApiService是用于网络请求的接口;

GET 注解,代表了这个网络请求应该是 GET 请求,这是HTTP请求的一种方式。GET 注解当中的“/repo”,代表了 API 的 path,它是和 baseURL 拼接的;

Field 注解,代表了 GET 请求的参数。Field 注解当中的值也会和 URL 拼接在一起。

看看KtHttp 是如何使用的:

fun main() {
    // ①
    val api: ApiService = KtHttpV1.create(ApiService::class.java)

    // ②
    val data: RepoList = api.repos(lang = "Kotlin", since = "weekly")

    println(data)
}

注释①:我们调用 KtHttpV1.create() 方法,传入了 ApiService::class.java,参数的类型是Class,返回值类型是 ApiService。这就相当于创建了 ApiService 这个接口的实现类的对象。

注释②:我们调用 api.repos() 这个方法,传入了 Kotlin、weekly 这两个参数,代表我们想查询最近一周最热门的 Kotlin 开源项目。

那KtHttpV1.create() 是如何创建 ApiService 的实例的呢?要知道 ApiService 可是一个接口,我们要创建它的对象,必须要先定义一个类实现它的接口方法,然后再用这个类来创建对象才行。不过在这里,我们不会使用这种传统的方式,而是会用动态代理,也就是 JDK 的Proxy。Proxy 的底层,其实也用到了反射。暂不去深究 Proxy 的底层原理。在这里只需要知道,我们通过 Proxy,就可以动态地创建 ApiService 接口的实例化对象。

fun <T> create(service: Class<T>): T {

    // 调用 Proxy.newProxyInstance 就可以创建接口的实例化对象
    return Proxy.newProxyInstance(
        service.classLoader,
        arrayOf<Class<*>>(service),
        object : InvocationHandler{
            override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
                // 省略
            }
        }
    ) as T
}    

在 create() 方法当中,直接返回了 Proxy.newProxyInstance() 这个方法的返回值,最后再将其转换成了 T 类型。newProxyInstance() 方法定义如下:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h){ 
        ...
}

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

最后一个参数,InvocationHandler 符合 SAM 转换要求,所以create() 方法可以进一步简化成这样

fun <T> create(service: Class<T>): T {

    return Proxy.newProxyInstance(
        service.classLoader,
        arrayOf<Class<*>>(service)
    ) { proxy, method, args ->
        // 待完成
    } as T
}

到这里,程序的基本框架搭建好了。

程序的主要逻辑还没实现,所以接下来,看看上面那个“待完成”的 InvocationHandler,这个 Lambda 表达式应该怎么写。这个换句话说,也就是 Proxy.newProxyInstance(),会帮我们创建 ApiService 的实例对象,而 ApiService 当中的接口方法的具体逻辑,需要在 Lambda 表达式当中实现。

回头看看 ApiService 当中的代码细节:

interface ApiService {
// 假设我们的baseurl是:https://baseurl.com
// 这里拼接结果会是这样:https://baseurl.com/repo
//          ↓
    @GET("/repo")
    fun repos(
    //                Field注解当中的lang,最终会拼接到url当中去
    //            ↓                                                 ↓
        @Field("lang") lang: String,  // https://baseurl.com/repo?lang=Kotlin
        @Field("since") since: String // https://baseurl.com/repo?lang=Kotlin&since=weekly
    ): RepoList
}

从代码注释中可以看出来,其实真正需要实现的逻辑,就是想办法把注解当中的值 /repo、lang、since 取出来,然后拼接到 URL 当中去。接下来需要通过反射实现:

object KtHttpV1 {

    // 底层使用 OkHttp
    private var okHttpClient: OkHttpClient = OkHttpClient()
    // 使用 Gson 解析 JSON
    private var gson: Gson = Gson()

    // 这里以baseurl.com为例,实际上我们的KtHttpV1可以请求任意API
    var baseUrl = "https://baseurl.com"

    fun <T> create(service: Class<T>): T {
        return Proxy.newProxyInstance(
            service.classLoader,
            arrayOf<Class<*>>(service)
        //           ①     ②
        //           ↓      ↓
        ) { proxy, method, args ->
            // ③
            val annotations = method.annotations
            for (annotation in annotations) {
                // ④
                if (annotation is GET) {
                    // ⑤
                    val url = baseUrl + annotation.value
                    // ⑥
                    return@newProxyInstance invoke(url, method, args!!)
                }
            }
            return@newProxyInstance null

        } as T
    }

    private fun invoke(url: String, method: Method, args: Array<Any>): Any? {
        // 待完成
    }
}

注释①:method 的类型是反射后的 Method,在我们这个例子当中,它最终会代表被调用的方法,也就是 ApiService 接口里面的 repos() 这个方法。

注释②:args 的类型是对象的数组,在我们的例子当中,它最终会代表方法的参数的值,也就是“api.repos("Kotlin", "weekly")”当中的"Kotlin"和"weekly"。

注释③:method.annotations,代表了我们会取出 repos() 这个方法上面的所有注解,由于 repos() 这个方法上面可能会有多个注解,因此它是数组类型。

注释④:我们使用 for 循环,遍历所有的注解,找到 GET 注解。

注释⑤:我们找到 GET 注解以后,要取出 @GET(“/repo”) 当中的"/repo",也就是“annotation.value”。这时候我们只需要用它与 baseURL 进行拼接,就可以得到完整的 URL;

注释⑥:return@newProxyInstance,用的是 Lambda 表达式当中的返回语法,在得到完整的 URL 以后,我们将剩下的逻辑都交给了 invoke() 这个方法。

接着实现 invoke() 当中的“待完成代码”,思路步骤如下:

private fun invoke(url: String, method: Method, args: Array<Any>): Any? {
    // ① 根据url拼接参数,也就是:url + ?lang=Kotlin&since=weekly
    // ② 使用okHttpClient进行网络请求
    // ③ 使用gson进行JSON解析
    // ④ 返回结果
}

主要看1、2如何实现:

private fun invoke(url: String, method: Method, args: Array<Any>): Any? {
    // ① 根据url拼接参数,也就是:url + ?lang=Kotlin&since=weekly

    // 使用okHttpClient进行网络请求
    val request = Request.Builder()
            .url(url)
            .build()
    val response = okHttpClient.newCall(request).execute()

    // ② 获取repos()的返回值类型 genericReturnType

    // 使用gson进行JSON解析
    val body = response.body
    val json = body?.string()
    //                              根据repos()的返回值类型解析JSON
    //                                            ↓
    val result = gson.fromJson<Any?>(json, genericReturnType)

    // 返回结果
    return result
}

经过分解,现在的问题变成了下面这样:

注释①,利用反射,解析出“api.repos("Kotlin", "weekly")”这个方法当中的"Kotlin"和"weekly",将其与 URL 进行拼接得到:url + ?lang=Kotlin&since=weekly

注释②,利用反射,解析出 repos() 的返回值类型,用于 JSON 解析。

invoke()最终的代码:

private fun invoke(path: String, method: Method, args: Array<Any>): Any? {
    // 条件判断
    if (method.parameterAnnotations.size != args.size) return null

    // 解析完整的url
    var url = path
    // ①
    val parameterAnnotations = method.parameterAnnotations
    for (i in parameterAnnotations.indices) {
        for (parameterAnnotation in parameterAnnotations[i]) {
            // ②
            if (parameterAnnotation is Field) {
                val key = parameterAnnotation.value
                val value = args[i].toString()
                if (!url.contains("?")) {
                    // ③
                    url += "?$key=$value"
                } else {
                    // ④
                    url += "&$key=$value"
                }

            }
        }
    }
    // 最终的url会是这样:
    // https://baseurl.com/repo?lang=Kotlin&since=weekly

    // 执行网络请求
    val request = Request.Builder()
        .url(url)
        .build()
    val response = okHttpClient.newCall(request).execute()

    // ⑤
    val genericReturnType = method.genericReturnType
    val body = response.body
    val json = body?.string()
    // JSON解析
    val result = gson.fromJson<Any?>(json, genericReturnType)

    // 返回值
    return result
}

注释①,method.parameterAnnotations,它的作用是取出方法参数当中的所有注解,在我们这个案例当中,repos() 这个方法当中涉及到两个注解,它们分别是@Field("lang")、@Field("since")。 使用 动态代理 + 注解 + 反射 实现这个网络请求框架的原因了。通过这样的方式,我们就不必在代码当中去实现每一个接口,而是只要是符合这样的代码模式,任意的接口和方法,我们都可以直接传进去。 注释②,由于方法当中可能存在其他注解,因此要筛选出我们想要的 Field 注解。

注释③,这里是取出注解当中的值“lang”,以及参数当中对应的值“Kotlin”进行拼接,URL 第一次拼接参数的时候,要用“?”分隔。

注释④,这里是取出注解当中的值“since”,以及参数当中对应的值“weekly”进行拼接,后面的参数拼接格式,是用“&”分隔。

注释⑤,method.genericReturnType 取出 repos() 的返回值类型,也就是 RepoList,最终,我们用它来解析JSON。

动态代理的模式,由于大量应用了反射,加之代码当中还牵涉到了泛型和注解,导致这个案例的代码不是那么容易理解。不过可以利用调试的手段,去查看代码当中每一步执行的结果,这样就能对注解、反射、动态代理有一个更具体的认识。

使用 动态代理 + 注解 + 反射 实现这个网络请求框架的原因:

通过这样的方式,我们就不必在代码当中去实现每一个接口,而是只要是符合这样的代码模式,任意的接口和方法,我们都可以直接传进去。只要我们定义的 Service 接口拥有对应的注解 GET、Field,我们就可以通过注解与反射,将这些信息拼凑在一起。灵活性非常好。

KtHttp,就是将 URL 的信息存储在了注解当中(比如 lang 和 since),而实际的参数值,是在函数调用的时候传进来的(比如 Kotlin 和 weekly)。我们通过泛型、注解、反射的结合,将这些信息集到一起,完成整个 URL 的拼接,最后才通过 OkHttp 完成的网络请求、Gson 完成的解析。

2.0:函数式思维

先过一遍重构后的完整代码:

object KtHttpV2 {

    private val okHttpClient by lazy { OkHttpClient() }
    private val gson by lazy { Gson() }
    var baseUrl = "https://baseurl.com" // 可改成任意url

    inline fun <reified T> create(): T {
        return Proxy.newProxyInstance(
            T::class.java.classLoader,
            arrayOf(T::class.java)
        ) { proxy, method, args ->

            return@newProxyInstance method.annotations
                .filterIsInstance<GET>()
                .takeIf { it.size == 1 }
                ?.let { invoke("$baseUrl${it[0].value}", method, args) }
        } as T
    }

    fun invoke(url: String, method: Method, args: Array<Any>): Any? =
        method.parameterAnnotations
            .takeIf { method.parameterAnnotations.size == args.size }
            ?.mapIndexed { index, it -> Pair(it, args[index]) }
            ?.fold(url, ::parseUrl)
            ?.let { Request.Builder().url(it).build() }
            ?.let { okHttpClient.newCall(it).execute().body?.string() }
            ?.let { gson.fromJson(it, method.genericReturnType) }


    private fun parseUrl(acc: String, pair: Pair<Array<Annotation>, Any>) =
        pair.first.filterIsInstance<Field>()
            .first()
            .let { field ->
                if (acc.contains("?")) {
                    "$acc&${field.value}=${pair.second}"
                } else {
                    "$acc?${field.value}=${pair.second}"
                }
            }
}

解析下 KtHttp 2.0 的重构思路:

首先让成员变量支持懒加载:

重构前:

object KtHttpV1 {
    private var okHttpClient: OkHttpClient = OkHttpClient()
    private var gson: Gson = Gson()

    fun <T> create(service: Class<T>): T {}
    fun invoke(url: String, method: Method, args: Array<Any>): Any? {}
}

okHttpClient、gson 这两个成员是不支持懒加载的,因此我们首先应该让它们支持懒加载。

重构后:

object KtHttpV2 {
    private val okHttpClient by lazy { OkHttpClient() }
    private val gson by lazy { Gson() }

    fun <T> create(service: Class<T>): T {}
    fun invoke(url: String, method: Method, args: Array<Any>): Any? {}
}

使用了 by lazy 委托的方式,它简洁的语法可以让我们快速实现懒加载。

接下来看看 create() 这个方法的定义:

//                      注意这里
//                         ↓
fun <T> create(service: Class<T>): T {
    return Proxy.newProxyInstance(
        service.classLoader,
        arrayOf<Class<*>>(service)
    ) { proxy, method, args ->
    }
}

在上面的代码中,create() 会接收一个Class类型的参数。针对这样的情况,完全可以省略掉这个参数。具体做法,是使用inline来实现类型实化(Reified Type)。我们常说,Java 的泛型是伪泛型,而这里我们要实现的就是真泛型。

//  注意这两个关键字
//  ↓          ↓
inline fun <reified T> create(): T {
    return Proxy.newProxyInstance(
        T::class.java.classLoader, // ① 变化在这里
        arrayOf(T::class.java) // ② 变化在这里
    ) { proxy, method, args ->
        // 待重构
    }
}

正常情况下,泛型参数类型会被擦除,这就是 Java 的泛型被称为“伪泛型”的原因。而通过使用 inline 和 reified 这两个关键字,我们就能实现类型实化,也就是“真泛型”,进一步,我们就可以在代码注释①、②的地方,使用“T::class.java”来得到 Class 对象。

接下来看看 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
            .filterIsInstance<GET>()
            .takeIf { it.size == 1 }
            ?.let { invoke("$baseUrl${it[0].value}", method, args) }
    } as T
}

首先,通过 method.annotations,来获取 method 的所有注解;

接着,我们用filterIsInstance(),来筛选出我们想要找的 GET 注解。这里的 filterIsInstance 其实是 filter 的升级版,也就是过滤的意思;

之后,我们判断 GET 注解的数量,它的数量必须是 1,其他的都不行,这里的 takeIf 其实相当于我们的 if;

最后,我们通过拼接出 URL,然后将程序执行流程交给 invoke() 方法。这里的"?.let{}"相当于判空。

接下来看看 invoke() 方法的重构:

fun invoke(url: String, method: Method, args: Array<Any>): Any? =
    method.parameterAnnotations
        .takeIf { method.parameterAnnotations.size == args.size }
        ?.mapIndexed { index, it -> Pair(it, args[index]) }
        ?.fold(url, ::parseUrl)
        ?.let { Request.Builder().url(it).build() }
        ?.let { okHttpClient.newCall(it).execute().body?.string() }
        ?.let { gson.fromJson(it, method.genericReturnType) }

第一步,我们通过 method.parameterAnnotations,获取方法当中所有的参数注解,在这里也就是@Field("lang")、@Field("since")。

第二步,我们通过 takeIf 来判断,参数注解数组的数量与参数的数量相等,也就是说@Field("lang")、@Field("since")的数量是 2,那么["Kotlin", "weekly"]的 size 也应该是 2,它必须是一一对应的关系。

第三步,我们将@Field("lang")与"Kotlin"进行配对,将@Field("since")与"weekly"进行配对。这里的 mapIndexed,其实就是 map 的升级版,它本质还是一种映射的语法,“注解数组类型”映射成了“Pair 数组”,只是多了一个 index 而已。

第四步,我们使用 fold 与 parseUrl() 这个方法,拼接出完整的 URL,也就是:baseurl.com/repo?lang=K… 这里我们使用了函数引用的语法“::parseUrl”。而 fold 这个操作符,其实就是高阶函数版的 for 循环。

第五步,我们构建出 OkHttp 的 Request 对象,并且将 URL 传入了进去,准备做网络请求。

第六步,我们通过 okHttpClient 发起了网络请求,并且拿到了 String 类型的 JSON 数据。最后,我们通过 Gson 解析出 JSON 的内容,并且返回 RepoList 对象。

再看看用于实现 URL 拼接的 parseUrl() 是如何实现的:

private fun parseUrl(acc: String, pair: Pair<Array<Annotation>, Any>) =
    pair.first.filterIsInstance<Field>()
        .first()
        .let { field ->
            if (acc.contains("?")) {
                "$acc&${field.value}=${pair.second}"
            } else {
                "$acc?${field.value}=${pair.second}"
            }
        }

这里只是把从前的 for 循环代码,换成了 Kotlin 的集合操作符而已。大致流程如下:

首先,我们从注解的数组里筛选出 Field 类型的注解;

接着,通过 first() 取出第一个 Field 注解,这里它也应该是唯一的;

最后,我们判断当前的 acc 是否已经拼接过参数,如果没有拼接过,就用“?”分隔,如果已经拼接过参数,我们就用“&”分隔。

至此,2.0的代码完成解析。

对比看下2.0和1.0完整代码区别

2.0

object KtHttpV2 {

    private val okHttpClient by lazy { OkHttpClient() }
    private val gson by lazy { Gson() }
    var baseUrl = "https://baseurl.com" // 可改成任意url

    inline fun <reified T> create(): T {
        return Proxy.newProxyInstance(
            T::class.java.classLoader,
            arrayOf(T::class.java)
        ) { proxy, method, args ->

            return@newProxyInstance method.annotations
                .filterIsInstance<GET>()
                .takeIf { it.size == 1 }
                ?.let { invoke("$baseUrl${it[0].value}", method, args) }
        } as T
    }

    fun invoke(url: String, method: Method, args: Array<Any>): Any? =
        method.parameterAnnotations
            .takeIf { method.parameterAnnotations.size == args.size }
            ?.mapIndexed { index, it -> Pair(it, args[index]) }
            ?.fold(url, ::parseUrl)
            ?.let { Request.Builder().url(it).build() }
            ?.let { okHttpClient.newCall(it).execute().body?.string() }
            ?.let { gson.fromJson(it, method.genericReturnType) }


    private fun parseUrl(acc: String, pair: Pair<Array<Annotation>, Any>) =
        pair.first.filterIsInstance<Field>()
            .first()
            .let { field ->
                if (acc.contains("?")) {
                    "$acc&${field.value}=${pair.second}"
                } else {
                    "$acc?${field.value}=${pair.second}"
                }
            }
}

1.0

object KtHttpV1 {

    private var okHttpClient: OkHttpClient = OkHttpClient()
    private var gson: Gson = Gson()
    var baseUrl = "https://baseurl.com" // 可改成任意url

    fun <T> create(service: Class<T>): T {
        return Proxy.newProxyInstance(
            service.classLoader,
            arrayOf<Class<*>>(service)
        ) { proxy, method, args ->
            val annotations = method.annotations
            for (annotation in annotations) {
                if (annotation is GET) {
                    val url = baseUrl + annotation.value
                    return@newProxyInstance invoke(url, method, args!!)
                }
            }
            return@newProxyInstance null

        } as T
    }

    private fun invoke(path: String, method: Method, args: Array<Any>): Any? {
        if (method.parameterAnnotations.size != args.size) return null

        var url = path
        val parameterAnnotations = method.parameterAnnotations
        for (i in parameterAnnotations.indices) {
            for (parameterAnnotation in parameterAnnotations[i]) {
                if (parameterAnnotation is Field) {
                    val key = parameterAnnotation.value
                    val value = args[i].toString()
                    if (!url.contains("?")) {
                        url += "?$key=$value"
                    } else {
                        url += "&$key=$value"
                    }

                }
            }
        }

        val request = Request.Builder()
            .url(url)
            .build()

        val response = okHttpClient.newCall(request).execute()

        val genericReturnType = method.genericReturnType
        val body = response.body
        val json = body?.string()
        val result = gson.fromJson<Any?>(json, genericReturnType)

        return result
    }
}

小结:

在 1.0 版本的代码中,灵活利用了动态代理、泛型、注解、反射这几个技术,实现了 KtHttp 的基础功能。动态代理,由于它的底层原理比较复杂,我们通过 ApiImpl 这个类,来模拟了它动态生成的 Proxy 类。用这种直观的方式来帮助理解它存在的意义。泛型方面,我们将其用在了动态代理的 create() 方法上,后面我们还使用了“类型实化”的技术,也就是 inline + reified 关键字。注解方面,我们首先自定义了两个注解,分别是 GET、Field。其中,@GET 用于标记接口的方法,它的值是 URL 的 path;@Field 用于标记参数,它的值是参数的 key。反射方面,这个技术点,几乎是贯穿于整个代码实现流程的。我们通过反射的自省能力,去分析 repos() 方法,从 GET 注解当中取出了“/repo”这个 path,从注解 Field 当中取出了 lang、since,还取出了 repos() 方法的返回值 RepoList,用于 JSON 数据的解析。在 2.0 版本的代码中,我们几乎删除了之前所有的代码,以函数式的思维重写了 KtHttp 的内部逻辑。在这个版本当中,我们大量地使用了 Kotlin 标准库里的高阶函数,进一步提升了代码的可读性。

Kotlin 的编程范式问题。命令式还是函数式,这完全取决于我们开发者自身。 image.png