gitbook/朱涛 · Kotlin编程第一课/docs/481787.md
2022-09-03 22:05:03 +08:00

652 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 12 | 实战用Kotlin实现一个网络请求框架KtHttp
你好,我是朱涛,又到了实战环节。
在前面几节课当中我们一起学习了Kotlin的委托、泛型、注解、反射这几个高级特性。那么今天这节课我们将会运用这些特性来写一个**Kotlin版本的HTTP网络请求框架**。由于它是纯Kotlin开发的我们就把它叫做是KtHttp吧。
事实上在Java和Kotlin领域有许多出色的网络请求框架比如 [OkHttp](https://github.com/square/okhttp)、[Retrofit](https://github.com/square/Retrofit)、[Fuel](https://github.com/kittinunf/fue)。而我们今天要实现的KtHttp它的灵感来自于Retrofit。之所以选择Retrofit作为借鉴的对象是因为它的底层使用了大量的**泛型、注解和反射**的技术。如果你能跟着我一起用泛型、注解、反射来实现一个简单的网络请求框架,相信你对这几个知识点的认识也会更加透彻。
在这节课当中我会带你从0开始实现这个网络请求框架。和往常一样为了方便你理解我们的代码会分为两个版本
* 1.0 版本我们会用Java思维以最简单直白的方式来实现KtHttp的基础功能——同步式的GET网络请求
* 2.0 版本,我们会用函数式思维来重构代码。
另外在正式开始学习之前我也建议你去clone我GitHub上面的KtHttp工程[https://github.com/chaxiu/KtHttp.git](https://github.com/chaxiu/KtHttp.git)然后用IntelliJ打开并切换到**start**分支跟着课程一步步敲代码。
## 1.0Java思维
在正式开始之前,我们还是先来看看程序的运行效果:
![图片](https://static001.geekbang.org/resource/image/32/bc/321d151db077766997ed8d8b911f1fbc.gif?wh=1294x862)
在上面的动图中我们通过KtHttp请求了一个服务器的API然后在控制台输出了结果。这其实是我们在开发工作当中十分常见的需求。通过这个KtHttp我们就可以在程序当中访问任何服务器的API比如[GitHub的API](https://docs.github.com/en)。
那么,为了描述服务器返回的内容,我们定义了两个数据类:
```plain
// 这种写法是有问题的,但这节课我们先不管。
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?
)
```
除了数据类以外,我们还要定义一个用于网络请求的接口:
```plain
interface ApiService {
@GET("/repo")
fun repos(
@Field("lang") lang: String,
@Field("since") since: String
): RepoList
}
```
在这个接口当中,有两个注解,我们一个个分析:
* **GET注解**代表了这个网络请求应该是GET请求这是[HTTP](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)请求的一种方式。GET注解当中的“/repo”代表了API的path它是和baseURL拼接的
* **Field注解**代表了GET请求的参数。Field注解当中的值也会和URL拼接在一起。
也许你会好奇,**GET、Field这两个注解是从哪里来的呢**这其实也是需要我们自己定义的。根据上节课学过的内容,我们很容易就能写出下面的代码:
```plain
@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这意味着这两个注解都是运行时可访问的。而这也正好是我们后面要使用的反射的前提。
最后我们再来看看KtHttp是如何使用的
```plain
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<T>`返回值类型是ApiService。这就相当于创建了ApiService这个接口的实现类的对象。
* 注释②我们调用api.repos()这个方法传入了Kotlin、weekly这两个参数代表我们想查询最近一周最热门的Kotlin开源项目。
看到这里,你也许会好奇,**KtHttpV1.create()是如何创建ApiService的实例的呢**要知道ApiService可是一个接口我们要创建它的对象必须要先定义一个类实现它的接口方法然后再用这个类来创建对象才行。
不过在这里,我们不会使用这种传统的方式,而是会用**动态代理**也就是JDK的[Proxy](https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html)。Proxy的底层其实也用到了反射。
不过,由于这个案例涉及到的知识点都很抽象,在正式开始编写逻辑代码之前,我们先来看看下面这个动图,对整体的程序有一个粗略的认识。
![](https://static001.geekbang.org/resource/image/cf/95/cf441d3df1bb4b49432319e160cd3c95.gif?wh=1080x608)
现在,相信你大概就知道这个程序是如何实现的了。下面,我再带你来看看具体的代码是怎么写的。
这里我要先说明一点为了不偏离这次实战课的主题我们不会去深究Proxy的底层原理。在这里**你只需要知道我们通过Proxy就可以动态地创建ApiService接口的实例化对象**。具体的做法如下:
```plain
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()这个方法又是如何定义的呢?
```java
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()方法可以进一步简化成这样:
```plain
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当中的代码细节
```plain
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当中去。那么我们如何才能得到注解当中的值呢
答案自然就是我们在上节课学过的:**反射**。
```plain
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? {
// 待完成
}
}
```
在上面的代码中一共有6个注释我们一个个看。
* 注释①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()当中的“待完成代码”应该怎么写。
```plain
private fun invoke(url: String, method: Method, args: Array<Any>): Any? {
// ① 根据url拼接参数也就是url + ?lang=Kotlin&since=weekly
// ② 使用okHttpClient进行网络请求
// ③ 使用gson进行JSON解析
// ④ 返回结果
}
```
在上面的代码中我们的invoke()方法一共分成了四个步骤,其中的③、④两个步骤其实很容易实现:
```plain
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解析。
我们来看看最终的代码:
```plain
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。
说实话,动态代理的这种模式,由于它大量应用了反射,加之我们的代码当中还牵涉到了泛型和注解,导致这个案例的代码不是那么容易理解。不过,我们其实可以利用**调试**的手段,去查看代码当中每一步执行的结果,这样就能对注解、反射、动态代理有一个更具体的认识。
前面带你看过的这个动图,其实就是在向你展示代码在调试过程中的关键节点,我们可以再来回顾一下整个代码的执行流程:
![](https://static001.geekbang.org/resource/image/cf/95/cf441d3df1bb4b49432319e160cd3c95.gif?wh=1080x608)
相信现在,你已经能够体会我们使用 **动态代理+注解+反射** 实现这个网络请求框架的原因了。通过这样的方式我们就不必在代码当中去实现每一个接口而是只要是符合这样的代码模式任意的接口和方法我们都可以直接传进去。在这个例子当中我们用的是ApiService这个接口如果下次我们定义了另一个接口比如说
```plain
interface GitHubService {
@GET("/search")
fun search(
@Field("id") id: String
): User
}
```
这时候我们的KtHttp根本不需要做任何的改动直接这样调用即可
```plain
fun main() {
KtHttpV1.baseUrl = "https://api.github.com"
// 换一个接口名即可 换一个接口名即可
// ↓ ↓
val api: GitHubService = KtHttpV1.create(GitHubService::class.java)
val data: User = api.search(id = "JetBrains")
}
```
可以发现,使用动态代理实现网络请求的优势,它的**灵活性**是非常好的。只要我们定义的Service接口拥有对应的注解GET、Field我们就可以通过注解与反射将这些信息拼凑在一起。下面这个动图就展示了它们整体的流程
![图片](https://static001.geekbang.org/resource/image/8b/31/8b5997dc2f036020dc16a1a5efb8c531.gif?wh=1080x608)
实际上我们的KtHttp就是将URL的信息存储在了注解当中比如lang和since而实际的参数值是在函数调用的时候传进来的比如Kotlin和weekly。我们通过泛型、注解、反射的结合将这些信息集到一起完成整个URL的拼接最后才通过OkHttp完成的网络请求、Gson完成的解析。
到这里我们1.0版本的开发就算是完成了。这里的单元测试代码很容易写,我就不贴出来了,**单元测试是个好习惯,我们不能忘**。
接下来我们正式进入2.0版本的开发。
## 2.0:函数式思维
其实如果你理解了1.0版本的代码2.0版本的程序也就不难实现了。因为这个程序的主要功能都已经完成了,现在要做的只是:**换一种思路重构代码**。
我们先来看看KtHttpV1这个单例的成员变量
```plain
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这两个成员是不支持懒加载的因此我们首先应该让它们**支持懒加载**。
```plain
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()这个方法的定义:
```plain
// 注意这里
// ↓
fun <T> create(service: Class<T>): T {
return Proxy.newProxyInstance(
service.classLoader,
arrayOf<Class<*>>(service)
) { proxy, method, args ->
}
}
```
在上面的代码中create()会接收一个`Class<T>`类型的参数。其实,针对这样的情况,我们完全可以省略掉这个参数。具体做法,是使用我们前面学过的[inline](https://time.geekbang.org/column/article/477295),来实现**类型实化**Reified Type。我们常说Java的泛型是伪泛型而这里我们要实现的就是真泛型。
```plain
// 注意这两个关键字
// ↓ ↓
inline fun <reified T> create(): T {
return Proxy.newProxyInstance(
T::class.java.classLoader, // ① 变化在这里
arrayOf(T::class.java) // ② 变化在这里
) { proxy, method, args ->
// 待重构
}
}
```
正常情况下,泛型参数[类型会被擦除](https://zh.wikipedia.org/wiki/%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4)这就是Java的泛型被称为“伪泛型”的原因。而通过使用**inline和reified**这两个关键字我们就能实现类型实化也就是“真泛型”进一步我们就可以在代码注释①、②的地方使用“T::class.java”来得到Class对象。
下面我们来看看KtHttp的主要逻辑该如何重构。
为了方便理解我们会使用Kotlin标准库当中已有的高阶函数尽量不去涉及函数式编程里的高级概念。**在这里我强烈建议你打开IDE一边敲代码一边阅读**这样一来当你遇到不熟悉的标准函数时就可以随时去看它的实现源码了。相信在学习过第7讲的[高阶函数](https://time.geekbang.org/column/article/476637)以后,这些库函数都不会难倒你。
首先我们来看看create()里面“待重构”的代码该如何写。在这个方法当中我们需要读取method当中的GET注解解析出它的值然后与baseURL拼接。这里我们完全可以**借助Kotlin的标准库函数**来实现:
```plain
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>()`来筛选出我们想要找的GET注解。这里的filterIsInstance其实是filter的升级版也就是**过滤**的意思;
* 之后我们判断GET注解的数量它的数量必须是1其他的都不行这里的takeIf其实相当于我们的if
* 最后我们通过拼接出URL然后将程序执行流程交给invoke()方法。这里的"?.let{}"相当于判空。
好了create()方法的重构已经完成接下来我们来看看invoke()方法该如何重构。
```plain
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也就是[https://baseurl.com/repo?lang=Kotlin&since=weekly](https://baseurl.com/repo?lang=Kotlin&since=weekly)。 这里我们使用了**函数引用**的语法“::parseUrl”。而fold这个操作符其实就是高阶函数版的for循环。
* 第五步我们构建出OkHttp的Request对象并且将URL传入了进去准备做网络请求。
* 第六步我们通过okHttpClient发起了网络请求并且拿到了String类型的JSON数据。
* 最后我们通过Gson解析出JSON的内容并且返回RepoList对象。
到目前为止我们的invoke()方法的主要流程就分析完了接下来我们再来看看用于实现URL拼接的parseUrl()是如何实现的。
```plain
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版本的代码就完成了,完整的代码如下:
```plain
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版本的完整代码:
```plain
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版本、2.0版本,它们之间可以说是天壤之别。
## 小结
好了,这节实战就到这里。接下来我们来简单总结一下:
* 在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标准库里的高阶函数进一步提升了代码的可读性。
在前面的[加餐](https://time.geekbang.org/column/article/478106)课程当中我们也讨论过Kotlin的编程范式问题。**命令式还是函数式,这完全取决于我们开发者自身。**
相比起前面实战课中的[单词频率统计程序](https://time.geekbang.org/column/article/477295)这一次我们的函数式范式的代码实现起来就没有那么得流畅了。原因其实也很简单Kotlin提供了强大的集合操作符这就让Kotlin十分擅长“集合操作”的场景因此词频统计程序我们不到10行代码就解决了。而对于注解、反射相关的场景函数式的编程范式就没那么擅长了。
在这节课里我之所以费尽心思地用函数式风格重构出KtHttp 2.0版本,主要还是想让你看到函数式编程在它不那么擅长的领域表现会如何。毕竟,我们在工作中什么问题都可能会遇到。
## 思考题
好了,学完这节课以后,请问你有哪些感悟和收获?请在评论区里分享出来,我们一起交流吧!