§使用 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()
,那麼您應該使用 ThreadPools 中所述的自訂執行緒環境,最好透過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 表單編碼資料,您可以設定適當的標頭和格式化資料,其內容類型為「application/x-www-form-urlencoded」。
ws.url(url)
.setContentType("application/x-www-form-urlencoded")
.post("key1=value1&key2=value2");
§提交 multipart/form 資料
張貼 multipart/form 資料最簡單的方法是使用 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 資料
使用 Play 的 JSON 支援,使用 play.libs.Json
是發佈 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 資料
使用 Play 的 XML 支援,使用 play.libs.XML
是發佈 XML 資料最簡單的方法
Document xml = play.libs.XML.fromString("<document></document>");
ws.url(url).post(xml);
§提交串流資料
也可以使用 Pekko 串流 在請求主體中串流資料。
以下是顯示如何將大型圖片串流到不同端點以進行進一步處理的範例
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.dev.org.tw")
.setRequestFilter(new AhcCurlRequestLogger())
.addHeader("Header-Name", "Header value")
.get();
會輸出
curl \
--verbose \
--request GET \
--header 'Header-Key: Header value' \
'https://play.dev.org.tw'
§處理回應
透過對 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()
會在回應可用之前將回應主體載入記憶體中。當您下載大型、多 GB 的檔案時,這可能會導致不必要的垃圾回收,甚至記憶體不足的錯誤。
您可以使用 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 Timeout 搭配使用
如果一連串的 WS 呼叫無法及時完成,將結果包裝在 timeout 區塊中可能會很有用,如果該串呼叫無法及時完成,則會傳回失敗的 Future – 這比使用 withRequestTimeout
更通用,後者僅適用於單一請求。
執行此操作的最佳方式是使用 Play 的 非阻斷 timeout 功能,使用 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 提供豐富的類型支援,用於以 play.libs.ws.WSBodyWritables
形式呈現的本文,其中包含用於將輸入(例如 JsonNode
或 XML
)轉換為 WSRequest
本文中的 ByteString
或 Source<ByteString, ?>
的方法,以及 play.libs.ws.WSBodyReadables
,其中包含用於從 ByteString
或 Source[ByteString, _]
讀取 WSResponse
本文並傳回適當類型(例如 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
,如下所示,為要求建立自訂本文可寫入。若要指定具有串流的自訂本文可寫入,請使用 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.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 服務
在此文件發現錯誤?此頁面的原始程式碼可在此處找到 這裡。在閱讀 文件指南 後,請隨時貢獻一個拉取請求。有問題或建議要分享?前往 我們的社群論壇 與社群展開對話。