文档

§使用 Play WS 调用 REST API

有时我们希望从 Play 应用程序内部调用其他 HTTP 服务。Play 通过其 WS (“WebService”) 库 支持此功能,该库提供了一种通过 WSClient 实例进行异步 HTTP 调用的方法。

使用 WSClient 有两个重要部分:发出请求和处理响应。我们将首先讨论如何发出 GET 和 POST HTTP 请求,然后展示如何处理来自 WSClient 的响应。最后,我们将讨论一些常见用例。

注意:在 Play 2.6 中,Play WS 已被拆分为两个,一个底层独立客户端不依赖于 Play,另一个是使用 Play 特定类的包装器。此外,Play WS 中现在使用 AsyncHttpClient 和 Netty 的阴影版本来最大程度地减少库冲突,主要目的是让 Play 的 HTTP 引擎可以使用不同版本的 Netty。有关更多信息,请参阅 2.6 迁移指南

§将 WS 添加到项目

要使用 WSClient,首先将 ws 添加到您的 build.sbt 文件中

libraryDependencies += ws

§在 Play WS 中启用 HTTP 缓存

Play WS 支持 HTTP 缓存,但需要 JSR-107 缓存实现才能启用此功能。您可以添加 ehcache

libraryDependencies += ehcache

或者您可以使用其他与 JSR-107 兼容的缓存,例如 Caffeine

拥有库依赖项后,请在 WS 缓存配置 页面上显示启用 HTTP 缓存。

使用 HTTP 缓存可以节省对后端 REST 服务的重复请求,尤其是在与弹性功能(如 stale-on-errorstale-while-revalidate)结合使用时。

§发出请求

现在,任何想要使用 WS 的组件都必须声明对 WSClient 的依赖关系。

import javax.inject.Inject

import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.scaladsl._
import org.apache.pekko.stream.SystemMaterializer
import org.apache.pekko.util.ByteString
import play.api.http.HttpEntity
import play.api.libs.ws._
import play.api.mvc._

class Application @Inject() (ws: WSClient, val controllerComponents: ControllerComponents) extends BaseController {}

我们已将 WSClient 实例命名为 ws,以下所有示例都将假定此名称。

要构建 HTTP 请求,您需要从 ws.url() 开始,以指定 URL。

val request: WSRequest = ws.url(url)

这将返回一个 WSRequest,您可以使用它来指定各种 HTTP 选项,例如设置标头。您可以将调用链接在一起以构建复杂的请求。

val complexRequest: WSRequest =
  request
    .addHttpHeaders("Accept" -> "application/json")
    .addQueryStringParameters("search" -> "play")
    .withRequestTimeout(10000.millis)

最后,您需要调用一个与您要使用的 HTTP 方法相对应的方法。这将结束链,并在 WSRequest 中使用为构建的请求定义的所有选项。

val futureResponse: Future[WSResponse] = complexRequest.get()

这将返回一个 Future[WSResponse],其中 Response 包含从服务器返回的数据。

如果您正在执行任何阻塞工作,包括任何类型的 DNS 工作(例如调用 java.util.URL.equals()),那么您应该使用自定义执行上下文,如 ThreadPools 中所述,最好通过 CustomExecutionContext。您应该调整池的大小,以留出足够大的安全裕量来应对故障。

如果您正在调用 不可靠的网络,请考虑使用 Futures.timeout断路器(如 Failsafe)。

§带身份验证的请求

如果您需要使用 HTTP 身份验证,则可以在构建器中指定它,使用用户名、密码和 AuthSchemeAuthScheme 的有效案例对象是 BASICDIGESTKERBEROSNTLMSPNEGO

ws.url(url).withAuth(user, password, WSAuthScheme.BASIC).get()

§带重定向跟踪的请求

如果 HTTP 调用导致 302 或 301 重定向,您可以自动跟踪重定向,而无需进行另一次调用。

ws.url(url).withFollowRedirects(true).get()

§带查询参数的请求

参数可以指定为一系列键/值元组。使用 addQueryStringParameters 添加参数,并使用 withQueryStringParameters 覆盖所有查询字符串参数。

ws.url(url).addQueryStringParameters("paramKey" -> "paramValue").get()

§带附加标头的请求

标头可以指定为一系列键值对。使用addHttpHeaders追加额外的标头,使用withHttpHeaders覆盖所有标头。

