The Play WS API

    使用 WS API 包含两个重要部分:构造请求以及处理响应。我们会先讨论如何构造 GET 和 POST 的 HTTP 请求,然后展示如何处理返回的响应。最后,我们会讨论一些常见用例。

    想要使用 WS,首先需要在你的 文件中加上 ws 库:

    然后导入以下内容:

    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)有:BASICDIGESTKERBEROSNONENTLMSPNEGO

    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)

    带虚拟主机的请求

    你可以用一个字符串来指定一个虚拟主机:

    如果你想指定请求的超时时间,你可以使用 withRequestTimeout 来设置一个单位为毫秒的值:

    1. WS.url(url).withRequestTimeout(5000).get()

    提交表单数据

    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 上的一个操作,都是需要一个隐式的执行上下文的,这一点声明了回调应该运行在哪个线程池。通常,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._
    2. implicit val personReads = Json.reads[Person]
    3. val futureResult: Future[JsResult[Person]] = WS.url(url).get().map {
    4. response => (response.json \ "person").validate[Person]
    5. }

    你可以通过调用 response.xml 将响应当作 XML 字面量来处理。

    1. val futureResult: Future[scala.xml.NodeSeq] = WS.url(url).get().map {
    2. response =>
    3. response.xml \ "message"
    4. }

    处理大块响应

    调用 或 post() 方法会导致一个问题,就是只有当响应体完全载入到内存中,响应才可用。当你在下载几个 G 的大文件时,这可能会导致另人不爽的 GC,甚至出现内存不足的错误。

    WS 可以让你通过 iteratee 来增量地使用响应。WSRequestHolderstream()getStream() 方法返回一个 Future[(WSResponseHeaders, Enumerator[Array[Byte]])]。其中,枚举器包含了响应体。

    下面是一个常见的例子,使用 iteratee 计算响应返回的字节数:

    当然,通常情况下你不会像上面那样只是计算数据的字节数,更常见的情况是把响应返回的数据写到另一个地方去,比如写入文件:

    1. import play.api.libs.iteratee._
    2. // Make the request
    3. val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
    4. WS.url(url).getStream()
    5. val downloadedFile: Future[File] = futureResponse.flatMap {
    6. case (headers, body) =>
    7. val outputStream = new FileOutputStream(file)
    8. // The iteratee that writes to the output stream
    9. val iteratee = Iteratee.foreach[Array[Byte]] { bytes =>
    10. outputStream.write(bytes)
    11. }
    12. // Feed the body into the iteratee
    13. (body |>>> iteratee).andThen {
    14. case result =>
    15. // Close the output stream whether there was an error or not
    16. outputStream.close()
    17. // Get the result or rethrow the error
    18. result.get
    19. }.map(_ => file)
    20. }

    另一种情况是,当前服务器把拿到的响应体流式写入另一个响应中,返回给它所服务的对象:

    1. def downloadFile = Action.async {
    2. // Make the request
    3. WS.url(url).getStream().map {
    4. case (response, body) =>
    5. // Check that the response was successful
    6. if (response.status == 200) {
    7. // Get the content type
    8. val contentType = response.headers.get("Content-Type").flatMap(_.headOption)
    9. .getOrElse("application/octet-stream")
    10. // If there's a content length, send that, otherwise return the body chunked
    11. response.headers.get("Content-Length") match {
    12. case Some(Seq(length)) =>
    13. Ok.feed(body).as(contentType).withHeaders("Content-Length" -> length)
    14. case _ =>
    15. Ok.chunked(body).as(contentType)
    16. }
    17. } else {
    18. BadGateway
    19. }
    20. }
    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. responseThree <- WS.url(responseTwo.body).get()
    4. } yield responseThree
    5. futureResponse.recover {
    6. case e: Exception =>
    7. val exceptionData = Map("error" -> Seq(e.getMessage))
    8. WS.url(exceptionUrl).post(exceptionData)
    9. }

    在控制器中使用

    当在控制器中构建请求时,你可以将响应映射为 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 是底层 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()

    也可以像下面直接使用:

    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. }
    10. import scala.language.implicitConversions
    11. import PairMagnet._
    12. val client = WS.client
    13. val exampleURL = new java.net.URL(url)
    14. val response = WS.url(client -> exampleURL).get()

    默认情况下,配置一般写在 application.conf 里,但你也可以直接在代码中设置配置:

    你也可以直接访问底层的 。

    1. import com.ning.http.client.AsyncHttpClient
    2. val client: AsyncHttpClient = WS.client.underlying

    可以直接访问底层类是非常重要的,因为 WS 在有的时候会有一些限制:

    • WS 并不直接支持 multi-part-form 数据的上传。你可以使用底层客户端的 RequestBuilder.addBodyPart 来做。
    • WS 不支持流式数据上传。在这种情况下,你应该使用 AsyncHttpClient 提供的 FeedableBodyGenerator

    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.request:你能允许的请求使用的总时间(当达到这个时间时,请求就会中断,哪怕远程主机仍然在发送数据)(默认是 none,为的是可以处理流式数据)。