The Play WS API
有的时候我们需要在 Play 应用中调用其它 HTTP 服务。Play 通过 WS 库来支持这一需求,它提供了一种方法来做异步 HTTP 调用。
使用 WS API 包含两个重要部分:构造请求以及处理响应。我们会先讨论如何构造 GET 和 POST 的 HTTP 请求,然后展示如何处理返回的响应。最后,我们会讨论一些常见用例。
构造请求
想要使用 WS,首先需要在你的 build.sbt 文件中加上 ws 库:
1. libraryDependencies ++= Seq(
2. ws
3. )
然后导入以下内容:
1. import play.api.Play.current
2. import play.api.libs.ws._
3. import play.api.libs.ws.ning.NingAsyncHttpClientConfigBuilder
4. import scala.concurrent.Future
想要构建一个 HTTP 请求,你首先要用 WS.url() 来指定 URL:
1. val holder: WSRequestHolder = WS.url(url)
上面的语句返回一个类型为 WSRequestHolder 的值,你可以通过它指定各种 HTTP 选项,例如设置各种报头。你可以使用链式调用来构造复杂的请求。
1. val complexHolder: WSRequestHolder =
2. holder.withHeaders("Accept" -> "application/json")
3. .withRequestTimeout(10000)
4. .withQueryString("search" -> "play")
最后你通过调用一个与 HTTP 方法对应的方法来发出请求,它会使用你之前设置的所有选项。
1. val futureResponse: Future[WSResponse] = complexHolder.get()
上述调用返回的类型是 Future[WSResponse],返回的响应包含了服务器传来的数据。
带验证的请求
如果你需要使用 HTTP 验证,你可以要构造请求的过程中指定它,使用用户名、密码以及 AuthScheme。AuthScheme 的有效样例对象(case objects)有:BASIC,DIGEST,KERBEROS,NONE,NTLM 和 SPNEGO。
1. WS.url(url).withAuth(user, password, WSAuthScheme.BASIC).get()
带重定向的请求
如果一个 HTTP 请求的结果是 302 或 301 重定向,你可以自动执行重定向而无需另一次的调用。
1. WS.url(url).withFollowRedirects(true).get()
带查询参数的请求
参数可以被指定为一系列的 K/V 元组。
1. WS.url(url).withQueryString("paramKey" -> "paramValue").get()
带额外报头的请求
报头也可以被指定为一系列的 K/V 元组。
1. WS.url(url).withHeaders("headerKey" -> "headerValue").get()
如果你想以特殊格式发送纯文本,你需要显式地定义内容类型:
1. WS.url(url).withHeaders("Content-Type" -> "application/xml").post(xmlString)
带虚拟主机的请求
你可以用一个字符串来指定一个虚拟主机:
1. WS.url(url).withVirtualHost("192.168.1.1").get()
带超时时间的请求
如果你想指定请求的超时时间,你可以使用 withRequestTimeout 来设置一个单位为毫秒的值:
1. WS.url(url).withRequestTimeout(5000).get()
提交表单数据
想要 post 一个 url-form-encoded 的数据,你需要给 post 方法传一个类型为 Map[String, Seq[String]] 的值。
1. WS.url(url).post(Map("key" -> Seq("value")))
提交 JSON 数据
post Json 数据最简单的方法就是使用 JSON 库。
1. import play.api.libs.json._
2. val data = Json.obj(
3. "key1" -> "value1",
4. "key2" -> "value2"
5. )
6. val futureResponse: Future[WSResponse] = WS.url(url).post(data)
提交 XML 数据
post XML 数据最简单的方法是使用 XML 字面量。XML 字面量用起来很方便,但速度不快。追求效率的话,请考虑使用 XML 视图模板或 JAXB 库。
1. val data = <person>
2. <name>Steve</name>
3. <age>23</age>
4. </person>
5. val futureResponse: Future[WSResponse] = WS.url(url).post(data)
处理响应
处理 Response 可以通过在 Future 内做映射来简单完成。
下面给出的例子都有一些共同的依赖,在这里先简单地说明一下。
任何时候 Future 上的一个操作,都是需要一个隐式的执行上下文的,这一点声明了回调应该运行在哪个线程池。通常,Play 默认的执行上下文就足够了:
1. implicit val context = play.api.libs.concurrent.Execution.Implicits.defaultContext
下面的例子还使用了以下样例类来进行序列化与反序列化:
1. case class Person(name: String, age: Int)
处理 JSON 响应
你可以通过调用 response.json 将响应当作 JSON 对象来处理。
1. val futureResult: Future[String] = WS.url(url).get().map {
2. response =>
3. (response.json \ "person" \ "name").as[String]
4. }
Play 的 JSON 库还有一个有用的特性,可以将一个隐式的 Reads[T] 直接映射成一个类:
1. import play.api.libs.json._
3. implicit val personReads = Json.reads[Person]
5. val futureResult: Future[JsResult[Person]] = WS.url(url).get().map {
6. response => (response.json \ "person").validate[Person]
7. }
处理 XML 响应
你可以通过调用 response.xml 将响应当作 XML 字面量来处理。
1. val futureResult: Future[scala.xml.NodeSeq] = WS.url(url).get().map {
2. response =>
3. response.xml \ "message"
4. }
处理大块响应
调用 get() 或 post() 方法会导致一个问题,就是只有当响应体完全载入到内存中,响应才可用。当你在下载几个 G 的大文件时,这可能会导致另人不爽的 GC,甚至出现内存不足的错误。
WS 可以让你通过 iteratee 来增量地使用响应。WSRequestHolder 的 stream() 和 getStream() 方法返回一个 Future[(WSResponseHeaders, Enumerator[Array[Byte]])]。其中,枚举器包含了响应体。
下面是一个常见的例子,使用 iteratee 计算响应返回的字节数:
1. import play.api.libs.iteratee._
3. // Make the request
4. val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
5. WS.url(url).getStream()
7. val bytesReturned: Future[Long] = futureResponse.flatMap {
8. case (headers, body) =>
9. // Count the number of bytes returned
10. body |>>> Iteratee.fold(0l) { (total, bytes) =>
11. total + bytes.length
12. }
13. }
当然,通常情况下你不会像上面那样只是计算数据的字节数,更常见的情况是把响应返回的数据写到另一个地方去,比如写入文件:
1. import play.api.libs.iteratee._
3. // Make the request
4. val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
5. WS.url(url).getStream()
7. val downloadedFile: Future[File] = futureResponse.flatMap {
8. case (headers, body) =>
9. val outputStream = new FileOutputStream(file)
11. // The iteratee that writes to the output stream
12. val iteratee = Iteratee.foreach[Array[Byte]] { bytes =>
13. outputStream.write(bytes)
14. }
16. // Feed the body into the iteratee
17. (body |>>> iteratee).andThen {
18. case result =>
19. // Close the output stream whether there was an error or not
20. outputStream.close()
21. // Get the result or rethrow the error
22. result.get
23. }.map(_ => file)
24. }
另一种情况是,当前服务器把拿到的响应体流式写入另一个响应中,返回给它所服务的对象:
1. def downloadFile = Action.async {
3. // Make the request
4. WS.url(url).getStream().map {
5. case (response, body) =>
7. // Check that the response was successful
8. if (response.status == 200) {
10. // Get the content type
11. val contentType = response.headers.get("Content-Type").flatMap(_.headOption)
12. .getOrElse("application/octet-stream")
14. // If there's a content length, send that, otherwise return the body chunked
15. response.headers.get("Content-Length") match {
16. case Some(Seq(length)) =>
17. Ok.feed(body).as(contentType).withHeaders("Content-Length" -> length)
18. case _ =>
19. Ok.chunked(body).as(contentType)
20. }
21. } else {
22. BadGateway
23. }
24. }
25. }
POST 和 PUT 的调用需要手动调用 withMethod 方法:
1. val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
2. WS.url(url).withMethod("PUT").withBody("some body").stream()
常见模式和用例
链式 WS 调用
使用 for 解析是一种链接 WS 调用的好方式。for 解析应该和 Future.recover 配合使用,用于处理可能的失败。
1. val futureResponse: Future[WSResponse] = for {
2. responseOne <- WS.url(urlOne).get()
3. responseTwo <- WS.url(responseOne.body).get()
4. responseThree <- WS.url(responseTwo.body).get()
5. } yield responseThree
7. futureResponse.recover {
8. case e: Exception =>
9. val exceptionData = Map("error" -> Seq(e.getMessage))
10. WS.url(exceptionUrl).post(exceptionData)
11. }
在控制器中使用
当在控制器中构建请求时,你可以将响应映射为 Future[Result]。这可以与 Play 的 Action.async 结合使用,详见:Handling Asynchronous Results。
1. def wsAction = Action.async {
2. WS.url(url).get().map { response =>
3. Ok(response.body)
4. }
5. }
6. status(wsAction(FakeRequest())) must_== OK
使用 WSClient
WSClient 是底层 AsyncHttpClient 的 wrapper。有时候你需要定义多个客户端,建议使用不同的配置文件,或使用模拟的方式。
默认的客户端可以由 WS 单例调用:
1. val client: WSClient = WS.client
你可以直接从代码中定义一个 WS 客户端,通过 WS.clientUrl() 隐式地使用。注意,当你配置你的客户端时,你应该使用 NingAsyncHttpClientConfigBuilder 来做 TLS 配置:
1. val clientConfig = new DefaultWSClientConfig()
2. val secureDefaults:com.ning.http.client.AsyncHttpClientConfig = new NingAsyncHttpClientConfigBuilder(clientConfig).build()
3. // You can directly use the builder for specific options once you have secure TLS defaults...
4. val builder = new com.ning.http.client.AsyncHttpClientConfig.Builder(secureDefaults)
5. builder.setCompressionEnabled(true)
6. val secureDefaultsWithSpecificOptions:com.ning.http.client.AsyncHttpClientConfig = builder.build()
7. implicit val implicitClient = new play.api.libs.ws.ning.NingWSClient(secureDefaultsWithSpecificOptions)
8. val response = WS.clientUrl(url).get()
注意:如果你实例化一个 NingWSClient 对象,它并不会使用 WS 插件系统,因此它不会在
Application.onStop里自动关闭。当处理结束时,你需要使用client.close()来手动关闭客户端。这会释放 AsyncHttpClient 使用的底层 ThreadPoolExecutor。客户端关闭失败可能会导致内存不足的异常(尤其在开发模式下,需要经常性重新加载应用的时候)。
也可以像下面直接使用:
1. val response = client.url(url).get()
或使用磁铁模式(magnet pattern)来自动匹配合适的客户端:
1. object PairMagnet {
2. implicit def fromPair(pair: (WSClient, java.net.URL)) =
3. new WSRequestHolderMagnet {
4. def apply(): WSRequestHolder = {
5. val (client, netUrl) = pair
6. client.url(netUrl.toString)
7. }
8. }
9. }
11. import scala.language.implicitConversions
12. import PairMagnet._
14. val client = WS.client
15. val exampleURL = new java.net.URL(url)
16. val response = WS.url(client -> exampleURL).get()
默认情况下,配置一般写在 application.conf 里,但你也可以直接在代码中设置配置:
1. import com.typesafe.config.ConfigFactory
2. import play.api.libs.ws._
3. import play.api.libs.ws.ning._
5. val configuration = play.api.Configuration(ConfigFactory.parseString(
6. """
7. |ws.followRedirects = true
8. """.stripMargin))
10. val classLoader = app.classloader // Play.current.classloader or other
11. val parser = new DefaultWSConfigParser(configuration, classLoader)
12. val builder = new NingAsyncHttpClientConfigBuilder(parser.parse())
你也可以直接访问底层的 async client。
1. import com.ning.http.client.AsyncHttpClient
3. val client: AsyncHttpClient = WS.client.underlying
可以直接访问底层类是非常重要的,因为 WS 在有的时候会有一些限制:
WS并不直接支持 multi-part-form 数据的上传。你可以使用底层客户端的 RequestBuilder.addBodyPart 来做。WS不支持流式数据上传。在这种情况下,你应该使用 AsyncHttpClient 提供的FeedableBodyGenerator。
配置 WS
在 application.conf 文件中使用如下属性来配置 WS 客户端:
ws.followRedirects:配置客户端做 301、302 重定向(默认为 true)。ws.useProxyProperties:使用系统的 http 代理设置(http.proxyHost,http.proxyPort)(默认为 true)。ws.useragent:配置 User-Agent 报头。ws.compressionEnabled:如果使用 gzip/deflater 编码,则将它设置为 true(默认为 false)。
用 SSL 配置 WS
想要配置 WS 在 SSL/TLS(HTTPS)之上使用 HTTP,请移步:配置 WS SSL
配置超时时间
WS 中有 3 种不同的超时。超时会导致 WS 请求中断。
ws.timeout.connection:连接远程主机的最大等待时间(默认是 120 秒)。ws.timeout.idle:请求保持空闲状态的最大时间(此时连接已经建立,在等待更多的数据)(默认是 120 秒)。ws.timeout.request:你能允许的请求使用的总时间(当达到这个时间时,请求就会中断,哪怕远程主机仍然在发送数据)(默认是 none,为的是可以处理流式数据)。
在一个具体的连接中,你可以用 withRequestTimeout() 来覆盖配置文件中的请求超时设置(详见「构造请求」一节)。
