§使用 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-error
和 stale-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 的有效案例物件為 BASIC
、DIGEST
、KERBEROS
、NTLM
和 SPNEGO
。
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")
上述程式碼片段中的 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
執行作業,就必須提供一個隱含執行內容,這會宣告未來回呼應執行的執行緒池。你可以透過在類別建構函式中宣告對 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-ws 和 2.6 移轉指南 以取得更多資訊。
§自訂 BodyReadables 和 BodyWritables
Play WS 提供豐富的類型支援,可透過 play.api.libs.ws.WSBodyWritables
處理主體,其中包含類型類別,用於將 WSRequest
主體中的輸入(例如 JsValue
或 XML
)轉換成 ByteString
或 Source[ByteString, _]
,以及 play.api.libs.ws.WSBodyReadables
,它彙總從 ByteString
或 Source[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
您可以使用 BodyWritable
和 InMemoryBody
,如下所示,為要求建立自訂主體可寫入。若要指定具有串流的自訂主體可寫入,請使用 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
play.ws.followRedirects
:設定用戶端以追蹤 301 和 302 轉址(預設為 true)。play.ws.useProxyProperties
:使用 JVM 系統的 HTTP 代理設定(http.proxyHost、http.proxyPort)(預設為 true)。play.ws.useragent
:設定 User-Agent 標頭欄位。play.ws.compressionEnabled
:設為 true 以使用 gzip/deflater 編碼(預設為 false)。
§使用 SSL 設定 WSClient
若要設定 WS 以搭配 HTTP over SSL/TLS (HTTPS) 使用,請參閱 設定 WS SSL。
§使用快取設定 WS
若要設定 WS 以搭配 HTTP 快取使用,請參閱 設定 WS 快取。
§設定逾時
WSClient 中有 3 個不同的逾時。逾時會導致 WSClient 要求中斷。
play.ws.timeout.connection
:連線至遠端主機時等待的最長時間(預設為 120 秒)。play.ws.timeout.idle
:要求可以閒置的最長時間(已建立連線,但等待更多資料)(預設為 120 秒)。play.ws.timeout.request
:您接受要求所需總時間(即使遠端主機仍傳送資料,也會中斷)(預設為 120 秒)。
可以使用 withRequestTimeout()
(請參閱「提出要求」區段)針對特定連線覆寫要求逾時。
§設定 AsyncHttpClientConfig
可以在基礎 AsyncHttpClientConfig 中設定下列進階設定。
請參閱 AsyncHttpClientConfig 文件 以取得更多資訊。
play.ws.ahc.keepAlive
play.ws.ahc.maxConnectionsPerHost
play.ws.ahc.maxConnectionsTotal
play.ws.ahc.maxConnectionLifetime
play.ws.ahc.idleConnectionInPoolTimeout
play.ws.ahc.maxNumberOfRedirects
play.ws.ahc.maxRequestRetry
play.ws.ahc.disableUrlEncoding
下一步:連線到 OpenID 服務
在此文件發現錯誤?此頁面的原始程式碼可在此處找到 here。在閱讀 文件指南 後,歡迎貢獻一個 pull request。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。