§使用 Play WS 调用 REST API
有时我们希望从 Play 应用程序内部调用其他 HTTP 服务。Play 通过其 WS(“WebService”)库 支持此功能,该库提供了一种进行异步 HTTP 调用的方法。
使用 WS API 包含两个重要部分:发出请求和处理响应。我们将首先讨论如何发出 GET 和 POST HTTP 请求,然后展示如何处理来自 WS 库的响应。最后,我们将讨论一些常见用例。
注意:在 Play 2.6 中,Play WS 已拆分为两个部分,一个底层的独立客户端,它不依赖于 Play,另一个是基于它的包装器,它使用 Play 特定的类。此外,Play WS 中现在使用 AsyncHttpClient 和 Netty 的阴影版本来最大程度地减少库冲突,主要目的是让 Play 的 HTTP 引擎可以使用不同版本的 Netty。有关更多信息,请参阅 2.6 迁移指南。
§将 WS 添加到项目
要使用 WS,首先将 javaWs 添加到您的 build.sbt 文件中
libraryDependencies ++= Seq(
javaWs
)§在 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 play.libs.ws.*;
import play.mvc.*;
public class MyClient implements WSBodyReadables, WSBodyWritables {
private final WSClient ws;
@Inject
public MyClient(WSClient ws) {
this.ws = ws;
}
// ...
}
要构建 HTTP 请求,您需要从 ws.url() 开始,以指定 URL。
WSRequest request = ws.url("http://example.com");
这将返回一个 WSRequest,您可以使用它来指定各种 HTTP 选项,例如设置标头。您可以将调用链接在一起以构建复杂的请求。
WSRequest complexRequest =
request
.addHeader("headerKey", "headerValue")
.setRequestTimeout(Duration.of(1000, ChronoUnit.MILLIS))
.addQueryParameter("paramKey", "paramValue");
最后,您需要调用一个与您要使用的 HTTP 方法相对应的方法。这将结束链,并在 WSRequest 中使用构建的请求中定义的所有选项。
CompletionStage<? extends WSResponse> responsePromise = complexRequest.get();
这将返回一个 CompletionStage<WSResponse>,其中 WSResponse 包含从服务器返回的数据。
Java 1.8 使用
CompletionStage来管理异步代码,Java WS API 很大程度上依赖于将CompletionStage与不同的方法组合在一起。如果您一直在使用早期版本的 Play,该版本使用F.Promise,那么 迁移指南中的 CompletionStage 部分 将非常有用。如果您正在执行任何阻塞工作,包括任何类型的 DNS 工作,例如调用
java.util.URL.equals(),那么您应该使用自定义执行上下文,如 线程池 中所述,最好通过CustomExecutionContext。您应该调整池的大小,以留出足够大的安全裕量来应对故障。如果您正在调用 不可靠的网络,请考虑使用
Futures.timeout和 断路器,例如 Failsafe。
§带有身份验证的请求
如果您需要使用 HTTP 身份验证,您可以在构建器中指定它,使用用户名、密码和 WSAuthScheme。WSAuthScheme 的选项包括 BASIC、DIGEST、KERBEROS、NTLM 和 SPNEGO。
ws.url(url).setAuth("user", "password", WSAuthScheme.BASIC).get();§带有重定向跟随的请求
如果 HTTP 调用导致 302 或 301 重定向,您可以自动跟随重定向,而无需进行另一次调用。
ws.url(url).setFollowRedirects(true).get();§带有查询参数的请求
您可以为请求指定查询参数。
ws.url(url).addQueryParameter("paramKey", "paramValue");§带有附加标头的请求
ws.url(url).addHeader("headerKey", "headerValue").get();
例如,如果您以特定格式发送纯文本,您可能需要明确定义内容类型。
ws.url(url).addHeader("Content-Type", "application/json").post(jsonString);
// OR
ws.url(url).setContentType("application/json").post(jsonString);§带有 Cookie 的请求
您可以使用 WSCookieBuilder 为请求指定 Cookie。
ws.url(url)
.addCookies(new WSCookieBuilder().setName("headerKey").setValue("headerValue").build())
.get();§带有超时的请求
如果您希望指定请求超时,可以使用 setRequestTimeout 以毫秒为单位设置值。可以使用 Duration.ofMillis(Long.MAX_VALUE) 设置无限超时。
ws.url(url).setRequestTimeout(Duration.of(1000, ChronoUnit.MILLIS)).get();§提交表单数据
要发布 url-form-encoded 数据,您可以设置适当的标头和格式化的数据,内容类型为“application/x-www-form-urlencoded”。
ws.url(url)
.setContentType("application/x-www-form-urlencoded")
.post("key1=value1&key2=value2");§提交 multipart/form-data
发布 multipart/form-data 的最简单方法是使用 Source<Http.MultipartFormData.Part<Source<ByteString>, ?>, ?>
import play.libs.ws.ahc.AhcCurlRequestLogger;
import play.mvc.Http.MultipartFormData.*;
ws.url(url).post(Source.single(new DataPart("hello", "world")));
要将文件作为 multipart 表单数据的一部分上传,您需要将 Http.MultipartFormData.FilePart<Source<ByteString>, ?> 传递给 Source
Source<ByteString, ?> file = FileIO.fromPath(Paths.get("hello.txt"));
FilePart<Source<ByteString, ?>> fp = new FilePart<>("hello", "hello.txt", "text/plain", file);
DataPart dp = new DataPart("key", "value");
ws.url(url).post(Source.from(Arrays.asList(fp, dp)));§提交 JSON 数据
发布 JSON 数据的最简单方法是使用 Play 的 JSON 支持,使用 play.libs.Json
import com.fasterxml.jackson.databind.JsonNode;
import play.libs.Json;
JsonNode json = Json.newObject().put("key1", "value1").put("key2", "value2");
ws.url(url).post(json);
您也可以传入自定义 ObjectMapper
ObjectMapper objectMapper = createCustomObjectMapper();
ws.url(url).post(body(json, objectMapper));§提交 XML 数据
发布 XML 数据的最简单方法是使用 Play 的 XML 支持,使用 play.libs.XML
Document xml = play.libs.XML.fromString("<document></document>");
ws.url(url).post(xml);§提交流数据
也可以使用 Pekko Streams 在请求正文中流式传输数据。
以下示例展示了如何将大型图像流式传输到另一个端点以进行进一步处理
CompletionStage<WSResponse> wsResponse = ws.url(url).setBody(body(largeImage)).execute("PUT");
上面代码片段中的 largeImage 是一个 Source<ByteString, ?>。
§请求过滤器
您可以通过添加请求过滤器对 WSRequest 进行额外处理。请求过滤器通过扩展 play.libs.ws.WSRequestFilter 接口添加,然后使用 request.setRequestFilter(filter) 将其添加到请求中。
public CompletionStage<Result> index() {
WSRequestFilter filter =
executor ->
request -> {
logger.debug("url = " + request.getUrl());
return executor.apply(request);
};
return ws.url(feedUrl)
.setRequestFilter(filter)
.get()
.thenApply(
(WSResponse r) -> {
String title = r.getBody(json()).findPath("title").asText();
return ok("Feed title: " + title);
});
}
在 play.libs.ws.ahc.AhcCurlRequestLogger 中添加了一个示例请求过滤器,它以 cURL 格式将请求记录到 SLF4J。
ws.url("https://play.java.net.cn")
.setRequestFilter(new AhcCurlRequestLogger())
.addHeader("Header-Name", "Header value")
.get();
将输出
curl \
--verbose \
--request GET \
--header 'Header-Key: Header value' \
'https://play.java.net.cn'
§处理响应
通过对 CompletionStage 应用 thenApply 和 thenCompose 等转换来处理 WSResponse。
§将响应处理为 JSON
您可以通过调用 r.getBody(json()) 将响应处理为 JsonNode,使用 play.libs.ws.WSBodyReadables.json() 中的默认方法。
// implements WSBodyReadables or use WSBodyReadables.instance.json()
CompletionStage<JsonNode> jsonPromise = ws.url(url).get().thenApply(r -> r.getBody(json()));§将响应处理为 XML
类似地,您可以通过调用 r.getBody(xml()) 将响应处理为 XML,使用 play.libs.ws.WSBodyReadables.xml() 中的默认方法。
// implements WSBodyReadables or use WSBodyReadables.instance.xml()
CompletionStage<Document> documentPromise =
ws.url(url).get().thenApply(r -> r.getBody(xml()));§处理大型响应
调用 get()、post() 或 execute() 将导致响应主体在响应可用之前加载到内存中。当您下载大型多吉字节文件时,这可能会导致意外的垃圾回收甚至内存不足错误。
您可以使用 Pekko Streams Sink 增量地使用响应主体。WSRequest 上的 stream() 方法返回一个 CompletionStage<WSResponse>,其中 WSResponse 包含一个 getBodyAsStream() 方法,该方法提供一个 Source<ByteString, ?>。
注意:在 2.5.x 中,
StreamedResponse是对request.stream()调用的响应。在 2.6.x 中,返回标准的WSResponse,并且应使用getBodyAsSource()方法返回 Source。
任何想要利用 WS 流功能的控制器或组件都需要添加以下导入和依赖项
import javax.inject.Inject;
import org.apache.pekko.stream.Materializer;
import org.apache.pekko.stream.javadsl.*;
import play.libs.ws.*;
import play.mvc.*;
public class MyController extends Controller {
@Inject WSClient ws;
@Inject Materializer materializer;
// ...
}
这是一个使用折叠 Sink 来计算响应返回的字节数的简单示例
// Make the request
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<Long> bytesReturned =
futureResponse.thenCompose(
res -> {
Source<ByteString, ?> responseBody = res.getBodyAsSource();
// Count the number of bytes returned
Sink<ByteString, CompletionStage<Long>> bytesSum =
Sink.fold(0L, (total, bytes) -> total + bytes.length());
return responseBody.runWith(bytesSum, materializer);
});
或者,您也可以将主体流到另一个位置。例如,一个文件
File file = java.nio.file.Files.createTempFile("stream-to-file-", ".txt").toFile();
OutputStream outputStream = java.nio.file.Files.newOutputStream(file.toPath());
// Make the request
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<File> downloadedFile =
futureResponse.thenCompose(
res -> {
Source<ByteString, ?> responseBody = res.getBodyAsSource();
// The sink that writes to the output stream
Sink<ByteString, CompletionStage<org.apache.pekko.Done>> outputWriter =
Sink.<ByteString>foreach(bytes -> outputStream.write(bytes.toArray()));
// materialize and run the stream
CompletionStage<File> result =
responseBody
.runWith(outputWriter, materializer)
.whenComplete(
(value, error) -> {
// Close the output stream whether there was an error or not
try {
outputStream.close();
} catch (IOException e) {
}
})
.thenApply(v -> file);
return result;
});
响应主体的另一个常见目的地是将它们从控制器的 Action 流回
// Make the request
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<Result> result =
futureResponse.thenApply(
response -> {
Source<ByteString, ?> body = response.getBodyAsSource();
// Check that the response was successful
if (response.getStatus() == 200) {
// Get the content type
String contentType =
Optional.ofNullable(response.getHeaders().get("Content-Type"))
.map(contentTypes -> contentTypes.get(0))
.orElse("application/octet-stream");
// If there's a content length, send that, otherwise return the body chunked
Optional<String> contentLength =
Optional.ofNullable(response.getHeaders().get("Content-Length"))
.map(contentLengths -> contentLengths.get(0));
if (contentLength.isPresent()) {
return ok().sendEntity(
new HttpEntity.Streamed(
body,
Optional.of(Long.parseLong(contentLength.get())),
Optional.of(contentType)));
} else {
return ok().chunked(body).as(contentType);
}
} else {
return new Result(Status.BAD_GATEWAY);
}
});
您可能已经注意到,在调用 stream() 之前,我们需要通过在请求上调用 setMethod(String) 来设置要使用的 HTTP 方法。以下是一个使用 PUT 而不是 GET 的另一个示例
CompletionStage<WSResponse> futureResponse =
ws.url(url).setMethod("PUT").setBody(body("some body")).stream();
当然,您可以使用任何其他有效的 HTTP 动词。
§常见模式和用例
§链接 WS 调用
您可以使用 thenCompose 来链接 WS 调用。
final CompletionStage<WSResponse> responseThreePromise =
ws.url(urlOne)
.get()
.thenCompose(responseOne -> ws.url(responseOne.getBody()).get())
.thenCompose(responseTwo -> ws.url(responseTwo.getBody()).get());§异常恢复
如果您想从调用中的异常中恢复,可以使用 handle 或 exceptionally 来替换响应。
CompletionStage<WSResponse> responsePromise = ws.url("http://example.com").get();
responsePromise.handle(
(result, error) -> {
if (error != null) {
return ws.url("http://backup.example.com").get();
} else {
return CompletableFuture.completedFuture(result);
}
});§在控制器中使用
您可以将 CompletionStage<WSResponse> 映射到 CompletionStage<Result>,该结果可以直接由 Play 服务器处理,使用 处理异步结果 中定义的异步操作模式。
public CompletionStage<Result> index() {
return ws.url(feedUrl)
.get()
.thenApply(response -> ok("Feed title: " + response.asJson().findPath("title").asText()));
}§使用 WSClient 与 Futures 超时
如果 WS 调用的链在一段时间内未完成,则将结果包装在超时块中可能很有用,如果链在一段时间内未完成,该块将返回一个失败的 Future - 这比使用 withRequestTimeout 更通用,withRequestTimeout 只适用于单个请求。
最好的方法是使用 Play 的 非阻塞超时功能,使用 Futures.timeout 和 CustomExecutionContext 来确保某种类型的解析
public CompletionStage<Result> index() {
CompletionStage<Result> f =
futures.timeout(
ws.url("http://playframework.com")
.get()
.thenApplyAsync(
result -> {
try {
Thread.sleep(10000L);
return Results.ok();
} catch (InterruptedException e) {
return Results.status(SERVICE_UNAVAILABLE);
}
},
customExecutionContext),
1L,
TimeUnit.SECONDS);
return f.handleAsync(
(result, e) -> {
if (e != null) {
if (e instanceof CompletionException) {
Throwable completionException = e.getCause();
if (completionException instanceof TimeoutException) {
return Results.status(SERVICE_UNAVAILABLE, "Service has timed out");
} else {
return internalServerError(e.getMessage());
}
} else {
logger.error("Unknown exception " + e.getMessage(), e);
return internalServerError(e.getMessage());
}
} else {
return result;
}
});
}§直接创建 WSClient
我们建议您使用上面描述的 依赖注入 获取 WSClient 实例。通过依赖注入创建的 WSClient 实例更易于使用,因为它们在应用程序启动时自动创建,并在应用程序停止时清理。
但是,如果您选择,您可以直接从代码实例化 WSClient,并将其用于发出请求或配置底层 AsyncHttpClient 选项。
注意:如果您手动创建
WSClient,则在完成操作后,您**必须**调用client.close()来清理它。每个客户端都会创建自己的线程池。如果您没有关闭客户端或创建了太多客户端,那么您将耗尽线程或文件句柄——您将遇到诸如“无法创建新的本机线程”或“打开的文件过多”之类的错误,因为底层资源被消耗掉了。
以下是如何自行创建 WSClient 实例的示例
import org.apache.pekko.stream.Materializer;
import org.apache.pekko.stream.javadsl.*;
import org.apache.pekko.util.ByteString;
import play.mvc.Results;
// Set up the client config (you can also use a parser here):
// play.api.Configuration configuration = ... // injection
// play.Environment environment = ... // injection
WSClient customWSClient =
play.libs.ws.ahc.AhcWSClient.create(
play.libs.ws.ahc.AhcWSClientConfigFactory.forConfig(
configuration.underlying(), environment.classLoader()),
null, // no HTTP caching
materializer);
您也可以使用 play.test.WSTestClient.newClient 在功能测试中创建 WSClient 实例。有关更多详细信息,请参阅 JavaTestingWebServiceClients。
或者,您可以完全独立地运行 WSClient,而无需涉及正在运行的 Play 应用程序或配置。
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.stream.Materializer;
import org.apache.pekko.stream.SystemMaterializer;
import play.shaded.ahc.org.asynchttpclient.*;
import play.libs.ws.*;
import play.libs.ws.ahc.*;
import org.junit.Test;
// Set up Pekko
String name = "wsclient";
ActorSystem system = ActorSystem.create(name);
Materializer materializer = SystemMaterializer.get(system).materializer();
// Set up AsyncHttpClient directly from config
AsyncHttpClientConfig asyncHttpClientConfig =
new DefaultAsyncHttpClientConfig.Builder()
.setMaxRequestRetry(0)
.setShutdownQuietPeriod(0)
.setShutdownTimeout(0)
.build();
AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig);
// Set up WSClient instance directly from asynchttpclient.
WSClient client = new AhcWSClient(asyncHttpClient, materializer);
// Call out to a remote system and then and close the client and pekko.
client
.url("http://www.google.com")
.get()
.whenComplete(
(r, e) -> {
Optional.ofNullable(r)
.ifPresent(
response -> {
String statusText = response.getStatusText();
System.out.println("Got a response " + statusText);
});
})
.thenRun(
() -> {
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
})
.thenRun(system::terminate);
这在存在从配置中无法访问的特定 HTTP 客户端选项的情况下非常有用。
如果您想独立运行 WSClient,但仍想使用 配置(包括 SSL),您可以使用类似于以下的配置解析器
// Set up Pekko
String name = "wsclient";
ActorSystem system = ActorSystem.create(name);
Materializer materializer = Materializer.matFromSystem(system);
// Read in config file from application.conf
Config conf = ConfigFactory.load();
WSConfigParser parser = new WSConfigParser(conf, ClassLoader.getSystemClassLoader());
AhcWSClientConfig clientConf = AhcWSClientConfigFactory.forClientConfig(parser.parse());
// Start up asynchttpclient
final DefaultAsyncHttpClientConfig asyncHttpClientConfig =
new AhcConfigBuilder(clientConf).configure().build();
final DefaultAsyncHttpClient asyncHttpClient =
new DefaultAsyncHttpClient(asyncHttpClientConfig);
// Create a new WSClient, and then close the client.
WSClient client = new AhcWSClient(asyncHttpClient, materializer);
client.close();
system.terminate();
同样,一旦您完成了自定义客户端工作,您**必须**关闭客户端,否则您将泄漏线程。
try {
customWSClient.close();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
理想情况下,您应该只在知道所有请求都已完成之后才关闭客户端。您不应该使用 try-with-resources 自动关闭 WSClient 实例,因为 WSClient 逻辑是异步的,而 try-with-resources 只支持其主体中的同步代码。
§自定义 BodyReadables 和 BodyWritables
Play WS 提供了丰富的类型支持,用于处理请求和响应的 body,通过 play.libs.ws.WSBodyWritables 类,可以将 WSRequest 的 body 中的 JsonNode 或 XML 等输入转换为 ByteString 或 Source<ByteString, ?>;通过 play.libs.ws.WSBodyReadables 类,可以将 WSResponse 的 body 从 ByteString 或 Source[ByteString, _] 中读取并返回相应的类型,例如 JsValue 或 XML。默认方法可以通过 WSRequest 和 WSResponse 使用,但也可以使用自定义类型,例如 response.getBody(myReadable()) 和 request.post(myWritable(data))。这在使用自定义库时特别有用,例如,如果希望通过 STaX API 流式传输 XML。
§创建自定义 Readable
可以通过解析响应来创建自定义 Readable。
public interface URLBodyReadables {
default BodyReadable<java.net.URL> url() {
return response -> {
try {
String s = response.getBody();
return java.net.URI.create(s).toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
};
}
}§创建自定义 BodyWritable
可以使用 InMemoryBodyWritable 创建自定义 BodyWritable,用于向请求发送数据。如果需要使用流式传输,可以使用 SourceBodyWritable。
public interface URLBodyWritables {
default InMemoryBodyWritable body(java.net.URL url) {
try {
String s = url.toURI().toString();
ByteString byteString = ByteString.fromString(s);
return new InMemoryBodyWritable(byteString, "text/plain");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}§独立的 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 迁移指南。
§访问 AsyncHttpClient
可以从 WSClient 中获取对底层阴影的 AsyncHttpClient 的访问权限。
play.shaded.ahc.org.asynchttpclient.AsyncHttpClient underlyingClient =
(play.shaded.ahc.org.asynchttpclient.AsyncHttpClient) ws.getUnderlying();§配置 WS
在 application.conf 中使用以下属性来配置 WS 客户端
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)。
§超时
WS 中有 3 种不同的超时。达到超时会导致 WS 请求中断。
play.ws.timeout.connection: 连接到远程主机时的最大等待时间 (默认值为 120 秒)。play.ws.timeout.idle: 请求保持空闲的最大时间(连接已建立但正在等待更多数据)(默认值为 **120 秒**)。play.ws.timeout.request: 接受请求持续的最大时间(即使远程主机仍在发送数据,也会被中断)(默认值为 **120 秒**)。
可以使用 setTimeout() 为特定连接覆盖请求超时(请参阅“发出请求”部分)。
§使用 SSL 配置 WS
要配置 WS 以使用 HTTP over SSL/TLS (HTTPS),请参阅 配置 WS SSL。
§使用缓存配置 WS
要配置 WS 以使用 HTTP 缓存,请参阅 配置 WS 缓存。
§配置 AsyncClientConfig
可以在底层的 AsyncHttpClientConfig 上配置以下高级设置。
有关更多信息,请参阅 AsyncHttpClientConfig 文档。
play.ws.ahc.keepAliveplay.ws.ahc.maxConnectionsPerHostplay.ws.ahc.maxConnectionsTotalplay.ws.ahc.maxConnectionLifetimeplay.ws.ahc.idleConnectionInPoolTimeoutplay.ws.ahc.maxNumberOfRedirectsplay.ws.ahc.maxRequestRetryplay.ws.ahc.disableUrlEncoding
下一步:连接到 OpenID 服务
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。