Deprecate ListenableFuture in favor of CompletableFuture

This commit deprecates ListenableFuture in favor of CompletableFuture.
ListenableFuture was introduced in Spring Framework 4.0, when
CompletableFuture was not yet available. Spring now requires JDK 17, so
having our own type no longer seems necessary.

Major changes in this commit include:
- Deprecation of ListenableFuture and related types
  (ListenableFutureCallback, SettableListenableFuture, etc.)
- Deprecation of AsyncListenableTaskExecutor in favor of default methods
  in AsyncTaskExecutor (submitCompletable).
- AsyncHandlerMethodReturnValueHandler now has toCompletableFuture
  instead of toListenableFuture.
- WebSocketClient now has execute methods, which do the same as
  doHandshake, but return CompletableFutures (cf. the reactive
  WebSocketClient).

All other changes
- add an overloaded method that takes a CompletableFuture parameter
  instead of ListenableFuture, and/or
- add a method with a 'Async' suffix that returns a CompletableFuture
  instead of a ListenableFuture (connectAsync, sendAsync).

Closes gh-27780
This commit is contained in:
Arjen Poutsma
2022-03-17 12:18:00 +01:00
parent 735051bf7d
commit 2aa74c9121
74 changed files with 1148 additions and 380 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,13 @@ package org.springframework.scheduling.concurrent;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -33,6 +36,7 @@ import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.lang.Nullable;
import org.springframework.util.concurrent.ListenableFuture;
import static org.assertj.core.api.Assertions.assertThat;
@@ -135,6 +139,21 @@ abstract class AbstractSchedulingTaskExecutorTests {
assertThreadNamePrefix(task);
}
@Test
void submitCompletableRunnable() throws Exception {
TestTask task = new TestTask(this.testName, 1);
// Act
CompletableFuture<Void> future = executor.submitCompletable(task);
future.whenComplete(this::storeOutcome);
// Assert
Awaitility.await()
.atMost(1, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.until(future::isDone);
assertThat(outcome).isNull();
assertThreadNamePrefix(task);
}
@Test
void submitFailingListenableRunnable() throws Exception {
TestTask task = new TestTask(this.testName, 0);
@@ -149,6 +168,20 @@ abstract class AbstractSchedulingTaskExecutorTests {
assertThat(outcome.getClass()).isSameAs(RuntimeException.class);
}
@Test
void submitFailingCompletableRunnable() throws Exception {
TestTask task = new TestTask(this.testName, 0);
CompletableFuture<?> future = executor.submitCompletable(task);
future.whenComplete(this::storeOutcome);
Awaitility.await()
.dontCatchUncaughtExceptions()
.atMost(1, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.until(() -> future.isDone() && outcome != null);
assertThat(outcome.getClass()).isSameAs(CompletionException.class);
}
@Test
void submitListenableRunnableWithGetAfterShutdown() throws Exception {
ListenableFuture<?> future1 = executor.submitListenable(new TestTask(this.testName, -1));
@@ -169,6 +202,26 @@ abstract class AbstractSchedulingTaskExecutorTests {
future2.get(1000, TimeUnit.MILLISECONDS)));
}
@Test
void submitCompletableRunnableWithGetAfterShutdown() throws Exception {
CompletableFuture<?> future1 = executor.submitCompletable(new TestTask(this.testName, -1));
CompletableFuture<?> future2 = executor.submitCompletable(new TestTask(this.testName, -1));
shutdownExecutor();
try {
future1.get(1000, TimeUnit.MILLISECONDS);
}
catch (Exception ex) {
/* ignore */
}
Awaitility.await()
.atMost(4, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.untilAsserted(() ->
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() ->
future2.get(1000, TimeUnit.MILLISECONDS)));
}
@Test
void submitCallable() throws Exception {
TestCallable task = new TestCallable(this.testName, 1);
@@ -246,6 +299,57 @@ abstract class AbstractSchedulingTaskExecutorTests {
});
}
@Test
void submitCompletableCallable() throws Exception {
TestCallable task = new TestCallable(this.testName, 1);
// Act
CompletableFuture<String> future = this.executor.submitCompletable(task);
future.whenComplete(this::storeOutcome);
// Assert
Awaitility.await()
.atMost(1, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.until(() -> future.isDone() && outcome != null);
assertThat(outcome.toString().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix);
}
@Test
void submitFailingCompletableCallable() throws Exception {
TestCallable task = new TestCallable(this.testName, 0);
// Act
CompletableFuture<String> future = this.executor.submitCompletable(task);
future.whenComplete(this::storeOutcome);
// Assert
Awaitility.await()
.dontCatchUncaughtExceptions()
.atMost(1, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.until(() -> future.isDone() && outcome != null);
assertThat(outcome.getClass()).isSameAs(CompletionException.class);
}
@Test
void submitCompletableCallableWithGetAfterShutdown() throws Exception {
CompletableFuture<?> future1 = executor.submitCompletable(new TestCallable(this.testName, -1));
CompletableFuture<?> future2 = executor.submitCompletable(new TestCallable(this.testName, -1));
shutdownExecutor();
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> {
future1.get(1000, TimeUnit.MILLISECONDS);
future2.get(1000, TimeUnit.MILLISECONDS);
});
}
private void storeOutcome(@Nullable Object o, @Nullable Throwable t) {
if (o != null) {
this.outcome = o;
}
else if (t != null) {
this.outcome = t;
}
}
protected void assertThreadNamePrefix(TestTask task) {
assertThat(task.lastThread.getName().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix);