各Scala HTTP client库比较[译]

这两天打算把Java写的HTTP请求代码改写成Scala,作为Scala新手自然又要谷歌一番,发现一篇不错HTTP client库对比的文章,翻译了下供大伙儿参考:)

今天我们要看一下Scala HTTP请求的库,横向做个对比。

这次比较与性能或者实现无关。我们只讨论长期存在和流行的几个库(假设日常使用都没什么问题),我的关注点在文档质量、API易用程度,以及代码是否优美。

Scala初学者通过谷歌搜索”scala http client”会出现以下排名:

1.Dispatch
2.Newman
3.scalaj-http
4.spray-client
5.Play! WS API

在它们的文档中大多只提供了一行简单的Get请求。这是远远不够的,实际使用你所碰到的情况要被文档中复杂的多。

所以我们打算用稍微复杂点的请求来看看这些库是否合适。 下面是我们的需求:

  • GET 请求地址:http://jsonplaceholder.typicode.com/comments/1
  • 添加两个查询变量? some_parameter=some_value&some_other_parameter=some_other_value
  • 添加HTTP header。 我随机选了 Cache-Control : no-cache
  • 异常处理,如果API没响应或者状态为200到299
  • 打印获取的内容和一个HTTP header (我随机选了 Content-Length)

没有什么花哨的,应该很容易。

Dispatch

Dispatch 看着不错,谷歌返回结果里面排第一,文档http://dispatch.databinder.net/看着也不错。真的是这样么? 让我们来看一下。

libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.11.3"

代码

import dispatch._
import dispatch.Defaults._

// Instantiation of the client
// In a real-life application, you would instantiate it once, share it everywhere,
// and call h.shutdown() when you're done
val h = new Http
val requestWithHandler =
// Defining the request
url("http://jsonplaceholder.typicode.com/comments/1")
    .<<?(Seq("some_parameter" -> "some_value", "some_other_parameter" -> "some_other_value"))
    .<:<(Seq("Cache-Control" -> "no-cache"))
    // Requires a 2xx status code
    .OK { response =>
    // Defines a handler
    println(s"OK, received ${response.getResponseBody}")
    println(s"The response header Content-Length was ${response.getHeader("Content-Length")}")
}
// Executes it
h(requestWithHandler)

操作是没什么问题,如果你喜欢上面代码各种API里面定义的符号方法/, /?, <<, <<<, as_!的话。另外Dispatch 移到了另外一个repo,已经很久没维护了。还有奇怪的复数方法名,比如setQueryParameters。个人不想用它,文档还少。

结论:过时并且‘模糊’的DSL,不推荐使用。

Newman

从Github的文档里面https://github.com/stackmob/newman可以知道Newman采用的Dispatch的DSL,不过大多数符号都被英语代替了。应该不错,对吧?但是这个项目自从2014年起就没有任何更新了,而且不支持Scala 2.11

有人fork了一个版本使之支持Scala 2.11,但是没有更多信息,看着并不会持续维护,合并原作者版本或者直接替换掉。

结论:过时,不推荐使用。

scalaj-http

Scala-http 看着不错,而且还在维护。但它是synchronous。每个HTTP请求会阻塞进程。

在Scala世界中习惯于异步处理,同步的话会很奇怪。虽然看起来不错但是我不会用他的。

结论:synchronous。这些东西依然存在表示挺高兴的,不过你程序不用这个模式,那就完全无效了。

spray-client

Spray 是一个大家伙,他是一个HTTP框架包含了许多模块,其中一个是spray-client

libraryDependencies += "io.spray" %% "spray-client" % "1.3.1"

代码

import akka.actor._
import spray.http._
import spray.client.pipelining._

// Start an Akka Actor System
// In a real-life webapp, you would use only one, share it everywhere,
// and call actorSystem.shutdown() when you're done
implicit val actorSystem = ActorSystem()
import actorSystem.dispatcher

val pipeline = sendReceive
pipeline(
// Building the request
Get(
    Uri(
    "http://jsonplaceholder.typicode.com/comments/1"
    ).withQuery("some_parameter" -> "some_value", "some_other_parameter" -> "some_other_value")
)
    .withHeaders(HttpHeaders.`Cache-Control`(CacheDirectives.`no-cache`))
)
.map { response =>
    // Treating the response
    if (response.status.isFailure) {
    sys.error(s"Received unexpected status ${response.status} : ${response.entity.asString(HttpCharsets.`UTF-8`)}")
    }
    println(s"OK, received ${response.entity.asString(HttpCharsets.`UTF-8`)}")
    println(s"The response header Content-Length was ${response.header[HttpHeaders.`Content-Length`]}")
}