ws.url(url).addHttpHeaders("headerKey" -> "headerValue").get()

如果您正在以特定格式发送纯文本,您可能需要明确定义内容类型。

ws.url(url)
  .addHttpHeaders("Content-Type" -> "application/xml")
  .post(xmlString)

§带有 Cookie 的请求

可以使用DefaultWSCookie或通过传递play.api.mvc.Cookie将 Cookie 添加到请求中。使用addCookies追加 Cookie,使用withCookies覆盖所有 Cookie。

ws.url(url).addCookies(DefaultWSCookie("cookieName", "cookieValue")).get()

§带有虚拟主机的请求

虚拟主机可以指定为字符串。

ws.url(url).withVirtualHost("192.168.1.1").get()

§带有超时的请求

如果您希望指定请求超时,可以使用withRequestTimeout设置值。通过传递Duration.Inf可以设置无限超时。

ws.url(url).withRequestTimeout(5000.millis).get()

§提交表单数据

要发布 URL 编码的表单数据,需要将Map[String, Seq[String]]传递到post中。
如果主体为空,您必须将play.api.libs.ws.EmptyBody传递到post方法中。

ws.url(url).post(Map("key" -> Seq("value")))

§提交 multipart/form-data

要发布 multipart-form-encoded 数据,需要将Source[play.api.mvc.MultipartFormData.Part[Source[ByteString, Any]], Any]传递到post中。

ws.url(url).post(Source.single(DataPart("key", "value")))

要上传文件,您需要将play.api.mvc.MultipartFormData.FilePart[Source[ByteString, Any]]传递到Source中。

ws.url(url)
  .post(
    Source(
      FilePart("hello", "hello.txt", Option("text/plain"), FileIO.fromPath(tmpFile.toPath)) :: DataPart(
        "key",
        "value"
      ) :: List()
    )
  )

§提交 JSON 数据

发布 JSON 数据最简单的方法是使用JSON库。

import play.api.libs.json._
val data = Json.obj(
  "key1" -> "value1",
  "key2" -> "value2"
)
val futureResponse: Future[WSResponse] = ws.url(url).post(data)

§提交 XML 数据

发布 XML 数据最简单的方法是使用 XML 文字。XML 文字很方便,但速度不快。为了提高效率,请考虑使用 XML 视图模板或 JAXB 库。

val data = <person>
  <name>Steve</name>
  <age>23</age>
</person>
val futureResponse: Future[WSResponse] = ws.url(url).post(data)

§提交流数据

也可以使用Pekko Streams在请求主体中流式传输数据。

例如,假设您执行了一个返回大型图像的数据库查询,并且您希望将该数据转发到另一个端点以进行进一步处理。理想情况下,如果您可以在从数据库接收数据时将其发送出去,您将减少延迟,并避免因在内存中加载大量数据而导致的问题。如果您的数据库访问库支持Reactive Streams(例如,Slick支持),以下示例展示了如何实现所述行为。

val wsResponse: Future[WSResponse] = ws
  .url(url)
  .withBody(largeImageFromDB)
  .execute("PUT")

上面代码片段中的 largeImageFromDB 是一个 Source[ByteString, _]

§请求过滤器

您可以通过添加请求过滤器对 WSRequest 进行额外处理。请求过滤器通过扩展 play.api.libs.ws.WSRequestFilter 特性添加,然后使用 request.withRequestFilter(filter) 将其添加到请求中。

play.api.libs.ws.ahc.AhcCurlRequestLogger 中添加了一个示例请求过滤器,用于将请求以 cURL 格式记录到 SLF4J。

ws.url(s"https://127.0.0.1:$serverPort")
  .withRequestFilter(AhcCurlRequestLogger())
  .put(Map("key" -> Seq("value")))

将输出

curl \
  --verbose \
  --request PUT \
 --header 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
 --data 'key=value' \
 https://127.0.0.1:19001/

§处理响应

通过在 Future 中进行映射,可以轻松地使用 Response

下面给出的示例有一些常见的依赖项,为了简洁起见,这里只显示一次。

每当对 Future 进行操作时,都必须提供一个隐式执行上下文 - 这声明了 Future 的回调应该在哪个线程池中运行。您可以在 DI-ed 类中注入默认的 Play 执行上下文,方法是在类的构造函数中声明对 ExecutionContext 的额外依赖项。

