1. 概述
当我们构建一个依赖其他服务的应用时,常常需要处理依赖服务响应过慢的情况。如果我们使用CompletableFuture
异步管理对依赖的服务调用,它的超时功能允许我们设置结果的最大等待时间。如果在指定时间内没有接收到预期的结果,我们可以采取行动,比如提供默认值,防止应用陷入漫长的进程。
本文将讨论在CompletableFuture
中管理超时的三种不同方式。
2. 超时管理
想象一个电子商务应用,它需要调用外部服务获取特殊产品优惠。我们可以使用带有超时设置的CompletableFuture
来保持响应性。如果服务响应不及时,这可能会抛出错误或提供默认值。
例如,假设我们要请求一个返回PRODUCT_OFFERS
的API,我们可以将其包装在一个CompletableFuture
中,以便处理超时:
private CompletableFuture<String> fetchProductData() {
return CompletableFuture.supplyAsync(() -> {
try {
URL url = new URL("http://localhost:8080/api/dummy");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
return response.toString();
} finally {
connection.disconnect();
}
} catch (IOException e) {
return "";
}
});
}
为了使用WireMock测试超时,可以根据WireMock使用指南轻松配置模拟服务器。假设在典型互联网连接下网页加载时间为1000毫秒,我们可以设置一个默认超时时间DEFAULT_TIMEOUT
:
private static final int DEFAULT_TIMEOUT = 1000; // 1 seconds
然后,我们将创建一个wireMockServer
,其响应体为PRODUCT_OFFERS
,并设置延迟5000毫秒或5秒,确保这个值超过DEFAULT_TIMEOUT
,以确保超时发生:
stubFor(get(urlEqualTo("/api/dummy"))
.willReturn(aResponse()
.withFixedDelay(5000) // must be > DEFAULT_TIMEOUT for a timeout to occur.
.withBody(PRODUCT_OFFERS)));
3. 使用completeOnTimeout()
completeOnTimeout()
方法如果任务在指定时间内未完成,会用默认值解决CompletableFuture
。
通过这种方法,我们可以设置当超时时返回的默认值<T>
。此方法返回被调用的CompletableFuture
本身。
在这个例子中,我们将默认值设为DEFAULT_PRODUCT
:
CompletableFuture<Integer> productDataFuture = fetchProductData();
productDataFuture.completeOnTimeout(DEFAULT_PRODUCT, DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertEquals(DEFAULT_PRODUCT, productDataFuture.get());
如果我们希望即使在请求失败或超时时,结果仍然有意义,那么这个方法是合适的。
例如,在电子商务场景中,当显示产品促销时,如果获取特殊促销产品数据失败或超时,系统将显示默认产品。
4. 使用orTimeout()
我们可以使用orTimeout()
来增强CompletableFuture
,在特定时间内未来未完成时添加超时处理行为。
这个方法返回被应用此方法的相同CompletableFuture
,如果超时则会抛出TimeoutException
。
然后,为了测试这个方法,我们应该使用assertThrows()
来证明异常被抛出:
CompletableFuture<Integer> productDataFuture = fetchProductData();
productDataFuture.orTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertThrows(ExecutionException.class, productDataFuture::get);
如果我们的优先级是响应性或耗时任务,并希望在超时时提供快速响应,那么这是个合适的策略。然而,需要妥善处理这些异常,因为这个方法明确地抛出异常,以保证性能。
此外,这种方法适用于各种场景,如管理网络连接、处理IO操作、处理实时数据和管理队列。
5. 使用completeExceptionally()
CompletableFuture
类的completeExceptionally()
方法允许我们用特定的异常完成未来状态。后续对结果检索方法(如get()
和join()
)的调用将抛出指定的异常。
此方法如果方法调用导致CompletableFuture
进入完成状态,将返回true
。否则,返回false
。
这里,我们将使用Java中的ScheduledExecutorService
接口,用于在特定时间和延迟安排任务执行。它提供了在并发环境中调度周期性任务、处理超时和管理错误的灵活性:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
//...
CompletableFuture<Integer> productDataFuture = fetchProductData();
executorService.schedule(() -> productDataFuture.completeExceptionally(
new TimeoutException("Timeout occurred")), DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
assertThrows(ExecutionException.class, productDataFuture::get);
如果我们需要处理TimeoutException
以及其他异常,或者想要定制化异常,这可能是合适的方法。我们通常使用这种方法处理数据验证失败、致命错误,或者当任务没有默认值时。
6. 对比:completeOnTimeout()
vs orTimeout()
vs completeExceptionally()
通过所有这些方法,我们可以在不同的场景中管理和控制CompletableFuture
的行为,特别是在处理需要定时和处理超时或错误的异步操作时。
让我们比较completeOnTimeout()
、orTimeout()
和completeExceptionally()
的优势和劣势:
方法
优势
劣势
completeOnTimeout()
允许替换长时间运行任务的默认结果
无需抛出异常
即可避免超时
不明确表示超时发生
orTimeout()
超时发生时明确生成TimeoutException
可以按特定方式处理超时
不提供替换默认结果的选项
completeExceptionally()
允许用自定义异常
明确标记结果
适合指示异步操作的失败
用途更广泛,用于超时管理
7. 总结
在这篇文章中,我们探讨了在CompletableFuture
中处理异步过程超时的三种不同方法。
选择我们的方法时,应考虑管理长运行任务的需求。我们需要决定是使用默认值,还是使用特定异常表示异步操作的超时。
一如既往,完整的源代码可在GitHub上找到。