§使用 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 编码的表单数据,需要将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-ws 和 2.6 迁移指南。
§自定义 BodyReadables 和 BodyWritables
Play WS 提供了丰富的类型支持,用于以 play.api.libs.ws.WSBodyWritables
的形式处理主体,其中包含用于将输入(例如 JsValue
或 XML
)从 WSRequest
主体转换为 ByteString
或 Source[ByteString, _]
的类型类,以及 play.api.libs.ws.WSBodyReadables
,它聚合了从 ByteString
或 Source[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
您可以使用 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 服务
发现此文档中的错误?此页面的源代码可以在这里找到。在阅读文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。