§The Play WS API
Play アプリケーションから他の HTTP サービスを呼び出したくなることがあります。Play は非同期の HTTP 呼び出しを実現する WS ライブラリ でこれをサポートしています。
WS API には、リクエストの作成とレスポンスの処理という2つの重要な部品があります。まず、GET および POST の HTTP リクエストを作成する方法について紹介し、次に WS からレスポンスを処理する方法について紹介します。最後に、よくあるユースケースを紹介します。
§リクエストの作成
WS を使用するには、まず ws
を build.sbt
ファイルに追加してください。
libraryDependencies ++= Seq(
ws
)
そして、WS を利用したいコントローラやコンポーネントで WSClient
への依存を宣言します。
import javax.inject.Inject
import scala.concurrent.Future
import play.api.mvc._
import play.api.libs.ws._
class Application @Inject() (ws: WSClient) extends Controller {
}
WSClient
のインスタンスを ws
と名付けたので、以下の例ではこの名前を用いることとします。
HTTP リクエストを構築するには、 WS.url()
を URL を設定して呼び出します。
val request: WSRequest = ws.url(url)
これは WSRequest を返します。WSRequest はヘッダの設定のような様々な HTTP のオプションを設定するために使用します。メソッド呼び出しを連鎖すると、複雑なリクエストをまとめて構築できます。
val complexRequest: WSRequest =
request.withHeaders("Accept" -> "application/json")
.withRequestTimeout(10000)
.withQueryString("search" -> "play")
最後に、使用したい HTTP メソッドに対応するメソッドを呼び出します。連鎖はこれで終了し、 WSRequest
のリクエストに設定した全てのオプションが使用されます。
val futureResponse: Future[WSResponse] = complexRequest.get()
すると、 Future[WSResponse]
が返されます。ここで Response がサーバから返されたデータを保持しています。
§認証の設定
HTTP 認証を使う必要がある場合には、ビルダーにユーザー名、パスワード、 AuthScheme を設定します。AuthScheme に適用可能なケースオブジェクトは、BASIC
, DIGEST
, KERBEROS
, NONE
, NTLM
そして SPNEGO
です。
ws.url(url).withAuth(user, password, WSAuthScheme.BASIC).get()
§リダイレクトの設定
もし、HTTP 呼び出しの結果が、302 や 301 のようなリダイレクトである場合には、他のメソッド呼び出しをしなくとも自動的にリダイレクトされます。
ws.url(url).withFollowRedirects(true).get()
§クエリストリングの設定
パラメーターは、キーと値のタプルで設定します。
ws.url(url).withQueryString("paramKey" -> "paramValue").get()
§ヘッダの設定
ヘッダは、キーと値のタプルで設定します。
ws.url(url).withHeaders("headerKey" -> "headerValue").get()
もし、プレーンなテキストを特定のフォーマットで送信したいのなら、コンテントタイプを明示的に設定する必要があります。
ws.url(url).withHeaders("Content-Type" -> "application/xml").post(xmlString)
§バーチャルホストの設定
バーチャルホストは文字列で設定します。
ws.url(url).withVirtualHost("192.168.1.1").get()
§タイムアウトの設定
リクエストにタイムアウトを設定する場合には、 withRequestTimeout
を使用しミリ秒で値を設定します。 -1
を設定すると無期限になります。
ws.url(url).withRequestTimeout(5000).get()
§フォームデータの送信
フォームエンコードされたデータを POST で送信するには、post
に Map[String, Seq[String]]
を渡してください。
ws.url(url).post(Map("key" -> Seq("value")))
§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)
§レスポンスの処理
Response に対する操作は Future の中でマッピングをすることで簡単に行えます。
以降の例には、いくつか共通の依存コードがあります。簡潔にするため、ここで1度だけ掲載します。
Future
上で処理を実行するときにはいつでも、暗黙的な実行コンテキストが必要となります。実行コンテキストとは Future
が実行されてコールバックを行うスレッドプールのことです 。通常は、Play のデフォルトの実行コンテキストで充分でしょう。
implicit val context = play.api.libs.concurrent.Execution.Implicits.defaultContext
次の例も、以降のコードで使用するシリアライゼーション、あるいはデシリアラーゼーションのためのケースクラスです。
case class Person(name: String, age: Int)
§JSON レスポンスの処理
レスポンスを JSON object として処理するには、response.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 = Json.reads[Person]
val futureResult: Future[JsResult[Person]] = ws.url(url).get().map {
response => (response.json \ "person").validate[Person]
}
§XML レスポンスの処理
レスポンスを XML リテラル として処理するには、response.xml
を呼び出します。
val futureResult: Future[scala.xml.NodeSeq] = ws.url(url).get().map {
response =>
response.xml \ "message"
}
§巨大なレスポンスの処理
get()
や post()
を実行すると、レスポンスが使用可能になる前に、リクエストボディをメモリに読み込みます。数ギガバイトのファイルのような大量のダウンロードを行うと、不愉快なガベージコレクション、ひいてはアウトオブメモリーエラーを招くかもしれません。
WS では インクリメンタルなレスポンスを iteratee によって扱うことができます。WSRequest
の stream()
と getStream()
メソッドは Future[(WSResponseHeaders, Enumerator[Array[Byte]])]
を返します。enumerator には レスポンスボディが含まれています。
iteratee を使用して、レスポンスによって返却されたバイト数を数える、ちょっとした例を示します。
import play.api.libs.iteratee._
// Make the request
val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
ws.url(url).getStream()
val bytesReturned: Future[Long] = futureResponse.flatMap {
case (headers, body) =>
// Count the number of bytes returned
body |>>> Iteratee.fold(0l) { (total, bytes) =>
total + bytes.length
}
}
もちろん、通常はこのような方法で大きなボディを消費したくはないでしょう。より一般的な使用方法は、ボディを別の場所にストリームによって出力することです。次の例では、ボディをファイルにストリームで出力します。
import play.api.libs.iteratee._
// Make the request
val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
ws.url(url).getStream()
val downloadedFile: Future[File] = futureResponse.flatMap {
case (headers, body) =>
val outputStream = new FileOutputStream(file)
// The iteratee that writes to the output stream
val iteratee = Iteratee.foreach[Array[Byte]] { bytes =>
outputStream.write(bytes)
}
// Feed the body into the iteratee
(body |>>> iteratee).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)
}
レスポンスボディの送り先としては他に、このサーバーが現在処理しているレスポンスへのストリームによる出力があります。
def downloadFile = Action.async {
// Make the request
ws.url(url).getStream().map {
case (response, body) =>
// 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)) =>
Ok.feed(body).as(contentType).withHeaders("Content-Length" -> length)
case _ =>
Ok.chunked(body).as(contentType)
}
} else {
BadGateway
}
}
}
POST
と PUT
は、 withMethod
メソッドを明示的に呼び出す必要があります。
val futureResponse: Future[(WSResponseHeaders, Enumerator[Array[Byte]])] =
ws.url(url).withMethod("PUT").withBody("some body").stream()
§よくある例と使用方法
§WS 呼び出しの連結
for 内包表記は、信頼された環境で WS の呼び出しを連結するのに適しています。起こりうる失敗を処理するには、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 を使用する
WSClient は AsyncHttpClient を包むラッパーです。別々のプロファイルを指定した複数のクライアントを定義したり、モックを使用できたりして便利です。
WSによる注入を受けることなく、コードから直接 WS クライアントを定義することができます。そして、 WS.clientUrl()
の際にはクライアントを暗黙的に使用することができます。
import play.api.libs.ws.ning._
implicit val sslClient = NingWSClient()
// close with sslClient.close() when finished with client
val response = WS.clientUrl(url).get()
NOTE: NingWSClient オブジェクトをインスタンス化した場合には、それは WS モジュールのライフサイクルに乗らないので、
Application.onStop
の際に自動的にはクローズされません。そのかわりに、処理が終了した際にclient.close()
することによりクライアントを手動でシャットダウンしなければなりません。これによって、AsyncHttpClient が内部で使用している ThreadPoolExecutor を開放します。クライアントを閉じることに失敗すると、アウトオブメモリー例外が生じるおそれがあります (特に、開発モードでアプリケーションを頻繁にリロードする場合)。
または直接、
val response = sslClient.url(url).get()
あるいは、マグネットパターンを利用して、特定のクライアントを自動的に紐付けることもできます。
object PairMagnet {
implicit def fromPair(pair: (WSClient, java.net.URL)) =
new WSRequestMagnet {
def apply(): WSRequest = {
val (client, netUrl) = pair
client.url(netUrl.toString)
}
}
}
import scala.language.implicitConversions
import PairMagnet._
val exampleURL = new java.net.URL(url)
val response = WS.url(ws -> exampleURL).get()
デフォルトでは、設定は application.conf
で行われますが、設定から直接ビルダーを構築することも可能です。
import com.typesafe.config.ConfigFactory
import play.api._
import play.api.libs.ws._
import play.api.libs.ws.ning._
val configuration = Configuration.reference ++ Configuration(ConfigFactory.parseString(
"""
|ws.followRedirects = true
""".stripMargin))
// If running in Play, environment should be injected
val environment = Environment(new File("."), this.getClass.getClassLoader, Mode.Prod)
val parser = new WSConfigParser(configuration, environment)
val config = new NingWSClientConfig(wsClientConfig = parser.parse())
val builder = new NingAsyncHttpClientConfigBuilder(config)
内部の 非同期クライアント を使用することも可能です。
import com.ning.http.client.AsyncHttpClient
val client: AsyncHttpClient = ws.underlying
重要な事柄が 2 点あります。WS はクライアントへのアクセス要求時に 2 つの制限があります。
WS
はマルチパートのフォームのアップロードを直接サポートしていません。内部的なクライアントの RequestBuilder.addBodyPart を使用してください。WS
はストリームによるボディのアップロードをサポートしていません。この場合、AsyncHttpClient によって提供されるFeedableBodyGenerator
を使用してください。
§WS の設定
WS クライアントの設定は、 application.conf
にある以下のプロパティで行います。
play.ws.followRedirects
: 301、および、302 でのリダイレクトにクライアントが従うかを設定します。 (デフォルトは true)play.ws.useProxyProperties
: システムのHTTPプロキシ設定(http.proxyHost, http.proxyPort)を使用するか設定します。 (デフォルトは true)play.ws.useragent
: User-Agent ヘッダーフィールドを設定します。play.ws.compressionEnabled
: このプロパティが true の場合 gzip/deflater によるエンコーディングを行います。 (デフォルトは false).
§SSL を用いた WS の設定
HTTP over SSL/TLS (HTTPS) を使用するための WS の設定については、 WS SSLの設定 を参照してください。
§タイムアウトの設定
WS には3種類のタイムアウト設定があります。タイムアウトになると、WS リクエストが中断します。
play.ws.timeout.connection
: リモートホストとの接続を行う最大の時間です。 ( デフォルトは 120秒 )play.ws.timeout.idle
: アイドル状態(コネクションは確立したが、データを待っている状態)を保持する最大の時間です。 ( デフォルトは 120秒 )play.ws.timeout.request
: リクエストにかかる全体の時間です(リモートホストがデータを送信中であっても、中断する可能性があります)。 ( デフォルトは 120秒 )
リクエストのタイムアウトは withRequestTimeout()
を使用した接続において上書き可能です。 (“リクエストの作成”の節を参照してください。)
§AsyncHttpClientConfig の設定
内部のAsyncHttpClientConfig にて、以下のような高度な設定を行うことができます。
詳しくはAsyncHttpClientConfig のドキュメントを参照してください。
play.ws.ning.allowPoolingConnection
play.ws.ning.allowSslConnectionPool
play.ws.ning.ioThreadMultiplier
play.ws.ning.maxConnectionsPerHost
play.ws.ning.maxConnectionsTotal
play.ws.ning.maxConnectionLifeTime
play.ws.ning.idleConnectionInPoolTimeout
play.ws.ning.webSocketIdleTimeout
play.ws.ning.maxNumberOfRedirects
play.ws.ning.maxRequestRetry
play.ws.ning.disableUrlEncoding
Next: OpenID サーバへの接続
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。