关于spary-client我们有许多可以讲述的地方。

首先:跟所有Spray的模块一样,spray-client太依赖Akka。如果你的app没使用Akka那你就需要一个Actor系统,这样使用就不优美了。如果你弄了个actor系统就为了HTTP请求,这样未免不太划算。

其次API比较含糊。 请看第一行代码

val pipeline = sendReceive

你可能觉得sendReceive是一个赋给pipeline的变量。实际上pipeline的类型是SendReceive而sendReceive是SendReceive里面的一个方法。最好加个括号区分清楚。(译者注:对于改值器方法(即改变对象状态的方法)使用(),而对于取值器(不会改变对象状态的方法)去掉()是不错的风格。)

val pipeline = sendReceive()

不过你没法这样做,因为方法有隐式参数(actor system 和 execution context)。添加括号会要求明确这些参数。

OK,先不管这个,那么SendReceive这个类型到底是什么? 实际上他是一个函数类型的类型别名:

type SendReceive = HttpRequest ⇒ Future[HttpResponse]

所以sendReceive 实际上是生成了另外一个函数。感觉迷糊了? 淡定,事实就是这样的。 不过我们不讨论这样做是否有问题,但是这样的确会造成逻辑更复杂。 我们只是简单弄个HTTP请求,这个API太复杂了点。

好了,我应该提一下Spary主张的特别的DSL编写样式,文档中的一段:

val pipeline: HttpRequest => Future[OrderConfirmation] = (
addHeader("X-My-Special-Header", "fancy-value")
~> addCredentials(BasicHttpCredentials("bob", "secret"))
~> encode(Gzip)
~> sendReceive
~> decode(Deflate)
~> unmarshal[OrderConfirmation]
)

我相信你一但熟悉这个DSL,你可能会觉得清楚并实用。但是你只是要做个简单的HTTP请求,却要花数小时来弄清楚spray-client的源码,这就比较蛋疼。

结论:非常强健,但是过度设计了。如果你想装逼就用它。

Play! WS

最好的要放在最后。Play!是一个重量级的Scala web框架。它提供了很多有用的东西。同时模块又比Spray少。有些东西可以直接被使用。其中之一就是WS API 学名为who calls HTTP APIs webservices these days ?不忽悠,的确不错哦。

libraryDependencies += "com.typesafe.play" %% "play-ws" % "2.4.3"

代码

import play.api.libs.ws.ning.NingWSClient
import scala.concurrent.ExecutionContext.Implicits.global

// Instantiation of the client
// In a real-life application, you would instantiate one, share it everywhere,
// and call wsClient.close() when you're done
val wsClient = NingWSClient()
wsClient
.url("http://jsonplaceholder.typicode.com/comments/1")
.withQueryString("some_parameter" -> "some_value", "some_other_parameter" -> "some_other_value")
.withHeaders("Cache-Control" -> "no-cache")
.get()
.map { wsResponse =>
    if (! (200 to 299).contains(wsResponse.status)) {
    sys.error(s"Received unexpected status ${wsResponse.status} : ${wsResponse.body}")
    }
    println(s"OK, received ${wsResponse.body}")
    println(s"The response header Content-Length was ${wsResponse.header("Content-Length")}")
}

好了就这些,这个API很容易理解,没有什么地方需要特别说明。

缺点:文档不是很友好,虽然这个组件‘在外面’也能很好俄使用,但是文档里面的例子都是针对Play! 框架的。

优点:你不需要怎么看文档,因为方法的命名清楚。使用你IDE的自动补全就能方便的寻找到你要使用的东西啦。

结论:很好 可以在Play!世界之外使用。

总结

我们今天学到了一些事情。

首先,谷歌的排名并不会精确的评估库质量:)

其次, 如果一个项目一年多没有更新了,Github会标一个大红条。

最后, Scala中HTTP库只有两个可用 spray-client 和 Play! WS.

最后的最后 : 你可能想用的是后者。

原文: Comparing Scala’s HTTP client libraries

打赏支持:支付宝/微信。如果你觉得我的文章对你有所帮助,可以打赏我哟。