文件

§使用 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 驗證,可以使用使用者名稱、密碼和 AuthScheme 在建構器中指定它。AuthScheme 的有效案例物件為 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-form-encoded 資料,需要將 Map[String, Seq[String]] 傳遞到 post 中。
如果主體為空,您必須將 play.api.libs.ws.EmptyBody 傳遞到 post 方法中。

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

§提交 multipart/form 資料

若要張貼 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 串流 在要求主體中串流資料。

例如,假設您已執行資料庫查詢,會傳回大型影像,而且您想要將該資料轉發到不同的端點以進行進一步處理。理想情況下,如果您可以在從資料庫接收資料時將其傳送出去,您將減少延遲,並避免因在記憶體中載入大量資料而產生的問題。如果您的資料庫存取函式庫支援 反應式串流(例如,Slick 支援),以下是顯示如何實作所述行為的範例

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

上述程式碼片段中的 largeImageFromDBSource[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 內部進行對應,可以輕鬆處理 回應

以下範例有一些常見的相依性,將在此處簡短說明。

只要對 Future 執行作業,就必須提供一個隱含執行內容,這會宣告未來回呼應執行的執行緒池。你可以透過在類別建構函式中宣告對 ExecutionContext 的額外相依性,將預設的 Play 執行內容注入到你的 DI-ed 類別中

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() 會導致回應主體在回應可供使用之前載入到記憶體中。當您下載一個大型的、多 GB 的檔案時,這可能會導致不必要的垃圾收集,甚至記憶體不足的錯誤。

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

§使用具備 Future Timeout 的 WSClient

如果 WS 呼叫鏈未及時完成,將結果包裝在逾時區塊中可能很有用,如果鏈未及時完成,這將會傳回失敗的 Future – 這比使用僅適用於單一請求的 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 處理主體,其中包含類型類別,用於將 WSRequest 主體中的輸入(例如 JsValueXML)轉換成 ByteStringSource[ByteString, _],以及 play.api.libs.ws.WSBodyReadables,它彙總從 ByteStringSource[ByteString, _] 讀取 WSResponse 主體並傳回適當類型(例如 JsValue 或 XML)的類型類別。匯入 ws 套件時,這些類型類別會自動出現在範圍內,但您也可以建立自訂類型。如果您想使用自訂函式庫,這會特別有用,亦即您想透過 STaX API 串流 XML 或使用其他 JSON 函式庫,例如 Argonaut 或 Circe。

§建立自訂 Readable

您可以透過存取回應主體來建立自訂 Readable

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 服務


在此文件發現錯誤?此頁面的原始程式碼可在此處找到 here。在閱讀 文件指南 後,歡迎貢獻一個 pull request。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。