class PersonService @Inject() (ec: ExecutionContext) {
  // ...
}

这些示例还使用以下案例类进行序列化/反序列化

case class Person(name: String, age: Int)

WSResponse 扩展了 play.api.libs.ws.WSBodyReadables 特性,其中包含 Play JSON 和 Scala XML 转换的类型类。如果您想将响应转换为自己的类型,或者使用不同的 JSON 或 XML 编码,也可以创建自己的自定义类型类。

§将响应处理为 JSON

您可以通过调用 response.json 将响应处理为 JSON 对象

val futureResult: Future[String] =
  ws.url(url).get().map { response => (response.json \ "person" \ "name").as[String] }

JSON 库有一个 有用的功能,它会将隐式 Reads[T] 直接映射到一个类

import play.api.libs.json._

implicit val personReads: Reads[Person] = Json.reads[Person]

val futureResult: Future[JsResult[Person]] =
  ws.url(url).get().map { response => (response.json \ "person").validate[Person] }

§将响应处理为 XML

您可以通过调用 response.xml 将响应处理为 XML 文本

val futureResult: Future[scala.xml.NodeSeq] = ws.url(url).get().map { response => response.xml \ "message" }

§处理大型响应

调用get()post()execute()会导致响应主体在响应可用之前加载到内存中。当您下载一个大型的多吉字节文件时,这可能会导致不受欢迎的垃圾回收,甚至内存不足错误。

WS允许您使用Pekko Streams Sink增量地使用响应主体。WSRequest上的stream()方法返回一个流式WSResponse,其中包含一个bodyAsSource方法,该方法返回一个Source[ByteString, _]

注意:在2.5.x中,StreamedResponse是在响应request.stream()调用时返回的。在2.6.x中,返回一个标准的WSResponse,并且应该使用bodyAsSource()方法来返回Source。

这是一个使用折叠Sink来计算响应返回的字节数的简单示例

// Make the request
val futureResponse: Future[WSResponse] =
  ws.url(url).withMethod("GET").stream()

val bytesReturned: Future[Long] = futureResponse.flatMap { res =>
  // Count the number of bytes returned
  res.bodyAsSource.runWith(Sink.fold[Long, ByteString](0L) { (total, bytes) => total + bytes.length })
}

或者,您也可以将主体流式传输到另一个位置。例如,一个文件

// Make the request
val futureResponse: Future[WSResponse] =
  ws.url(url).withMethod("GET").stream()

val downloadedFile: Future[File] = futureResponse.flatMap { res =>
  val outputStream = java.nio.file.Files.newOutputStream(file.toPath)

  // The sink that writes to the output stream
  val sink = Sink.foreach[ByteString] { bytes => outputStream.write(bytes.toArray) }

  // materialize and run the stream
  res.bodyAsSource
    .runWith(sink)
    .andThen {
      case result =>
        // Close the output stream whether there was an error or not
        outputStream.close()
        // Get the result or rethrow the error
        result.get
    }
    .map(_ => file)
}

响应主体的另一个常见目的地是从控制器的Action中流式传输它们

def downloadFile = Action.async {
  // Make the request
  ws.url(url).withMethod("GET").stream().map { response =>
    // Check that the response was successful
    if (response.status == 200) {
      // Get the content type
      val contentType = response.headers
        .get("Content-Type")
        .flatMap(_.headOption)
        .getOrElse("application/octet-stream")

      // If there's a content length, send that, otherwise return the body chunked
      response.headers.get("Content-Length") match {
        case Some(Seq(length: String)) =>
          Ok.sendEntity(HttpEntity.Streamed(response.bodyAsSource, Some(length.toLong), Some(contentType)))
        case _ =>
          Ok.chunked(response.bodyAsSource).as(contentType)
      }
    } else {
      BadGateway
    }
  }
}

您可能已经注意到,在调用stream()之前,我们需要通过在请求上调用withMethod来设置要使用的HTTP方法。以下是另一个使用PUT而不是GET的示例

val futureResponse: Future[WSResponse] =
  ws.url(url).withMethod("PUT").withBody("some body").stream()

当然,您可以使用任何其他有效的HTTP动词。

§常见模式和用例

§链接WSClient调用

使用for推导是在受信任的环境中链接WSClient调用的好方法。您应该将for推导与Future.recover一起使用来处理可能的失败。

