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上找到。