第十七章 web服务¶
有时候我们需要调用其它站点的HTTP服务,Play通过WS库来支持这些异步调用。
调用WS API包括两个部分,发起请求,处理响应。
发送请求¶
使用WS之前,需要在build.sbt中添加依赖:
libraryDependencies ++= Seq(
ws
)
接下来就可以在组件中通过声明WSClient的注入来调用WS服务。
import javax.inject.Inject
import scala.concurrent.Future
import scala.concurrent.duration._
import play.api.mvc._
import play.api.libs.ws._
import play.api.http.HttpEntity
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.stream.scaladsl._
import akka.util.ByteString
import scala.concurrent.ExecutionContext
class Application @Inject() (ws: WSClient) extends Controller {
}
使用 ws.url()
发送请求:
val request: WSRequest = ws.url(url)
返回一个 WSRequest
对象,可以给它指定各种HTTP选项,比如头部信息,可以通过链式调用来构建复杂的请求:
val complexRequest: WSRequest =
request.withHeaders("Accept" -> "application/json")
.withRequestTimeout(10000.millis)
.withQueryString("search" -> "play")
最后调用你想使用的HTTP方法结束请求:
val futureResponse: Future[WSResponse] = complexRequest.get()
请求结果以 Future[WSResponse]
形式返回, WSResponse
包含了服务端返回的数据。
带认证的请求¶
如果需要HTTP认证,可以在请求中指明认证方式以及相关参数,WS支持的认证模式包括以下几种:
- BASIC
- DIGEST
- KERBEROS
- NTLM
- SPNEGO
ws.url(url).withAuth(user, password, WSAuthScheme.BASIC).get()
带头部信息的请求¶
HTTP头部信息可以通过一系列的键值对元组来指明:
ws.url(url).withHeaders("headerKey" -> "headerValue").get()
例如,通过设置 Content-Type
来指定请求中发送的数据类型:
ws.url(url).withHeaders("Content-Type" -> "application/xml").post(xmlString)
设置超时时间¶
如果需要指定请求的超时时间,可以使用 withRequestTimeout``来设置, 如果要一直等待下去,可以使用 ``Duration.Inf
作为参数。
ws.url(url).withRequestTimeout(5000.millis).get()
提交表单数据¶
将表单数据以 Map[String, Seq[String]]]
的形式作为参数传递给 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]]
类型的数据传递给 post
方法:
ws.url(url).post(Source(FilePart("hello", "hello.txt", Option("text/plain"), FileIO.fromFile(tmpFile)) :: DataPart("key", "value") :: List()))
提交JSON数据¶
使用JSON库即可:
提交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)
流数据¶
WS还支持流数据,用于上传大型文件,如果数据库支持Reactive Streams,则可以使用流数据:
val wsResponse: Future[WSResponse] = ws.url(url)
.withBody(StreamedBody(largeImageFromDB)).execute("PUT")
上面l largeImageFormDB
的数据类型为 Source[ByteString, _]
。
过滤请求¶
还可以给WSRequest添加一个请求过滤器,请求过滤器通过继承特质 `` play.api.libs.ws.WSRequestFilter`` 来实现,然后使用 request.withRequestFilter(filter)
将它添加请求中.
WS提供了一个过滤器的实现,位于 play.api.libs.ws.ahc.AhcCurlRequestLogger
,它用于将请求的信息以SLF4J日志形式进行记录。
ws.url(s"http://localhost:$testServerPort")
.withRequestFilter(AhcCurlRequestLogger())
.withBody(Map("param1" -> Seq("value1")))
.put(Map("key" -> Seq("value")))
将输入以下日志:
curl \
--verbose \
--request PUT \
--header 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
--data 'key=value' \
'http://localhost:19001/
处理响应结果¶
响应结果包裹在 Future
中。
当 Future
中的计算任务完成是,需要指定一个隐式参数–执行上下文, ``Future``的回调将在该上线文的线程池中执行。我们可以通过依赖注入指定执行上下文
class PersonService @Inject()(implicit context: ExecutionContext) {
// ...
}
如果不适用依赖注入,也可以使用默认的执行上下文:
implicit val context = play.api.libs.concurrent.Execution.Implicits.defaultContext
下面的例子使用都使用如下定义的样式类进行序列化和反序列化:
case class Person(name: String, age: Int)
将响应解析为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 = 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
库允许我们逐步的读取响应数据,通过使用Akka的 Sink
。
WSRequest``的 ``stream()
方法返回 Future[StreamedResponse]
, 而 StreamResponse
是一个保存响应头部和响应体的容器。
// Make the request
val futureResponse: Future[StreamedResponse] =
ws.url(url).withMethod("GET").stream()
val bytesReturned: Future[Long] = futureResponse.flatMap {
res =>
// Count the number of bytes returned
res.body.runWith(Sink.fold[Long, ByteString](0L){ (total, bytes) =>
total + bytes.length
})
}
也可以将响应数据保存在文件中:
// Make the request
val futureResponse: Future[StreamedResponse] =
ws.url(url).withMethod("GET").stream()
val downloadedFile: Future[File] = futureResponse.flatMap {
res =>
val outputStream = new FileOutputStream(file)
// The sink that writes to the output stream
val sink = Sink.foreach[ByteString] { bytes =>
outputStream.write(bytes.toArray)
}
// materialize and run the stream
res.body.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 {
case StreamedResponse(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.sendEntity(HttpEntity.Streamed(body, Some(length.toLong), Some(contentType)))
case _ =>
Ok.chunked(body).as(contentType)
}
} else {
BadGateway
}
}
}
从上面我们可以注意到可以通过 withMethod
指定请求方法:
val futureResponse: Future[StreamedResponse] =
ws.url(url).withMethod("PUT").withBody("some body").stream()