val futureResponse: Future[WSResponse] = for {
  responseOne   <- ws.url(urlOne).get()
  responseTwo   <- ws.url(responseOne.body).get()
  responseThree <- ws.url(responseTwo.body).get()
} yield responseThree

futureResponse.recover {
  case e: Exception =>
    val exceptionData = Map("error" -> Seq(e.getMessage))
    ws.url(exceptionUrl).post(exceptionData)
}

§在控制器中使用

当从控制器发出请求时,您可以将响应映射到Future[Result]。这可以与Play的Action.async操作构建器结合使用,如处理异步结果中所述。

def wsAction = Action.async {
  ws.url(url).get().map { response => Ok(response.body) }
}
status(wsAction(FakeRequest())) must_== OK

§使用WSClient与Future超时

如果一系列 WS 调用没有及时完成,将结果包装在一个超时块中可能会有用,如果该链没有及时完成,该块将返回一个失败的 Future - 这比使用 `withRequestTimeout` 更通用,`withRequestTimeout` 仅适用于单个请求。最好的方法是使用 Play 的 非阻塞超时功能,使用 play.api.libs.concurrent.Futures

// Adds withTimeout as type enrichment on Future[WSResponse]
import play.api.libs.concurrent.Futures._

val result: Future[Result] =
  ws.url(url)
    .get()
    .withTimeout(1.second)
    .flatMap { response =>
      // val url2 = response.json \ "url"
      ws.url(url2).get().map { response2 => Ok(response.body) }
    }
    .recover {
      case e: scala.concurrent.TimeoutException =>
        GatewayTimeout
    }

§编译时依赖注入

如果您使用编译时依赖注入,则可以通过在您的 应用程序组件 中使用特性 `AhcWSComponents` 来访问 `WSClient` 实例。

§直接创建 WSClient

我们建议您使用上面描述的依赖注入来获取 `WSClient` 实例。通过依赖注入创建的 `WSClient` 实例更易于使用,因为它们会在应用程序启动时自动创建,并在应用程序停止时清理。

但是,如果您愿意,您可以直接从代码实例化 `WSClient`,并将其用于发出请求或配置底层 `AsyncHttpClient` 选项。

如果您手动创建 WSClient,则必须调用 `client.close()` 来清理它,当您完成使用它时。每个客户端都会创建自己的线程池。如果您未能关闭客户端或创建了太多客户端,那么您将耗尽线程或文件句柄 - 您将收到类似“无法创建新的本地线程”或“打开的文件过多”的错误,因为底层资源被消耗掉了。

您需要一个 `pekko.stream.Materializer` 实例才能直接创建一个 `play.api.libs.ws.ahc.AhcWSClient` 实例。通常您会使用依赖注入将其注入到服务中

import play.api.libs.ws.ahc._

// usually injected through @Inject()(implicit mat: Materializer)
implicit val materializer: Materializer = app.materializer
val wsClient = AhcWSClient()

直接创建客户端意味着您也可以在 AsyncHttpClient 和 Netty 配置层更改配置

import play.api._
import play.api.libs.ws._
import play.api.libs.ws.ahc._

val configuration = Configuration("ws.followRedirects" -> true).withFallback(Configuration.reference)

// If running in Play, environment should be injected
val environment        = Environment(new File("."), this.getClass.getClassLoader, Mode.Prod)
val wsConfig           = AhcWSClientConfigFactory.forConfig(configuration.underlying, environment.classLoader)
val mat                = app.materializer
val wsClient: WSClient = AhcWSClient(wsConfig)(mat)

您也可以使用 play.api.test.WsTestClient.withTestClient 在功能测试中创建 `WSClient` 实例。有关更多详细信息,请参阅 ScalaTestingWebServiceClients

或者,您可以完全独立地运行 `WSClient`,而无需涉及正在运行的 Play 应用程序

import scala.concurrent.Future

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import org.apache.pekko.stream.SystemMaterializer
import play.api.libs.ws._
import play.api.libs.ws.ahc.AhcWSClient
import play.api.libs.ws.ahc.StandaloneAhcWSClient
import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient
import play.shaded.ahc.org.asynchttpclient.AsyncHttpClientConfig
import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient
import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig

object Main {
  import scala.concurrent.ExecutionContext.Implicits._

  def main(args: Array[String]): Unit = {
    implicit val system = ActorSystem()

    val asyncHttpClientConfig = new DefaultAsyncHttpClientConfig.Builder()
      .setMaxRequestRetry(0)
      .setShutdownQuietPeriod(0)
      .setShutdownTimeout(0)
      .build
    val asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig)

    implicit val materializer = SystemMaterializer(system).materializer
    val wsClient: WSClient    = new AhcWSClient(new StandaloneAhcWSClient(asyncHttpClient))

    call(wsClient)
      .andThen { case _ => wsClient.close() }
      .andThen { case _ => system.terminate() }
  }

  def call(wsClient: WSClient): Future[Unit] = {
    wsClient.url("https://www.google.com").get().map { response =>
      val statusText: String = response.statusText
      println(s"Got a response $statusText")
    }
  }
}

这在存在从配置中无法访问的特定 HTTP 客户端选项的情况下非常有用。

再次,完成自定义客户端工作后,您**必须**关闭客户端。

wsClient.close()

理想情况下,您应该在确认所有请求都已完成之后关闭客户端。请谨慎使用自动资源管理模式来关闭客户端,因为 WSClient 逻辑是异步的,许多 ARM 解决方案可能针对单线程同步解决方案而设计。

§独立 WS

如果您想在 Play 上下文之外调用 WS,可以使用 Play WS 的独立版本,该版本不依赖于任何 Play 库。您可以通过将 play-ahc-ws-standalone 添加到您的项目来实现。

libraryDependencies += "org.playframework" %% "play-ahc-ws-standalone" % playWSStandalone

有关更多信息,请参阅 https://github.com/playframework/play-ws2.6 迁移指南

§自定义 BodyReadables 和 BodyWritables

Play WS 提供了丰富的类型支持,用于以 play.api.libs.ws.WSBodyWritables 的形式处理主体,其中包含用于将输入(例如 JsValueXML)从 WSRequest 主体转换为 ByteStringSource[ByteString, _] 的类型类,以及 play.api.libs.ws.WSBodyReadables,它聚合了从 ByteStringSource[ByteString, _] 读取 WSResponse 主体并返回相应类型(例如 JsValue 或 XML)的类型类。这些类型类在您导入 ws 包时会自动在范围内,但您也可以创建自定义类型。这在您想使用自定义库时特别有用,例如,您想通过 STaX API 流式传输 XML 或使用其他 JSON 库(如 Argonaut 或 Circe)。

§创建自定义可读

您可以通过访问响应主体来创建自定义可读。

trait URLBodyReadables {
  implicit val urlBodyReadable: BodyReadable[URL] = BodyReadable[java.net.URL] { response =>
    import play.shaded.ahc.org.asynchttpclient.{ Response => AHCResponse }
    val ahcResponse = response.underlying[AHCResponse]
    val s           = ahcResponse.getResponseBody
    java.net.URI.create(s).toURL
  }
}

§创建自定义 BodyWritable

您可以使用 BodyWritableInMemoryBody 创建一个自定义主体可写到请求。要使用流指定自定义主体可写,请使用 SourceBody

trait URLBodyWritables {
  implicit val urlBodyWritable: BodyWritable[URL] = BodyWritable[java.net.URL](
    { url =>
      val s          = url.toURI.toString
      val byteString = ByteString.fromString(s)
      InMemoryBody(byteString)
    },
    "text/plain"
  )
}

§访问 AsyncHttpClient

您可以从 WSClient 获取对底层 AsyncHttpClient 的访问权限。

import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient

val client: AsyncHttpClient = ws.underlying

§配置 WSClient

application.conf中使用以下属性配置 WSClient

§使用 SSL 配置 WSClient

要配置 WS 以使用 HTTP over SSL/TLS (HTTPS),请参阅配置 WS SSL

§使用缓存配置 WS

要配置 WS 以使用 HTTP 缓存,请参阅配置 WS 缓存

§配置超时

WSClient 中有 3 种不同的超时。达到超时会导致 WSClient 请求中断。

可以使用withRequestTimeout()(请参阅“发出请求”部分)为特定连接覆盖请求超时。

§配置 AsyncHttpClientConfig

可以在底层的 AsyncHttpClientConfig 上配置以下高级设置。

有关更多信息,请参阅AsyncHttpClientConfig 文档

下一步:连接到 OpenID 服务


发现此文档中的错误?此页面的源代码可以在这里找到。在阅读文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。