From 2aa74c912143273145b64b3d1c88eb0c73ea521a Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Mar 2022 12:18:00 +0100 Subject: [PATCH] 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 --- .../AsyncExecutionAspectSupport.java | 16 +-- .../quartz/SimpleThreadPoolTaskExecutor.java | 1 + .../ApplicationListenerMethodAdapter.java | 3 +- .../scheduling/annotation/AsyncResult.java | 4 +- .../concurrent/ConcurrentTaskExecutor.java | 1 + .../concurrent/ThreadPoolTaskExecutor.java | 2 +- .../concurrent/ThreadPoolTaskScheduler.java | 2 +- .../AbstractSchedulingTaskExecutorTests.java | 106 +++++++++++++++- .../task/AsyncListenableTaskExecutor.java | 10 +- .../core/task/AsyncTaskExecutor.java | 28 +++++ .../core/task/SimpleAsyncTaskExecutor.java | 2 +- .../task/support/TaskExecutorAdapter.java | 1 + .../CompletableToListenableFutureAdapter.java | 4 +- .../util/concurrent/FailureCallback.java | 7 +- .../util/concurrent/FutureAdapter.java | 4 +- .../util/concurrent/FutureUtils.java | 82 ++++++++++++ .../util/concurrent/ListenableFuture.java | 11 +- .../concurrent/ListenableFutureAdapter.java | 5 +- .../concurrent/ListenableFutureCallback.java | 7 +- .../ListenableFutureCallbackRegistry.java | 2 + .../util/concurrent/ListenableFutureTask.java | 4 +- .../MonoToListenableFutureAdapter.java | 4 +- .../concurrent/SettableListenableFuture.java | 4 +- .../util/concurrent/SuccessCallback.java | 7 +- .../util/concurrent/FutureUtilsTests.java | 116 +++++++++++++++++ .../AbstractMethodMessageHandler.java | 32 ++--- .../AsyncHandlerMethodReturnValueHandler.java | 31 ++++- .../CompletableFutureReturnValueHandler.java | 10 +- ...dlerMethodReturnValueHandlerComposite.java | 9 +- .../ListenableFutureReturnValueHandler.java | 10 +- .../ReactiveReturnValueHandler.java | 11 +- .../SimpAnnotationMethodMessageHandler.java | 3 +- .../stomp/ConnectionHandlingStompSession.java | 17 ++- .../simp/stomp/DefaultStompSession.java | 31 ++--- .../stomp/ReactorNettyTcpStompClient.java | 39 +++++- .../stomp/StompBrokerRelayMessageHandler.java | 55 ++++---- .../messaging/tcp/TcpConnection.java | 19 ++- .../messaging/tcp/TcpOperations.java | 50 +++++++- .../tcp/reactor/ReactorNettyTcpClient.java | 29 ++--- .../reactor/ReactorNettyTcpConnection.java | 13 +- ...mpAnnotationMethodMessageHandlerTests.java | 3 +- .../simp/stomp/DefaultStompSessionTests.java | 19 ++- .../ReactorNettyTcpStompClientTests.java | 8 +- .../StompBrokerRelayMessageHandlerTests.java | 18 ++- .../function/DefaultAsyncServerResponse.java | 5 +- .../DefaultEntityResponseBuilder.java | 5 +- ...eferredResultMethodReturnValueHandler.java | 9 +- ...DeferredResultReturnValueHandlerTests.java | 4 +- .../client/AbstractWebSocketClient.java | 33 ++++- .../web/socket/client/WebSocketClient.java | 56 ++++++++- .../client/WebSocketConnectionManager.java | 19 ++- .../client/jetty/JettyWebSocketClient.java | 29 ++--- .../standard/StandardWebSocketClient.java | 21 ++-- .../messaging/WebSocketStompClient.java | 117 +++++++++++++++--- .../client/AbstractClientSockJsSession.java | 18 ++- .../sockjs/client/AbstractXhrTransport.java | 17 ++- .../client/DefaultTransportRequest.java | 109 +++++++++++++++- .../sockjs/client/JettyXhrTransport.java | 12 +- .../client/RestTemplateXhrTransport.java | 8 +- .../socket/sockjs/client/SockJsClient.java | 13 +- .../web/socket/sockjs/client/Transport.java | 19 ++- .../sockjs/client/UndertowXhrTransport.java | 18 +-- .../client/WebSocketClientSockJsSession.java | 15 ++- .../sockjs/client/WebSocketTransport.java | 30 ++--- .../sockjs/client/XhrClientSockJsSession.java | 22 +++- .../AbstractWebSocketIntegrationTests.java | 6 +- .../WebSocketConnectionManagerTests.java | 13 +- .../StompWebSocketIntegrationTests.java | 14 +-- .../messaging/WebSocketStompClientTests.java | 18 +-- .../client/ClientSockJsSessionTests.java | 8 +- .../client/DefaultTransportRequestTests.java | 20 +-- .../sockjs/client/SockJsClientTests.java | 4 +- .../socket/sockjs/client/TestTransport.java | 20 +-- .../sockjs/client/XhrTransportTests.java | 6 +- 74 files changed, 1148 insertions(+), 380 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/util/concurrent/FutureUtils.java create mode 100644 spring-core/src/test/java/org/springframework/util/concurrent/FutureUtilsTests.java diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java index 50a62a0f84..4a0dd68861 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java @@ -20,7 +20,6 @@ import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -158,7 +157,6 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { /** * Determine the specific executor to use when executing the given method. - *

Should preferably return an {@link AsyncListenableTaskExecutor} implementation. * @return the executor to use (or {@code null}, but just if no default executor is available) */ @Nullable @@ -176,8 +174,8 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { if (targetExecutor == null) { return null; } - executor = (targetExecutor instanceof AsyncListenableTaskExecutor ? - (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); + executor = (targetExecutor instanceof AsyncTaskExecutor ? + (AsyncTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); this.executors.put(method, executor); } return executor; @@ -276,17 +274,11 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { * @param returnType the declared return type (potentially a {@link Future} variant) * @return the execution result (potentially a corresponding {@link Future} handle) */ + @SuppressWarnings("deprecation") @Nullable protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { if (CompletableFuture.class.isAssignableFrom(returnType)) { - return CompletableFuture.supplyAsync(() -> { - try { - return task.call(); - } - catch (Throwable ex) { - throw new CompletionException(ex); - } - }, executor); + return executor.submitCompletable(task); } else if (ListenableFuture.class.isAssignableFrom(returnType)) { return ((AsyncListenableTaskExecutor) executor).submitListenable(task); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java index 6cfa717398..8be2a3534a 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java @@ -47,6 +47,7 @@ import org.springframework.util.concurrent.ListenableFutureTask; * @see org.springframework.core.task.TaskExecutor * @see SchedulerFactoryBean#setTaskExecutor */ +@SuppressWarnings("deprecation") public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, InitializingBean, DisposableBean { diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index 748fa55b39..5d329ba148 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -262,6 +262,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe return new Object[] {event}; } + @SuppressWarnings("deprecation") protected void handleResult(Object result) { if (reactiveStreamsPresent && new ReactiveResultHandler().subscribeToPublisher(result)) { if (logger.isTraceEnabled()) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java index 686845f6cb..b38a9ed90f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -45,7 +45,9 @@ import org.springframework.util.concurrent.SuccessCallback; * @see Async * @see #forValue(Object) * @see #forExecutionException(Throwable) + * @deprecated as of 6.0, in favor of {@link CompletableFuture} */ +@Deprecated public class AsyncResult implements ListenableFuture { @Nullable diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index 235e626df1..8482cb14a3 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -62,6 +62,7 @@ import org.springframework.util.concurrent.ListenableFuture; * @see DefaultManagedTaskExecutor * @see ThreadPoolTaskExecutor */ +@SuppressWarnings("deprecation") public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { @Nullable diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java index 1fb2708bcc..ba6d1b8bbb 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -80,7 +80,7 @@ import org.springframework.util.concurrent.ListenableFutureTask; * @see ThreadPoolExecutorFactoryBean * @see ConcurrentTaskExecutor */ -@SuppressWarnings("serial") +@SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index fcc25e1e98..f779bd9bb2 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -59,7 +59,7 @@ import org.springframework.util.concurrent.ListenableFutureTask; * @see #setThreadFactory * @see #setErrorHandler */ -@SuppressWarnings("serial") +@SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler { diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java index e2bd98762e..1ebc5f4f4a 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java @@ -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 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 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 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); diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java index 026da8e8e8..fe30699a1c 100644 --- a/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -27,7 +27,11 @@ import org.springframework.util.concurrent.ListenableFuture; * @author Arjen Poutsma * @since 4.0 * @see ListenableFuture + * @deprecated as of 6.0, in favor of + * {@link AsyncTaskExecutor#submitCompletable(Runnable)} and + * {@link AsyncTaskExecutor#submitCompletable(Callable)} */ +@Deprecated public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor { /** @@ -36,7 +40,9 @@ public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor { * @param task the {@code Runnable} to execute (never {@code null}) * @return a {@code ListenableFuture} representing pending completion of the task * @throws TaskRejectedException if the given task was not accepted + * @deprecated in favor of {@link AsyncTaskExecutor#submitCompletable(Runnable)} */ + @Deprecated ListenableFuture submitListenable(Runnable task); /** @@ -46,7 +52,9 @@ public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor { * @param task the {@code Callable} to execute (never {@code null}) * @return a {@code ListenableFuture} representing pending completion of the task * @throws TaskRejectedException if the given task was not accepted + * @deprecated in favor of {@link AsyncTaskExecutor#submitCompletable(Callable)} */ + @Deprecated ListenableFuture submitListenable(Callable task); } diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java index d4cf0324bc..4d65c37313 100644 --- a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java @@ -17,8 +17,11 @@ package org.springframework.core.task; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import org.springframework.util.concurrent.FutureUtils; + /** * Extended interface for asynchronous {@link TaskExecutor} implementations, * offering support for {@link java.util.concurrent.Callable}. @@ -91,4 +94,29 @@ public interface AsyncTaskExecutor extends TaskExecutor { */ Future submit(Callable task); + /** + * Submit a {@code Runnable} task for execution, receiving a {@code CompletableFuture} + * representing that task. The Future will return a {@code null} result upon completion. + * @param task the {@code Runnable} to execute (never {@code null}) + * @return a {@code CompletableFuture} representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + * @since 6.0 + */ + default CompletableFuture submitCompletable(Runnable task) { + return CompletableFuture.runAsync(task, this); + } + + /** + * Submit a {@code Callable} task for execution, receiving a {@code CompletableFuture} + * representing that task. The Future will return the Callable's result upon + * completion. + * @param task the {@code Callable} to execute (never {@code null}) + * @return a {@code CompletableFuture} representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + * @since 6.0 + */ + default CompletableFuture submitCompletable(Callable task) { + return FutureUtils.callAsync(task, this); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 8d49ac2f44..a8de97fb51 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -46,7 +46,7 @@ import org.springframework.util.concurrent.ListenableFutureTask; * @see SyncTaskExecutor * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor */ -@SuppressWarnings("serial") +@SuppressWarnings({"serial", "deprecation"}) public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implements AsyncListenableTaskExecutor, Serializable { diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java index 6d6454aec8..2a9b336520 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -43,6 +43,7 @@ import org.springframework.util.concurrent.ListenableFutureTask; * @see java.util.concurrent.ExecutorService * @see java.util.concurrent.Executors */ +@SuppressWarnings("deprecation") public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { private final Executor concurrentExecutor; diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java index 4f10d3692c..48c3040d01 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -30,7 +30,9 @@ import java.util.concurrent.TimeoutException; * @author Juergen Hoeller * @since 4.2 * @param the result type returned by this Future's {@code get} method + * @deprecated as of 6.0, with no concrete replacement */ +@Deprecated public class CompletableToListenableFutureAdapter implements ListenableFuture { private final CompletableFuture completableFuture; diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java index cefe6db09c..46d2454666 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -16,12 +16,17 @@ package org.springframework.util.concurrent; +import java.util.function.BiConsumer; + /** * Failure callback for a {@link ListenableFuture}. * * @author Sebastien Deleuze * @since 4.1 + * @deprecated as of 6.0, in favor of + * {@link java.util.concurrent.CompletableFuture#whenComplete(BiConsumer)} */ +@Deprecated @FunctionalInterface public interface FailureCallback { diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java index 4de0b46bd0..36fa75966c 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -33,7 +33,9 @@ import org.springframework.util.Assert; * @since 4.0 * @param the type of this {@code Future} * @param the type of the adaptee's {@code Future} + * @deprecated as of 6.0, with no concrete replacement */ +@Deprecated public abstract class FutureAdapter implements Future { private final Future adaptee; diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FutureUtils.java b/spring-core/src/main/java/org/springframework/util/concurrent/FutureUtils.java new file mode 100644 index 0000000000..b0b1406fce --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FutureUtils.java @@ -0,0 +1,82 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.util.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Supplier; + +import org.springframework.util.Assert; + +/** + * Convenience utilities for working with {@link java.util.concurrent.Future} + * and implementations. + * + * @author Arjen Poutsma + * @since 6.0 + */ +public abstract class FutureUtils { + + /** + * Return a new {@code CompletableFuture} that is asynchronously completed + * by a task running in the {@link ForkJoinPool#commonPool()} with + * the value obtained by calling the given {@code Callable}. + * @param callable a function that returns the value to be used, or throws + * an exception + * @return the new CompletableFuture + */ + public static CompletableFuture callAsync(Callable callable) { + Assert.notNull(callable, "Callable must not be null"); + + CompletableFuture result = new CompletableFuture<>(); + return result.completeAsync(toSupplier(callable, result)); + } + + /** + * Return a new {@code CompletableFuture} that is asynchronously completed + * by a task running in the given executor with the value obtained + * by calling the given Supplier. + * @param callable a function that returns the value to be used, or throws + * an exception + * @param executor the executor to use for asynchronous execution + * @return the new CompletableFuture + */ + public static CompletableFuture callAsync(Callable callable, Executor executor) { + Assert.notNull(callable, "Callable must not be null"); + Assert.notNull(executor, "Executor must not be null"); + + CompletableFuture result = new CompletableFuture<>(); + return result.completeAsync(toSupplier(callable, result), executor); + } + + private static Supplier toSupplier(Callable callable, CompletableFuture result) { + return () -> { + try { + return callable.call(); + } + catch (Exception ex) { + // wrap the exception just like CompletableFuture::supplyAsync does + result.completeExceptionally((ex instanceof CompletionException) ? ex : new CompletionException(ex)); + return null; + } + }; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java index 6b3bcb43fa..3ca3f75e30 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,6 +18,7 @@ package org.springframework.util.concurrent; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.BiConsumer; /** * Extend {@link Future} with the capability to accept completion callbacks. @@ -31,13 +32,18 @@ import java.util.concurrent.Future; * @author Juergen Hoeller * @since 4.0 * @param the result type returned by this Future's {@code get} method + * @deprecated as of 6.0, in favor of {@link CompletableFuture} */ +@Deprecated public interface ListenableFuture extends Future { /** * Register the given {@code ListenableFutureCallback}. * @param callback the callback to register + * @deprecated as of 6.0, in favor of + * {@link CompletableFuture#whenComplete(BiConsumer)} */ + @Deprecated void addCallback(ListenableFutureCallback callback); /** @@ -45,7 +51,10 @@ public interface ListenableFuture extends Future { * @param successCallback the success callback * @param failureCallback the failure callback * @since 4.1 + * @deprecated as of 6.0, in favor of + * {@link CompletableFuture#whenComplete(BiConsumer)} */ + @Deprecated void addCallback(SuccessCallback successCallback, FailureCallback failureCallback); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java index 3223646e35..a561e9971c 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -31,7 +31,10 @@ import org.springframework.lang.Nullable; * @since 4.0 * @param the type of this {@code Future} * @param the type of the adaptee's {@code Future} + * @deprecated as of 6.0, in favor of + * {@link java.util.concurrent.CompletableFuture} */ +@Deprecated public abstract class ListenableFutureAdapter extends FutureAdapter implements ListenableFuture { /** diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java index 215d13621c..dd2bd668d3 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -16,6 +16,8 @@ package org.springframework.util.concurrent; +import java.util.function.BiConsumer; + /** * Callback mechanism for the outcome, success or failure, from a * {@link ListenableFuture}. @@ -24,7 +26,10 @@ package org.springframework.util.concurrent; * @author Sebastien Deleuze * @since 4.0 * @param the result type + * @deprecated as of 6.0, in favor of + * {@link java.util.concurrent.CompletableFuture#whenComplete(BiConsumer)} */ +@Deprecated public interface ListenableFutureCallback extends SuccessCallback, FailureCallback { } diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java index 627addbf30..6e00b26136 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java @@ -33,7 +33,9 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @since 4.0 * @param the callback result type + * @deprecated as of 6.0, with no concrete replacement */ +@Deprecated public class ListenableFutureCallbackRegistry { private final Queue> successCallbacks = new ArrayDeque<>(1); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java index fd517aa4f0..a7e08640e1 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -29,7 +29,9 @@ import org.springframework.lang.Nullable; * @author Arjen Poutsma * @since 4.0 * @param the result type returned by this Future's {@code get} method + * @deprecated as of 6.0, with no concrete replacement */ +@Deprecated public class ListenableFutureTask extends FutureTask implements ListenableFuture { private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry<>(); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java index 5f58ae1924..12bacc0efc 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java @@ -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. @@ -27,7 +27,9 @@ import reactor.core.publisher.Mono; * @author Stephane Maldini * @since 5.1 * @param the object type + * @deprecated as of 6.0, in favor of {@link Mono#toFuture()} */ +@Deprecated public class MonoToListenableFutureAdapter extends CompletableToListenableFutureAdapter { public MonoToListenableFutureAdapter(Mono mono) { diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java index e046fbd38c..ac9186a021 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -36,7 +36,9 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @since 4.1 * @param the result type returned by this Future's {@code get} method + * @deprecated as of 6.0, in favor of {@link CompletableFuture} */ +@Deprecated public class SettableListenableFuture implements ListenableFuture { private static final Callable DUMMY_CALLABLE = () -> { diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java index fbed76eadc..0e8244e24a 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -16,6 +16,8 @@ package org.springframework.util.concurrent; +import java.util.function.BiConsumer; + import org.springframework.lang.Nullable; /** @@ -24,7 +26,10 @@ import org.springframework.lang.Nullable; * @author Sebastien Deleuze * @since 4.1 * @param the result type + * @deprecated as of 6.0, in favor of + * {@link java.util.concurrent.CompletableFuture#whenComplete(BiConsumer)} */ +@Deprecated @FunctionalInterface public interface SuccessCallback { diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/FutureUtilsTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/FutureUtilsTests.java new file mode 100644 index 0000000000..307eac2231 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/concurrent/FutureUtilsTests.java @@ -0,0 +1,116 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.util.concurrent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Arjen Poutsma + */ +class FutureUtilsTests { + + @Test + void callAsyncNormal() throws ExecutionException, InterruptedException { + String foo = "Foo"; + CompletableFuture future = FutureUtils.callAsync(() -> foo); + + assertThat(future.get()).isEqualTo(foo); + assertThat(future.isCancelled()).isFalse(); + assertThat(future.isDone()).isTrue(); + + CountDownLatch latch = new CountDownLatch(1); + future.whenComplete((s, throwable) -> { + assertThat(s).isEqualTo(foo); + assertThat(throwable).isNull(); + latch.countDown(); + }); + latch.await(); + } + + @Test + void callAsyncException() throws InterruptedException { + RuntimeException ex = new RuntimeException("Foo"); + CompletableFuture future = FutureUtils.callAsync(() -> { + throw ex; + }); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(future::get) + .withCause(ex); + assertThat(future.isCancelled()).isFalse(); + assertThat(future.isDone()).isTrue(); + + CountDownLatch latch = new CountDownLatch(1); + future.whenComplete((s, throwable) -> { + assertThat(s).isNull(); + assertThat(throwable).isInstanceOf(CompletionException.class) + .hasCause(ex); + latch.countDown(); + }); + latch.await(); + } + + @Test + void callAsyncNormalExecutor() throws ExecutionException, InterruptedException { + String foo = "Foo"; + CompletableFuture future = FutureUtils.callAsync(() -> foo, new SimpleAsyncTaskExecutor()); + + assertThat(future.get()).isEqualTo(foo); + assertThat(future.isCancelled()).isFalse(); + assertThat(future.isDone()).isTrue(); + + CountDownLatch latch = new CountDownLatch(1); + future.whenComplete((s, throwable) -> { + assertThat(s).isEqualTo(foo); + assertThat(throwable).isNull(); + latch.countDown(); + }); + latch.await(); + } + + @Test + void callAsyncExceptionExecutor() throws InterruptedException { + RuntimeException ex = new RuntimeException("Foo"); + CompletableFuture future = FutureUtils.callAsync(() -> { + throw ex; + }, new SimpleAsyncTaskExecutor()); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(future::get) + .withCause(ex); + assertThat(future.isCancelled()).isFalse(); + assertThat(future.isDone()).isTrue(); + + CountDownLatch latch = new CountDownLatch(1); + future.whenComplete((s, throwable) -> { + assertThat(s).isNull(); + assertThat(throwable).isInstanceOf(CompletionException.class) + .hasCause(ex); + latch.countDown(); + }); + latch.await(); + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index 829f4fdb5f..c71db7fe35 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -26,7 +26,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -54,8 +56,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; /** * Abstract base class for HandlerMethod-based message handling. Provides most of @@ -571,9 +571,9 @@ public abstract class AbstractMethodMessageHandler return; } if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) { - ListenableFuture future = this.returnValueHandlers.toListenableFuture(returnValue, returnType); + CompletableFuture future = this.returnValueHandlers.toCompletableFuture(returnValue, returnType); if (future != null) { - future.addCallback(new ReturnValueListenableFutureCallback(invocable, message)); + future.whenComplete(new ReturnValueListenableFutureCallback(invocable, message)); } } else { @@ -704,7 +704,7 @@ public abstract class AbstractMethodMessageHandler } - private class ReturnValueListenableFutureCallback implements ListenableFutureCallback { + private class ReturnValueListenableFutureCallback implements BiConsumer { private final InvocableHandlerMethod handlerMethod; @@ -716,21 +716,21 @@ public abstract class AbstractMethodMessageHandler } @Override - public void onSuccess(@Nullable Object result) { - try { - MethodParameter returnType = this.handlerMethod.getAsyncReturnValueType(result); - returnValueHandlers.handleReturnValue(result, returnType, this.message); + public void accept(@Nullable Object result, @Nullable Throwable ex) { + if (result != null) { + try { + MethodParameter returnType = this.handlerMethod.getAsyncReturnValueType(result); + returnValueHandlers.handleReturnValue(result, returnType, this.message); + } + catch (Throwable throwable) { + handleFailure(throwable); + } } - catch (Throwable ex) { + else if (ex != null) { handleFailure(ex); } } - @Override - public void onFailure(Throwable ex) { - handleFailure(ex); - } - private void handleFailure(Throwable ex) { Exception cause = (ex instanceof Exception ? (Exception) ex : new IllegalStateException(ex)); processHandlerMethodException(this.handlerMethod, cause, this.message); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java index f7627b25a0..cd9a6abf16 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,8 +16,11 @@ package org.springframework.messaging.handler.invocation; +import java.util.concurrent.CompletableFuture; + import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; /** @@ -37,7 +40,7 @@ public interface AsyncHandlerMethodReturnValueHandler extends HandlerMethodRetur /** * Whether the return value represents an asynchronous, Future-like type * with success and error callbacks. If this method returns {@code true}, - * then {@link #toListenableFuture} is invoked next. If it returns + * then {@link #toCompletableFuture} is invoked next. If it returns * {@code false}, then {@link #handleReturnValue} is called. *

Note: this method will only be invoked after * {@link #supportsReturnType(org.springframework.core.MethodParameter)} @@ -61,8 +64,30 @@ public interface AsyncHandlerMethodReturnValueHandler extends HandlerMethodRetur * @param returnType the type of the return value * @return the resulting ListenableFuture, or {@code null} in which case * no further handling will be performed + * @deprecated as of 6.0, in favor of + * {@link #toCompletableFuture(Object, MethodParameter)} + */ + @Deprecated + @Nullable + default ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType) { + CompletableFuture result = toCompletableFuture(returnValue, returnType); + return (result != null) ? new CompletableToListenableFutureAdapter<>(result) : null; + } + + /** + * Adapt the asynchronous return value to a {@link CompletableFuture}. + * Return value handling will then continue when + * the CompletableFuture is completed with either success or error. + *

Note: this method will only be invoked after + * {@link #supportsReturnType(org.springframework.core.MethodParameter)} + * is called and it returns {@code true}. + * @param returnValue the value returned from the handler method + * @param returnType the type of the return value + * @return the resulting CompletableFuture, or {@code null} in which case + * no further handling will be performed + * @since 6.0 */ @Nullable - ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType); + CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java index fa7e6ed4b2..275b234411 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -20,8 +20,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import org.springframework.core.MethodParameter; -import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; -import org.springframework.util.concurrent.ListenableFuture; /** * Support for {@link CompletableFuture} (and as of 4.3.7 also {@link CompletionStage}) @@ -39,9 +37,7 @@ public class CompletableFutureReturnValueHandler extends AbstractAsyncReturnValu } @Override - @SuppressWarnings("unchecked") - public ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType) { - return new CompletableToListenableFutureAdapter<>((CompletionStage) returnValue); + public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { + return ((CompletionStage) returnValue).toCompletableFuture(); } - } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java index af3efda05c..ba9479ec10 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -19,6 +19,7 @@ package org.springframework.messaging.handler.invocation; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -26,7 +27,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; -import org.springframework.util.concurrent.ListenableFuture; /** * A HandlerMethodReturnValueHandler that wraps and delegates to others. @@ -135,12 +135,11 @@ public class HandlerMethodReturnValueHandlerComposite implements AsyncHandlerMet @Override @Nullable - public ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType) { + public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType); if (handler instanceof AsyncHandlerMethodReturnValueHandler) { - return ((AsyncHandlerMethodReturnValueHandler) handler).toListenableFuture(returnValue, returnType); + return ((AsyncHandlerMethodReturnValueHandler) handler).toCompletableFuture(returnValue, returnType); } return null; } - } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java index a80030d275..fa39e86a4d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -16,6 +16,8 @@ package org.springframework.messaging.handler.invocation; +import java.util.concurrent.CompletableFuture; + import org.springframework.core.MethodParameter; import org.springframework.util.concurrent.ListenableFuture; @@ -24,7 +26,9 @@ import org.springframework.util.concurrent.ListenableFuture; * * @author Sebastien Deleuze * @since 4.2 + * @deprecated as of 6.0, in favor of {@link CompletableFutureReturnValueHandler} */ +@Deprecated public class ListenableFutureReturnValueHandler extends AbstractAsyncReturnValueHandler { @Override @@ -38,4 +42,8 @@ public class ListenableFutureReturnValueHandler extends AbstractAsyncReturnValue return (ListenableFuture) returnValue; } + @Override + public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { + return ((ListenableFuture) returnValue).completable(); + } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java index c71e829387..f843ad2506 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,13 +16,13 @@ package org.springframework.messaging.handler.invocation; +import java.util.concurrent.CompletableFuture; + import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.MonoToListenableFutureAdapter; /** * Support for single-value reactive types (like {@code Mono} or {@code Single}) @@ -57,12 +57,11 @@ public class ReactiveReturnValueHandler extends AbstractAsyncReturnValueHandler } @Override - public ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType) { + public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { ReactiveAdapter adapter = this.adapterRegistry.getAdapter(returnType.getParameterType(), returnValue); if (adapter != null) { - return new MonoToListenableFutureAdapter<>(Mono.from(adapter.toPublisher(returnValue))); + return Mono.from(adapter.toPublisher(returnValue)).toFuture(); } return null; } - } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java index b1053add26..d7001f16a6 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java @@ -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. @@ -326,6 +326,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan return resolvers; } + @SuppressWarnings("deprecation") @Override protected List initReturnValueHandlers() { List handlers = new ArrayList<>(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java index 55fe372dd8..b1048f9116 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -16,6 +16,9 @@ package org.springframework.messaging.simp.stomp; +import java.util.concurrent.CompletableFuture; + +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; /** @@ -33,7 +36,17 @@ public interface ConnectionHandlingStompSession extends StompSession, StompTcpCo /** * Return a future that will complete when the session is ready for use. + * @deprecated as of 6.0, in favor of {@link #getSession()} */ - ListenableFuture getSessionFuture(); + @Deprecated + default ListenableFuture getSessionFuture() { + return new CompletableToListenableFutureAdapter<>(getSession()); + } + + /** + * Return a future that will complete when the session is ready for use. + * @since 6.0 + */ + CompletableFuture getSession(); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java index 576ba7c1a4..04a7174bad 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; @@ -47,9 +48,6 @@ import org.springframework.util.Assert; import org.springframework.util.IdGenerator; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.SettableListenableFuture; /** * Default implementation of {@link ConnectionHandlingStompSession}. @@ -85,7 +83,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { private final StompHeaders connectHeaders; - private final SettableListenableFuture sessionFuture = new SettableListenableFuture<>(); + private final CompletableFuture sessionFuture = new CompletableFuture<>(); private MessageConverter converter = new SimpleMessageConverter(); @@ -149,7 +147,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { } @Override - public ListenableFuture getSessionFuture() { + public CompletableFuture getSession() { return this.sessionFuture; } @@ -289,7 +287,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { TcpConnection conn = this.connection; Assert.state(conn != null, "Connection closed"); try { - conn.send(message).get(); + conn.sendAsync(message).get(); } catch (ExecutionException ex) { throw new MessageDeliveryException(message, ex.getCause()); @@ -407,7 +405,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { if (logger.isDebugEnabled()) { logger.debug("Failed to connect session id=" + this.sessionId, ex); } - this.sessionFuture.setException(ex); + this.sessionFuture.completeExceptionally(ex); this.sessionHandler.handleTransportError(this, ex); } @@ -450,7 +448,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { else if (StompCommand.CONNECTED.equals(command)) { initHeartbeatTasks(headers); this.version = headers.getFirst("version"); - this.sessionFuture.set(this); + this.sessionFuture.complete(this); this.sessionHandler.afterConnected(this, headers); } else if (StompCommand.ERROR.equals(command)) { @@ -506,7 +504,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { @Override public void handleFailure(Throwable ex) { try { - this.sessionFuture.setException(ex); // no-op if already set + this.sessionFuture.completeExceptionally(ex); // no-op if already set this.sessionHandler.handleTransportError(this, ex); } catch (Throwable ex2) { @@ -698,16 +696,11 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { public void run() { TcpConnection conn = connection; if (conn != null) { - conn.send(HEARTBEAT).addCallback( - new ListenableFutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { - } - @Override - public void onFailure(Throwable ex) { - handleFailure(ex); - } - }); + conn.sendAsync(HEARTBEAT).whenComplete((unused, throwable) -> { + if (throwable != null) { + handleFailure(throwable); + } + }); } } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java index 03fc2788e8..1c27d18bf5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -16,11 +16,14 @@ package org.springframework.messaging.simp.stomp; +import java.util.concurrent.CompletableFuture; + import org.springframework.lang.Nullable; import org.springframework.messaging.simp.SimpLogging; import org.springframework.messaging.tcp.TcpOperations; import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient; import org.springframework.util.Assert; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; /** @@ -71,9 +74,22 @@ public class ReactorNettyTcpStompClient extends StompClientSupport { * on the STOMP level. * @param handler the handler for the STOMP session * @return a ListenableFuture for access to the session when ready for use + * @deprecated as of 6.0, in favor of {@link #connectAsync(StompSessionHandler)} */ + @Deprecated public ListenableFuture connect(StompSessionHandler handler) { - return connect(null, handler); + return new CompletableToListenableFutureAdapter<>(connectAsync(handler)); + } + + /** + * Connect and notify the given {@link StompSessionHandler} when connected + * on the STOMP level. + * @param handler the handler for the STOMP session + * @return a ListenableFuture for access to the session when ready for use + * @since 6.0 + */ + public CompletableFuture connectAsync(StompSessionHandler handler) { + return connectAsync(null, handler); } /** @@ -82,18 +98,33 @@ public class ReactorNettyTcpStompClient extends StompClientSupport { * @param connectHeaders headers to add to the CONNECT frame * @param handler the handler for the STOMP session * @return a ListenableFuture for access to the session when ready for use + * @deprecated as of 6.0, in favor of {@link #connectAsync(StompHeaders, StompSessionHandler)} */ + @Deprecated public ListenableFuture connect(@Nullable StompHeaders connectHeaders, StompSessionHandler handler) { ConnectionHandlingStompSession session = createSession(connectHeaders, handler); - this.tcpClient.connect(session); + this.tcpClient.connectAsync(session); return session.getSessionFuture(); } + /** + * An overloaded version of {@link #connectAsync(StompSessionHandler)} that + * accepts headers to use for the STOMP CONNECT frame. + * @param connectHeaders headers to add to the CONNECT frame + * @param handler the handler for the STOMP session + * @return a CompletableFuture for access to the session when ready for use + */ + public CompletableFuture connectAsync(@Nullable StompHeaders connectHeaders, StompSessionHandler handler) { + ConnectionHandlingStompSession session = createSession(connectHeaders, handler); + this.tcpClient.connectAsync(session); + return session.getSession(); + } + /** * Shut down the client and release resources. */ public void shutdown() { - this.tcpClient.shutdown(); + this.tcpClient.shutdownAsync(); } @Override diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java index b2d24988fc..0ea8c90cb5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -47,9 +48,6 @@ import org.springframework.messaging.tcp.reactor.ReactorNettyCodec; import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.ListenableFutureTask; /** * A {@link org.springframework.messaging.MessageHandler} that handles messages by @@ -98,14 +96,13 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler private static final byte[] EMPTY_PAYLOAD = new byte[0]; - private static final ListenableFutureTask EMPTY_TASK = new ListenableFutureTask<>(new VoidCallable()); + private static final CompletableFuture EMPTY_TASK = CompletableFuture.completedFuture(null); private static final StompHeaderAccessor HEART_BEAT_ACCESSOR; private static final Message HEARTBEAT_MESSAGE; static { - EMPTY_TASK.run(); HEART_BEAT_ACCESSOR = StompHeaderAccessor.createForHeartbeat(); HEARTBEAT_MESSAGE = MessageBuilder.createMessage( StompDecoder.HEARTBEAT_PAYLOAD, HEART_BEAT_ACCESSOR.getMessageHeaders()); @@ -455,7 +452,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler this.connectionHandlers.put(handler.getSessionId(), handler); this.stats.incrementConnectCount(); - this.tcpClient.connect(handler, new FixedIntervalReconnectStrategy(5000)); + this.tcpClient.connectAsync(handler, new FixedIntervalReconnectStrategy(5000)); if (this.taskScheduler != null) { this.taskScheduler.scheduleWithFixedDelay(new ClientSendMessageCountTask(), Duration.ofMillis(5000)); @@ -478,7 +475,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler publishBrokerUnavailableEvent(); if (this.tcpClient != null) { try { - this.tcpClient.shutdown().get(5000, TimeUnit.MILLISECONDS); + this.tcpClient.shutdownAsync().get(5000, TimeUnit.MILLISECONDS); } catch (Throwable ex) { logger.error("Error in shutdown of TCP client", ex); @@ -572,7 +569,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler this.connectionHandlers.put(sessionId, handler); this.stats.incrementConnectCount(); Assert.state(this.tcpClient != null, "No TCP client available"); - this.tcpClient.connect(handler); + this.tcpClient.connectAsync(handler); } else if (StompCommand.DISCONNECT.equals(command)) { RelayConnectionHandler handler = this.connectionHandlers.get(sessionId); @@ -691,7 +688,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler MAX_TIME_TO_CONNECTED_FRAME + " ms.", null); } }, MAX_TIME_TO_CONNECTED_FRAME); - connection.send(MessageBuilder.createMessage(EMPTY_PAYLOAD, this.connectHeaders.getMessageHeaders())); + connection.sendAsync(MessageBuilder.createMessage(EMPTY_PAYLOAD, this.connectHeaders.getMessageHeaders())); } @Override @@ -865,7 +862,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler * @return a future to wait for the result */ @SuppressWarnings("unchecked") - public ListenableFuture forward(final Message message, final StompHeaderAccessor accessor) { + public CompletableFuture forward(final Message message, final StompHeaderAccessor accessor) { TcpConnection conn = this.tcpConnection; if (!this.isStompConnected || conn == null) { @@ -901,19 +898,17 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler logger.trace("Forwarding " + accessor.getDetailedLogMessage(message.getPayload())); } - ListenableFuture future = conn.send((Message) messageToSend); - future.addCallback(new ListenableFutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { + CompletableFuture future = conn.sendAsync((Message) messageToSend); + future.whenComplete((unused, throwable) -> { + if (throwable == null) { if (accessor.getCommand() == StompCommand.DISCONNECT) { afterDisconnectSent(accessor); } } - @Override - public void onFailure(Throwable ex) { - if (tcpConnection != null) { + else { + if (this.tcpConnection != null) { handleTcpConnectionFailure("failed to forward " + - accessor.getShortLogMessage(message.getPayload()), ex); + accessor.getShortLogMessage(message.getPayload()), throwable); } else if (logger.isErrorEnabled()) { logger.error("Failed to forward " + accessor.getShortLogMessage(message.getPayload())); @@ -1005,10 +1000,11 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler if (clientSendInterval > 0 && serverReceiveInterval > 0) { long interval = Math.max(clientSendInterval, serverReceiveInterval); con.onWriteInactivity(() -> - con.send(HEARTBEAT_MESSAGE).addCallback( - result -> {}, - ex -> handleTcpConnectionFailure( - "Failed to forward heartbeat: " + ex.getMessage(), ex)), interval); + con.sendAsync(HEARTBEAT_MESSAGE).whenComplete((unused, ex) -> { + if (ex != null) { + handleTcpConnectionFailure("Failed to forward heartbeat: " + ex.getMessage(), ex); + } + }), interval); } if (clientReceiveInterval > 0 && serverSendInterval > 0) { final long interval = Math.max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER; @@ -1029,12 +1025,11 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler TcpConnection conn = getTcpConnection(); if (conn != null) { MessageHeaders headers = accessor.getMessageHeaders(); - conn.send(MessageBuilder.createMessage(EMPTY_PAYLOAD, headers)).addCallback( - result -> {}, - ex -> { - String error = "Failed to subscribe in \"system\" session."; - handleTcpConnectionFailure(error, ex); - }); + conn.sendAsync(MessageBuilder.createMessage(EMPTY_PAYLOAD, headers)).whenComplete((unused, ex) -> { + if (ex != null) { + handleTcpConnectionFailure("Failed to subscribe in \"system\" session.", ex); + } + }); } } } @@ -1083,9 +1078,9 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler } @Override - public ListenableFuture forward(Message message, StompHeaderAccessor accessor) { + public CompletableFuture forward(Message message, StompHeaderAccessor accessor) { try { - ListenableFuture future = super.forward(message, accessor); + CompletableFuture future = super.forward(message, accessor); if (message.getHeaders().get(SimpMessageHeaderAccessor.IGNORE_ERROR) == null) { future.get(); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java index 956a7bf0b4..5296e2f518 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -17,8 +17,10 @@ package org.springframework.messaging.tcp; import java.io.Closeable; +import java.util.concurrent.CompletableFuture; import org.springframework.messaging.Message; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; /** @@ -35,8 +37,21 @@ public interface TcpConnection

extends Closeable { * @param message the message * @return a ListenableFuture that can be used to determine when and if the * message was successfully sent + * @deprecated as of 6.0, in favor of {@link #sendAsync(Message)} */ - ListenableFuture send(Message

message); + @Deprecated + default ListenableFuture send(Message

message) { + return new CompletableToListenableFutureAdapter<>(sendAsync(message)); + } + + /** + * Send the given message. + * @param message the message + * @return a CompletableFuture that can be used to determine when and if the + * message was successfully sent + * @since 6.0 + */ + CompletableFuture sendAsync(Message

message); /** * Register a task to invoke after a period of read inactivity. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java index 9d30b9105f..57fd83f8cb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -16,6 +16,9 @@ package org.springframework.messaging.tcp; +import java.util.concurrent.CompletableFuture; + +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; /** @@ -32,8 +35,21 @@ public interface TcpOperations

{ * @param connectionHandler a handler to manage the connection * @return a ListenableFuture that can be used to determine when and if the * connection is successfully established + * @deprecated as of 6.0, in favor of {@link #connectAsync(TcpConnectionHandler)} */ - ListenableFuture connect(TcpConnectionHandler

connectionHandler); + @Deprecated + default ListenableFuture connect(TcpConnectionHandler

connectionHandler) { + return new CompletableToListenableFutureAdapter<>(connectAsync(connectionHandler)); + } + + /** + * Open a new connection. + * @param connectionHandler a handler to manage the connection + * @return a CompletableFuture that can be used to determine when and if the + * connection is successfully established + * @since 6.0 + */ + CompletableFuture connectAsync(TcpConnectionHandler

connectionHandler); /** * Open a new connection and a strategy for reconnecting if the connection fails. @@ -41,14 +57,40 @@ public interface TcpOperations

{ * @param reconnectStrategy a strategy for reconnecting * @return a ListenableFuture that can be used to determine when and if the * initial connection is successfully established + * @deprecated as of 6.0, in favor of {@link #connectAsync(TcpConnectionHandler, ReconnectStrategy)} */ - ListenableFuture connect(TcpConnectionHandler

connectionHandler, ReconnectStrategy reconnectStrategy); + @Deprecated + default ListenableFuture connect(TcpConnectionHandler

connectionHandler, ReconnectStrategy reconnectStrategy) { + return new CompletableToListenableFutureAdapter<>(connectAsync(connectionHandler, reconnectStrategy)); + } + + /** + * Open a new connection and a strategy for reconnecting if the connection fails. + * @param connectionHandler a handler to manage the connection + * @param reconnectStrategy a strategy for reconnecting + * @return a CompletableFuture that can be used to determine when and if the + * initial connection is successfully established + * @since 6.0 + */ + CompletableFuture connectAsync(TcpConnectionHandler

connectionHandler, ReconnectStrategy reconnectStrategy); /** * Shut down and close any open connections. * @return a ListenableFuture that can be used to determine when and if the * connection is successfully closed + * @deprecated as of 6.0, in favor of {@link #shutdownAsync()} */ - ListenableFuture shutdown(); + @Deprecated + default ListenableFuture shutdown() { + return new CompletableToListenableFutureAdapter<>(shutdownAsync()); + } + + /** + * Shut down and close any open connections. + * @return a ListenableFuture that can be used to determine when and if the + * connection is successfully closed + * @since 6.0 + */ + CompletableFuture shutdownAsync(); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java index 59e6f92237..5b56c35f7f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java @@ -52,10 +52,6 @@ import org.springframework.messaging.tcp.TcpConnection; import org.springframework.messaging.tcp.TcpConnectionHandler; import org.springframework.messaging.tcp.TcpOperations; import org.springframework.util.Assert; -import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.MonoToListenableFutureAdapter; -import org.springframework.util.concurrent.SettableListenableFuture; /** * Reactor Netty based implementation of {@link TcpOperations}. @@ -179,20 +175,18 @@ public class ReactorNettyTcpClient

implements TcpOperations

{ @Override - public ListenableFuture connect(final TcpConnectionHandler

handler) { + public CompletableFuture connectAsync(TcpConnectionHandler

handler) { Assert.notNull(handler, "TcpConnectionHandler is required"); if (this.stopping) { return handleShuttingDownConnectFailure(handler); } - Mono connectMono = extendTcpClient(this.tcpClient, handler) + return extendTcpClient(this.tcpClient, handler) .handle(new ReactorNettyHandler(handler)) .connect() .doOnError(handler::afterConnectFailure) - .then(); - - return new MonoToListenableFutureAdapter<>(connectMono); + .then().toFuture(); } /** @@ -209,7 +203,7 @@ public class ReactorNettyTcpClient

implements TcpOperations

{ } @Override - public ListenableFuture connect(TcpConnectionHandler

handler, ReconnectStrategy strategy) { + public CompletableFuture connectAsync(TcpConnectionHandler

handler, ReconnectStrategy strategy) { Assert.notNull(handler, "TcpConnectionHandler is required"); Assert.notNull(strategy, "ReconnectStrategy is required"); @@ -234,14 +228,13 @@ public class ReactorNettyTcpClient

implements TcpOperations

{ .scan(1, (count, element) -> count++) .flatMap(attempt -> reconnect(attempt, strategy))) .subscribe(); - - return new CompletableToListenableFutureAdapter<>(connectFuture); + return connectFuture; } - private ListenableFuture handleShuttingDownConnectFailure(TcpConnectionHandler

handler) { + private CompletableFuture handleShuttingDownConnectFailure(TcpConnectionHandler

handler) { IllegalStateException ex = new IllegalStateException("Shutting down."); handler.afterConnectFailure(ex); - return new MonoToListenableFutureAdapter<>(Mono.error(ex)); + return Mono.error(ex).toFuture(); } private Publisher reconnect(Integer attempt, ReconnectStrategy reconnectStrategy) { @@ -250,11 +243,9 @@ public class ReactorNettyTcpClient

implements TcpOperations

{ } @Override - public ListenableFuture shutdown() { + public CompletableFuture shutdownAsync() { if (this.stopping) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set(null); - return future; + return CompletableFuture.completedFuture(null); } this.stopping = true; @@ -274,7 +265,7 @@ public class ReactorNettyTcpClient

implements TcpOperations

{ result = stopScheduler(); } - return new MonoToListenableFutureAdapter<>(result); + return result.toFuture(); } private Mono stopScheduler() { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java index c068324d28..36adf8e261 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java @@ -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. @@ -16,6 +16,8 @@ package org.springframework.messaging.tcp.reactor; +import java.util.concurrent.CompletableFuture; + import io.netty.buffer.ByteBuf; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -24,8 +26,6 @@ import reactor.netty.NettyOutbound; import org.springframework.messaging.Message; import org.springframework.messaging.tcp.TcpConnection; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.MonoToListenableFutureAdapter; /** * Reactor Netty based implementation of {@link TcpConnection}. @@ -56,11 +56,12 @@ public class ReactorNettyTcpConnection

implements TcpConnection

{ @Override - public ListenableFuture send(Message

message) { + public CompletableFuture sendAsync(Message

message) { ByteBuf byteBuf = this.outbound.alloc().buffer(); this.codec.encode(message, byteBuf); - Mono sendCompletion = this.outbound.send(Mono.just(byteBuf)).then(); - return new MonoToListenableFutureAdapter<>(sendCompletion); + return this.outbound.send(Mono.just(byteBuf)) + .then() + .toFuture(); } @Override diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java index 3768bad4cd..9e4ab0e3ab 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java @@ -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. @@ -535,6 +535,7 @@ public class SimpAnnotationMethodMessageHandlerTests { @Controller @MessageMapping("listenable-future") + @SuppressWarnings("deprecation") private static class ListenableFutureController { private ListenableFutureTask future; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/DefaultStompSessionTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/DefaultStompSessionTests.java index b1c180dc7e..7d13c217ee 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/DefaultStompSessionTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/DefaultStompSessionTests.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicReference; @@ -45,7 +46,6 @@ import org.springframework.messaging.tcp.TcpConnection; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; -import org.springframework.util.concurrent.SettableListenableFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -90,9 +90,9 @@ public class DefaultStompSessionTests { new CompositeMessageConverter( Arrays.asList(new StringMessageConverter(), new ByteArrayMessageConverter()))); - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set(null); - given(this.connection.send(this.messageCaptor.capture())).willReturn(future); + CompletableFuture future = new CompletableFuture<>(); + future.complete(null); + given(this.connection.sendAsync(this.messageCaptor.capture())).willReturn(future); } @@ -177,7 +177,7 @@ public class DefaultStompSessionTests { @Test public void heartbeatNotSupportedByServer() { this.session.afterConnected(this.connection); - verify(this.connection).send(any()); + verify(this.connection).sendAsync(any()); this.connectHeaders.setHeartbeat(new long[] {10000, 10000}); @@ -193,7 +193,7 @@ public class DefaultStompSessionTests { @Test public void heartbeatTasks() { this.session.afterConnected(this.connection); - verify(this.connection).send(any()); + verify(this.connection).sendAsync(any()); this.connectHeaders.setHeartbeat(new long[] {10000, 10000}); @@ -216,7 +216,7 @@ public class DefaultStompSessionTests { writeTask.run(); StompHeaderAccessor accessor = StompHeaderAccessor.createForHeartbeat(); Message message = MessageBuilder.createMessage(new byte[] {'\n'}, accessor.getMessageHeaders()); - verify(this.connection).send(eq(message)); + verify(this.connection).sendAsync(eq(message)); verifyNoMoreInteractions(this.connection); reset(this.sessionHandler); @@ -435,10 +435,9 @@ public class DefaultStompSessionTests { assertThat(this.session.isConnected()).isTrue(); IllegalStateException exception = new IllegalStateException("simulated exception"); - SettableListenableFuture future = new SettableListenableFuture<>(); - future.setException(exception); + CompletableFuture future = CompletableFuture.failedFuture(exception); - given(this.connection.send(any())).willReturn(future); + given(this.connection.sendAsync(any())).willReturn(future); assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> this.session.send("/topic/foo", "sample payload".getBytes(StandardCharsets.UTF_8))) .withCause(exception); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java index ee2ee306f5..5f46a70005 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java @@ -21,6 +21,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -38,7 +39,6 @@ import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.simp.stomp.StompSession.Subscription; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; import static org.assertj.core.api.Assertions.assertThat; @@ -107,10 +107,10 @@ public class ReactorNettyTcpStompClientTests { public void publishSubscribe() throws Exception { String destination = "/topic/foo"; ConsumingHandler consumingHandler1 = new ConsumingHandler(destination); - ListenableFuture consumerFuture1 = this.client.connect(consumingHandler1); + CompletableFuture consumerFuture1 = this.client.connectAsync(consumingHandler1); ConsumingHandler consumingHandler2 = new ConsumingHandler(destination); - ListenableFuture consumerFuture2 = this.client.connect(consumingHandler2); + CompletableFuture consumerFuture2 = this.client.connectAsync(consumingHandler2); assertThat(consumingHandler1.awaitForSubscriptions(5000)).isTrue(); assertThat(consumingHandler2.awaitForSubscriptions(5000)).isTrue(); @@ -118,7 +118,7 @@ public class ReactorNettyTcpStompClientTests { ProducingHandler producingHandler = new ProducingHandler(); producingHandler.addToSend(destination, "foo1"); producingHandler.addToSend(destination, "foo2"); - ListenableFuture producerFuture = this.client.connect(producingHandler); + CompletableFuture producerFuture = this.client.connectAsync(producingHandler); assertThat(consumingHandler1.awaitForMessageCount(2, 5000)).isTrue(); assertThat(consumingHandler1.getReceived()).containsExactly("foo1", "foo2"); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java index 05e9efe81d..a544d0e669 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,8 +40,6 @@ import org.springframework.messaging.tcp.TcpConnection; import org.springframework.messaging.tcp.TcpConnectionHandler; import org.springframework.messaging.tcp.TcpOperations; import org.springframework.scheduling.TaskScheduler; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; @@ -309,10 +308,8 @@ class StompBrokerRelayMessageHandlerTests { } - private static ListenableFutureTask getVoidFuture() { - ListenableFutureTask futureTask = new ListenableFutureTask<>(() -> null); - futureTask.run(); - return futureTask; + private static CompletableFuture getVoidFuture() { + return CompletableFuture.completedFuture(null); } @@ -336,21 +333,22 @@ class StompBrokerRelayMessageHandlerTests { } @Override - public ListenableFuture connect(TcpConnectionHandler handler) { + public CompletableFuture connectAsync(TcpConnectionHandler handler) { this.connectionHandler = handler; handler.afterConnected(this.connection); return getVoidFuture(); } @Override - public ListenableFuture connect(TcpConnectionHandler handler, ReconnectStrategy strategy) { + public CompletableFuture connectAsync(TcpConnectionHandler handler, + ReconnectStrategy reconnectStrategy) { this.connectionHandler = handler; handler.afterConnected(this.connection); return getVoidFuture(); } @Override - public ListenableFuture shutdown() { + public CompletableFuture shutdownAsync() { return getVoidFuture(); } @@ -371,7 +369,7 @@ class StompBrokerRelayMessageHandlerTests { } @Override - public ListenableFuture send(Message message) { + public CompletableFuture sendAsync(Message message) { this.messages.add(message); return getVoidFuture(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 975fcfb341..0fefc59a91 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -148,7 +148,7 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple else { result = new DeferredResult<>(); } - this.futureResponse.handle((value, ex) -> { + this.futureResponse.whenComplete((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); @@ -164,7 +164,6 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple else { result.setResult(value); } - return null; }); return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 2bed63dce8..497b09b7b4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -366,7 +366,7 @@ final class DefaultEntityResponseBuilder implements EntityResponse.Builder Context context) { DeferredResult result = new DeferredResult<>(); - entity().handle((value, ex) -> { + entity().whenComplete((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); @@ -388,7 +388,6 @@ final class DefaultEntityResponseBuilder implements EntityResponse.Builder result.setErrorResult(writeException); } } - return null; }); return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java index 3e73f356fe..9eebe5467d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 +18,6 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; -import java.util.function.BiFunction; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; @@ -39,6 +38,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; */ public class DeferredResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler { + @SuppressWarnings("deprecation") @Override public boolean supportsReturnType(MethodParameter returnType) { Class type = returnType.getParameterType(); @@ -47,6 +47,7 @@ public class DeferredResultMethodReturnValueHandler implements HandlerMethodRetu CompletionStage.class.isAssignableFrom(type)); } + @SuppressWarnings("deprecation") @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { @@ -75,6 +76,7 @@ public class DeferredResultMethodReturnValueHandler implements HandlerMethodRetu WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer); } + @SuppressWarnings("deprecation") private DeferredResult adaptListenableFuture(ListenableFuture future) { DeferredResult result = new DeferredResult<>(); future.addCallback(new ListenableFutureCallback() { @@ -92,7 +94,7 @@ public class DeferredResultMethodReturnValueHandler implements HandlerMethodRetu private DeferredResult adaptCompletionStage(CompletionStage future) { DeferredResult result = new DeferredResult<>(); - future.handle((BiFunction) (value, ex) -> { + future.whenComplete((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); @@ -102,7 +104,6 @@ public class DeferredResultMethodReturnValueHandler implements HandlerMethodRetu else { result.setResult(value); } - return null; }); return result; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java index 72cbfe4280..de831afa59 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -89,6 +89,7 @@ public class DeferredResultReturnValueHandlerTests { } @Test + @SuppressWarnings("deprecation") public void listenableFuture() throws Exception { SettableListenableFuture future = new SettableListenableFuture<>(); testHandle(future, ListenableFuture.class, () -> future.set("foo"), "foo"); @@ -107,6 +108,7 @@ public class DeferredResultReturnValueHandlerTests { } @Test + @SuppressWarnings("deprecation") public void listenableFutureWithError() throws Exception { SettableListenableFuture future = new SettableListenableFuture<>(); IllegalStateException ex = new IllegalStateException(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/AbstractWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/AbstractWebSocketClient.java index 03cdeac6cf..433a04e0b7 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/AbstractWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/AbstractWebSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -63,16 +64,16 @@ public abstract class AbstractWebSocketClient implements WebSocketClient { @Override - public ListenableFuture doHandshake(WebSocketHandler webSocketHandler, + public CompletableFuture execute(WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVars) { Assert.notNull(uriTemplate, "'uriTemplate' must not be null"); URI uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVars).encode().toUri(); - return doHandshake(webSocketHandler, null, uri); + return execute(webSocketHandler, null, uri); } @Override - public final ListenableFuture doHandshake(WebSocketHandler webSocketHandler, + public final CompletableFuture execute(WebSocketHandler webSocketHandler, @Nullable WebSocketHttpHeaders headers, URI uri) { Assert.notNull(webSocketHandler, "WebSocketHandler must not be null"); @@ -96,7 +97,7 @@ public abstract class AbstractWebSocketClient implements WebSocketClient { List extensions = (headers != null ? headers.getSecWebSocketExtensions() : Collections.emptyList()); - return doHandshakeInternal(webSocketHandler, headersToUse, uri, subProtocols, extensions, + return executeInternal(webSocketHandler, headersToUse, uri, subProtocols, extensions, Collections.emptyMap()); } @@ -119,8 +120,28 @@ public abstract class AbstractWebSocketClient implements WebSocketClient { * @param attributes the attributes to associate with the WebSocketSession, i.e. via * {@link WebSocketSession#getAttributes()}; currently always an empty map. * @return the established WebSocket session wrapped in a ListenableFuture. + * @deprecated as of 6.0, in favor of {@link #executeInternal(WebSocketHandler, HttpHeaders, URI, List, List, Map)} */ - protected abstract ListenableFuture doHandshakeInternal(WebSocketHandler webSocketHandler, + @Deprecated + protected ListenableFuture doHandshakeInternal(WebSocketHandler webSocketHandler, + HttpHeaders headers, URI uri, List subProtocols, List extensions, + Map attributes) { + throw new UnsupportedOperationException("doHandshakeInternal is deprecated in favor of executeInternal"); + } + + /** + * Perform the actual handshake to establish a connection to the server. + * @param webSocketHandler the client-side handler for WebSocket messages + * @param headers the HTTP headers to use for the handshake, with unwanted (forbidden) + * headers filtered out (never {@code null}) + * @param uri the target URI for the handshake (never {@code null}) + * @param subProtocols requested sub-protocols, or an empty list + * @param extensions requested WebSocket extensions, or an empty list + * @param attributes the attributes to associate with the WebSocketSession, i.e. via + * {@link WebSocketSession#getAttributes()}; currently always an empty map. + * @return the established WebSocket session wrapped in a ListenableFuture. + */ + protected abstract CompletableFuture executeInternal(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri, List subProtocols, List extensions, Map attributes); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java index 668dcad8b6..8b61d8d0a1 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -17,8 +17,10 @@ package org.springframework.web.socket.client; import java.net.URI; +import java.util.concurrent.CompletableFuture; import org.springframework.lang.Nullable; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHttpHeaders; @@ -35,10 +37,56 @@ import org.springframework.web.socket.WebSocketSession; */ public interface WebSocketClient { - ListenableFuture doHandshake(WebSocketHandler webSocketHandler, - String uriTemplate, Object... uriVariables); + /** + * Execute a handshake request to the given url and handle the resulting + * WebSocket session with the given handler. + * @param webSocketHandler the session handler + * @param uriTemplate the url template + * @param uriVariables the variables to expand the template + * @return a future that completes when the session is available + * @deprecated as of 6.0, in favor of {@link #execute(WebSocketHandler, String, Object...)} + */ + @Deprecated + default ListenableFuture doHandshake(WebSocketHandler webSocketHandler, + String uriTemplate, Object... uriVariables) { + return new CompletableToListenableFutureAdapter<>(execute(webSocketHandler, uriTemplate, uriVariables)); + } - ListenableFuture doHandshake(WebSocketHandler webSocketHandler, + /** + * Execute a handshake request to the given url and handle the resulting + * WebSocket session with the given handler. + * @param webSocketHandler the session handler + * @param uriTemplate the url template + * @param uriVariables the variables to expand the template + * @return a future that completes when the session is available + * @since 6.0 + */ + CompletableFuture execute(WebSocketHandler webSocketHandler, + String uriTemplate, Object... uriVariables); + + /** + * Execute a handshake request to the given url and handle the resulting + * WebSocket session with the given handler. + * @param webSocketHandler the session handler + * @param uri the url + * @return a future that completes when the session is available + * @deprecated as of 6.0, in favor of {@link #execute(WebSocketHandler, WebSocketHttpHeaders, URI)} + */ + @Deprecated + default ListenableFuture doHandshake(WebSocketHandler webSocketHandler, + @Nullable WebSocketHttpHeaders headers, URI uri) { + return new CompletableToListenableFutureAdapter<>(execute(webSocketHandler, headers, uri)); + } + + /** + * Execute a handshake request to the given url and handle the resulting + * WebSocket session with the given handler. + * @param webSocketHandler the session handler + * @param uri the url + * @return a future that completes when the session is available + * @since 6.0 + */ + CompletableFuture execute(WebSocketHandler webSocketHandler, @Nullable WebSocketHttpHeaders headers, URI uri); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java index c2ad134e8e..37f159e75e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -17,12 +17,11 @@ package org.springframework.web.socket.client; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.context.Lifecycle; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; @@ -137,17 +136,15 @@ public class WebSocketConnectionManager extends ConnectionManagerSupport { logger.info("Connecting to WebSocket at " + getUri()); } - ListenableFuture future = - this.client.doHandshake(this.webSocketHandler, this.headers, getUri()); + CompletableFuture future = + this.client.execute(this.webSocketHandler, this.headers, getUri()); - future.addCallback(new ListenableFutureCallback() { - @Override - public void onSuccess(@Nullable WebSocketSession result) { - webSocketSession = result; + future.whenComplete((result, ex) -> { + if (result != null) { + this.webSocketSession = result; logger.info("Successfully connected"); } - @Override - public void onFailure(Throwable ex) { + else if (ex != null) { logger.error("Failed to connect", ex); } }); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java index 17ce562275..d5c27a5cfd 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java @@ -21,6 +21,7 @@ import java.security.Principal; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -30,12 +31,12 @@ import org.eclipse.jetty.websocket.client.WebSocketClient; import org.springframework.context.Lifecycle; import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; +import org.springframework.util.concurrent.FutureUtils; import org.springframework.web.socket.WebSocketExtension; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; @@ -43,8 +44,6 @@ import org.springframework.web.socket.adapter.jetty.JettyWebSocketHandlerAdapter import org.springframework.web.socket.adapter.jetty.JettyWebSocketSession; import org.springframework.web.socket.adapter.jetty.WebSocketToJettyExtensionConfigAdapter; import org.springframework.web.socket.client.AbstractWebSocketClient; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; /** * Initiates WebSocket requests to a WebSocket server programmatically @@ -64,7 +63,7 @@ public class JettyWebSocketClient extends AbstractWebSocketClient implements Lif private final org.eclipse.jetty.websocket.client.WebSocketClient client; @Nullable - private AsyncListenableTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); /** @@ -90,7 +89,7 @@ public class JettyWebSocketClient extends AbstractWebSocketClient implements Lif * {@code doHandshake} methods will block until the connection is established. *

By default an instance of {@code SimpleAsyncTaskExecutor} is used. */ - public void setTaskExecutor(@Nullable AsyncListenableTaskExecutor taskExecutor) { + public void setTaskExecutor(@Nullable AsyncTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @@ -98,7 +97,7 @@ public class JettyWebSocketClient extends AbstractWebSocketClient implements Lif * Return the configured {@link TaskExecutor}. */ @Nullable - public AsyncListenableTaskExecutor getTaskExecutor() { + public AsyncTaskExecutor getTaskExecutor() { return this.taskExecutor; } @@ -130,15 +129,7 @@ public class JettyWebSocketClient extends AbstractWebSocketClient implements Lif @Override - public ListenableFuture doHandshake(WebSocketHandler webSocketHandler, - String uriTemplate, Object... uriVars) { - - UriComponents uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVars).encode(); - return doHandshake(webSocketHandler, null, uriComponents.toUri()); - } - - @Override - public ListenableFuture doHandshakeInternal(WebSocketHandler wsHandler, + public CompletableFuture executeInternal(WebSocketHandler wsHandler, HttpHeaders headers, final URI uri, List protocols, List extensions, Map attributes) { @@ -162,12 +153,10 @@ public class JettyWebSocketClient extends AbstractWebSocketClient implements Lif }; if (this.taskExecutor != null) { - return this.taskExecutor.submitListenable(connectTask); + return FutureUtils.callAsync(connectTask, this.taskExecutor); } else { - ListenableFutureTask task = new ListenableFutureTask<>(connectTask); - task.run(); - return task; + return FutureUtils.callAsync(connectTask); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java index 3661300815..6f4d409098 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -26,6 +26,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.ClientEndpointConfig.Configurator; @@ -36,13 +37,13 @@ import jakarta.websocket.HandshakeResponse; import jakarta.websocket.WebSocketContainer; import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; +import org.springframework.util.concurrent.FutureUtils; import org.springframework.web.socket.WebSocketExtension; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; @@ -64,7 +65,7 @@ public class StandardWebSocketClient extends AbstractWebSocketClient { private final Map userProperties = new HashMap<>(); @Nullable - private AsyncListenableTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); /** @@ -113,7 +114,7 @@ public class StandardWebSocketClient extends AbstractWebSocketClient { * {@code doHandshake} methods will block until the connection is established. *

By default, an instance of {@code SimpleAsyncTaskExecutor} is used. */ - public void setTaskExecutor(@Nullable AsyncListenableTaskExecutor taskExecutor) { + public void setTaskExecutor(@Nullable AsyncTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @@ -121,13 +122,13 @@ public class StandardWebSocketClient extends AbstractWebSocketClient { * Return the configured {@link TaskExecutor}. */ @Nullable - public AsyncListenableTaskExecutor getTaskExecutor() { + public AsyncTaskExecutor getTaskExecutor() { return this.taskExecutor; } @Override - protected ListenableFuture doHandshakeInternal(WebSocketHandler webSocketHandler, + protected CompletableFuture executeInternal(WebSocketHandler webSocketHandler, HttpHeaders headers, final URI uri, List protocols, List extensions, Map attributes) { @@ -153,12 +154,10 @@ public class StandardWebSocketClient extends AbstractWebSocketClient { }; if (this.taskExecutor != null) { - return this.taskExecutor.submitListenable(connectTask); + return FutureUtils.callAsync(connectTask, this.taskExecutor); } else { - ListenableFutureTask task = new ListenableFutureTask<>(connectTask); - task.run(); - return task; + return FutureUtils.callAsync(connectTask); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java index 1b65927d01..1bc75bb1d5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java @@ -23,7 +23,9 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -47,9 +49,8 @@ import org.springframework.messaging.tcp.TcpConnectionHandler; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; import org.springframework.util.MimeTypeUtils; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.socket.BinaryMessage; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -210,9 +211,25 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif * @param handler the session handler * @param uriVars the URI variables to expand into the URL * @return a ListenableFuture for access to the session when ready for use + * @deprecated as of 6.0, in favor of {@link #connectAsync(String, StompSessionHandler, Object...)} */ + @Deprecated public ListenableFuture connect(String url, StompSessionHandler handler, Object... uriVars) { - return connect(url, null, handler, uriVars); + return new CompletableToListenableFutureAdapter<>(connectAsync(url, handler, uriVars)); + } + + /** + * Connect to the given WebSocket URL and notify the given + * {@link org.springframework.messaging.simp.stomp.StompSessionHandler} + * when connected on the STOMP level after the CONNECTED frame is received. + * @param url the url to connect to + * @param handler the session handler + * @param uriVars the URI variables to expand into the URL + * @return a CompletableFuture for access to the session when ready for use + * @since 6.0 + */ + public CompletableFuture connectAsync(String url, StompSessionHandler handler, Object... uriVars) { + return connectAsync(url, null, handler, uriVars); } /** @@ -224,11 +241,31 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif * @param handler the session handler * @param uriVariables the URI variables to expand into the URL * @return a ListenableFuture for access to the session when ready for use + * @deprecated as of 6.0, in favor of {@link #connectAsync(String, WebSocketHttpHeaders, StompSessionHandler, Object...)} */ + @Deprecated public ListenableFuture connect(String url, @Nullable WebSocketHttpHeaders handshakeHeaders, StompSessionHandler handler, Object... uriVariables) { - return connect(url, handshakeHeaders, null, handler, uriVariables); + return new CompletableToListenableFutureAdapter<>( + connectAsync(url, handshakeHeaders, null, handler, uriVariables)); + } + + /** + * An overloaded version of + * {@link #connect(String, StompSessionHandler, Object...)} that also + * accepts {@link WebSocketHttpHeaders} to use for the WebSocket handshake. + * @param url the url to connect to + * @param handshakeHeaders the headers for the WebSocket handshake + * @param handler the session handler + * @param uriVariables the URI variables to expand into the URL + * @return a ListenableFuture for access to the session when ready for use + * @since 6.0 + */ + public CompletableFuture connectAsync(String url, @Nullable WebSocketHttpHeaders handshakeHeaders, + StompSessionHandler handler, Object... uriVariables) { + + return connectAsync(url, handshakeHeaders, null, handler, uriVariables); } /** @@ -242,13 +279,35 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif * @param handler the session handler * @param uriVariables the URI variables to expand into the URL * @return a ListenableFuture for access to the session when ready for use + * @deprecated as of 6.0, in favor of {@link #connectAsync(String, WebSocketHttpHeaders, StompHeaders, StompSessionHandler, Object...)} */ + @Deprecated public ListenableFuture connect(String url, @Nullable WebSocketHttpHeaders handshakeHeaders, @Nullable StompHeaders connectHeaders, StompSessionHandler handler, Object... uriVariables) { + return new CompletableToListenableFutureAdapter<>( + connectAsync(url, handshakeHeaders, connectHeaders, handler, uriVariables)); + } + + /** + * An overloaded version of + * {@link #connect(String, StompSessionHandler, Object...)} that also accepts + * {@link WebSocketHttpHeaders} to use for the WebSocket handshake and + * {@link StompHeaders} for the STOMP CONNECT frame. + * @param url the url to connect to + * @param handshakeHeaders headers for the WebSocket handshake + * @param connectHeaders headers for the STOMP CONNECT frame + * @param handler the session handler + * @param uriVariables the URI variables to expand into the URL + * @return a CompletableFuture for access to the session when ready for use + * @since 6.0 + */ + public CompletableFuture connectAsync(String url, @Nullable WebSocketHttpHeaders handshakeHeaders, + @Nullable StompHeaders connectHeaders, StompSessionHandler handler, Object... uriVariables) { + Assert.notNull(url, "'url' must not be null"); URI uri = UriComponentsBuilder.fromUriString(url).buildAndExpand(uriVariables).encode().toUri(); - return connect(uri, handshakeHeaders, connectHeaders, handler); + return connectAsync(uri, handshakeHeaders, connectHeaders, handler); } /** @@ -260,17 +319,37 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif * @param connectHeaders headers for the STOMP CONNECT frame * @param sessionHandler the STOMP session handler * @return a ListenableFuture for access to the session when ready for use + * @deprecated as of 6.0, in favor of {@link #connectAsync(URI, WebSocketHttpHeaders, StompHeaders, StompSessionHandler)} */ + @Deprecated public ListenableFuture connect(URI url, @Nullable WebSocketHttpHeaders handshakeHeaders, @Nullable StompHeaders connectHeaders, StompSessionHandler sessionHandler) { + return new CompletableToListenableFutureAdapter<>( + connectAsync(url, handshakeHeaders, connectHeaders, sessionHandler)); + } + + /** + * An overloaded version of + * {@link #connect(String, WebSocketHttpHeaders, StompSessionHandler, Object...)} + * that accepts a fully prepared {@link java.net.URI}. + * @param url the url to connect to + * @param handshakeHeaders the headers for the WebSocket handshake + * @param connectHeaders headers for the STOMP CONNECT frame + * @param sessionHandler the STOMP session handler + * @return a CompletableFuture for access to the session when ready for use + * @since 6.0 + */ + public CompletableFuture connectAsync(URI url, @Nullable WebSocketHttpHeaders handshakeHeaders, + @Nullable StompHeaders connectHeaders, StompSessionHandler sessionHandler) { + Assert.notNull(url, "'url' must not be null"); ConnectionHandlingStompSession session = createSession(connectHeaders, sessionHandler); WebSocketTcpConnectionHandlerAdapter adapter = new WebSocketTcpConnectionHandlerAdapter(session); getWebSocketClient() - .doHandshake(new LoggingWebSocketHandlerDecorator(adapter), handshakeHeaders, url) - .addCallback(adapter); - return session.getSessionFuture(); + .execute(new LoggingWebSocketHandlerDecorator(adapter), handshakeHeaders, url) + .whenComplete(adapter); + return session.getSession(); } @Override @@ -286,7 +365,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif /** * Adapt WebSocket to the TcpConnectionHandler and TcpConnection contracts. */ - private class WebSocketTcpConnectionHandlerAdapter implements ListenableFutureCallback, + private class WebSocketTcpConnectionHandlerAdapter implements BiConsumer, WebSocketHandler, TcpConnection { private final TcpConnectionHandler connectionHandler; @@ -307,15 +386,13 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif this.connectionHandler = connectionHandler; } - // ListenableFutureCallback implementation: handshake outcome + // CompletableFuture callback implementation: handshake outcome @Override - public void onSuccess(@Nullable WebSocketSession webSocketSession) { - } - - @Override - public void onFailure(Throwable ex) { - this.connectionHandler.afterConnectFailure(ex); + public void accept(@Nullable WebSocketSession webSocketSession, @Nullable Throwable throwable) { + if (throwable != null) { + this.connectionHandler.afterConnectFailure(throwable); + } } // WebSocketHandler implementation @@ -375,17 +452,17 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif // TcpConnection implementation @Override - public ListenableFuture send(Message message) { + public CompletableFuture sendAsync(Message message) { updateLastWriteTime(); - SettableListenableFuture future = new SettableListenableFuture<>(); + CompletableFuture future = new CompletableFuture<>(); try { WebSocketSession session = this.session; Assert.state(session != null, "No WebSocketSession available"); session.sendMessage(this.codec.encode(message, session.getClass())); - future.set(null); + future.complete(null); } catch (Throwable ex) { - future.setException(ex); + future.completeExceptionally(ex); } finally { updateLastWriteTime(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java index 03338f9bfa..8093907f29 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.URI; import java.security.Principal; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; @@ -55,7 +56,7 @@ public abstract class AbstractClientSockJsSession implements WebSocketSession { private final WebSocketHandler webSocketHandler; - private final SettableListenableFuture connectFuture; + private final CompletableFuture connectFuture; private final Map attributes = new ConcurrentHashMap<>(); @@ -65,9 +66,18 @@ public abstract class AbstractClientSockJsSession implements WebSocketSession { @Nullable private volatile CloseStatus closeStatus; - + /** + * Create a new {@code AbstractClientSockJsSession}. + * @deprecated as of 6.0, in favor of {@link #AbstractClientSockJsSession(TransportRequest, WebSocketHandler, CompletableFuture)} + */ + @Deprecated protected AbstractClientSockJsSession(TransportRequest request, WebSocketHandler handler, SettableListenableFuture connectFuture) { + this(request, handler, connectFuture.completable()); + } + + protected AbstractClientSockJsSession(TransportRequest request, WebSocketHandler handler, + CompletableFuture connectFuture) { Assert.notNull(request, "'request' is required"); Assert.notNull(handler, "'handler' is required"); @@ -242,7 +252,7 @@ public abstract class AbstractClientSockJsSession implements WebSocketSession { this.state = State.OPEN; try { this.webSocketHandler.afterConnectionEstablished(this); - this.connectFuture.set(this); + this.connectFuture.complete(this); } catch (Exception ex) { if (logger.isErrorEnabled()) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractXhrTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractXhrTransport.java index f4c275bbed..c4d4b28b06 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractXhrTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractXhrTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -20,6 +20,7 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -28,7 +29,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; -import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.socket.TextMessage; @@ -91,8 +91,8 @@ public abstract class AbstractXhrTransport implements XhrTransport { // Transport methods @Override - public ListenableFuture connect(TransportRequest request, WebSocketHandler handler) { - SettableListenableFuture connectFuture = new SettableListenableFuture<>(); + public CompletableFuture connectAsync(TransportRequest request, WebSocketHandler handler) { + CompletableFuture connectFuture = new CompletableFuture<>(); XhrClientSockJsSession session = new XhrClientSockJsSession(request, handler, this, connectFuture); request.addTimeoutTask(session.getTimeoutTask()); @@ -109,9 +109,16 @@ public abstract class AbstractXhrTransport implements XhrTransport { return connectFuture; } + @Deprecated + protected void connectInternal(TransportRequest request, WebSocketHandler handler, + URI receiveUrl, HttpHeaders handshakeHeaders, XhrClientSockJsSession session, + SettableListenableFuture connectFuture) { + throw new UnsupportedOperationException("connectInternal has been deprecated in favor of connectInternal"); + } + protected abstract void connectInternal(TransportRequest request, WebSocketHandler handler, URI receiveUrl, HttpHeaders handshakeHeaders, XhrClientSockJsSession session, - SettableListenableFuture connectFuture); + CompletableFuture connectFuture); // InfoReceiver methods diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java index fc087e991b..83c05c5bac 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java @@ -22,7 +22,9 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -146,17 +148,40 @@ class DefaultTransportRequest implements TransportRequest { } + @Deprecated public void connect(WebSocketHandler handler, SettableListenableFuture future) { if (logger.isTraceEnabled()) { logger.trace("Starting " + this); } - ConnectCallback connectCallback = new ConnectCallback(handler, future); + ListenableConnectCallback connectCallback = new ListenableConnectCallback(handler, future); scheduleConnectTimeoutTask(connectCallback); this.transport.connect(this, handler).addCallback(connectCallback); } + public void connect(WebSocketHandler handler, CompletableFuture future) { + if (logger.isTraceEnabled()) { + logger.trace("Starting " + this); + } + CompletableConnectCallback connectCallback = new CompletableConnectCallback(handler, future); + scheduleConnectTimeoutTask(connectCallback); + this.transport.connectAsync(this, handler).whenComplete(connectCallback); + } - private void scheduleConnectTimeoutTask(ConnectCallback connectHandler) { + + private void scheduleConnectTimeoutTask(ListenableConnectCallback connectHandler) { + if (this.timeoutScheduler != null) { + if (logger.isTraceEnabled()) { + logger.trace("Scheduling connect to time out after " + this.timeoutValue + " ms."); + } + Instant timeoutDate = Instant.now().plus(this.timeoutValue, ChronoUnit.MILLIS); + this.timeoutScheduler.schedule(connectHandler, timeoutDate); + } + else if (logger.isTraceEnabled()) { + logger.trace("Connect timeout task not scheduled (no TaskScheduler configured)."); + } + } + + private void scheduleConnectTimeoutTask(CompletableConnectCallback connectHandler) { if (this.timeoutScheduler != null) { if (logger.isTraceEnabled()) { logger.trace("Scheduling connect to time out after " + this.timeoutValue + " ms."); @@ -182,7 +207,8 @@ class DefaultTransportRequest implements TransportRequest { * to connect. Also implements {@code Runnable} to handle a scheduled timeout * callback. */ - private class ConnectCallback implements ListenableFutureCallback, Runnable { + @SuppressWarnings("deprecation") + private class ListenableConnectCallback implements ListenableFutureCallback, Runnable { private final WebSocketHandler handler; @@ -190,7 +216,7 @@ class DefaultTransportRequest implements TransportRequest { private final AtomicBoolean handled = new AtomicBoolean(); - public ConnectCallback(WebSocketHandler handler, SettableListenableFuture future) { + public ListenableConnectCallback(WebSocketHandler handler, SettableListenableFuture future) { this.handler = handler; this.future = future; } @@ -250,4 +276,79 @@ class DefaultTransportRequest implements TransportRequest { } } + /** + * Updates the given (global) future based success or failure to connect for + * the entire SockJS request regardless of which transport actually managed + * to connect. Also implements {@code Runnable} to handle a scheduled timeout + * callback. + */ + private class CompletableConnectCallback + implements Runnable, BiConsumer { + + private final WebSocketHandler handler; + + private final CompletableFuture future; + + private final AtomicBoolean handled = new AtomicBoolean(); + + public CompletableConnectCallback(WebSocketHandler handler, CompletableFuture future) { + this.handler = handler; + this.future = future; + } + + @Override + public void accept(@Nullable WebSocketSession session, @Nullable Throwable throwable) { + if (session != null) { + if (this.handled.compareAndSet(false, true)) { + this.future.complete(session); + } + else if (logger.isErrorEnabled()) { + logger.error("Connect success/failure already handled for " + DefaultTransportRequest.this); + } + } + else if (throwable != null) { + handleFailure(throwable, false); + } + } + + @Override + public void run() { + handleFailure(null, true); + } + + private void handleFailure(@Nullable Throwable ex, boolean isTimeoutFailure) { + if (this.handled.compareAndSet(false, true)) { + if (isTimeoutFailure) { + String message = "Connect timed out for " + DefaultTransportRequest.this; + logger.error(message); + ex = new SockJsTransportFailureException(message, getSockJsUrlInfo().getSessionId(), ex); + } + if (fallbackRequest != null) { + logger.error(DefaultTransportRequest.this + " failed. Falling back on next transport.", ex); + fallbackRequest.connect(this.handler, this.future); + } + else { + logger.error("No more fallback transports after " + DefaultTransportRequest.this, ex); + if (ex != null) { + this.future.completeExceptionally(ex); + } + } + if (isTimeoutFailure) { + try { + for (Runnable runnable : timeoutTasks) { + runnable.run(); + } + } + catch (Throwable ex2) { + logger.error("Transport failed to run timeout tasks for " + DefaultTransportRequest.this, ex2); + } + } + } + else { + logger.error("Connect success/failure events already took place for " + + DefaultTransportRequest.this + ". Ignoring this additional failure event.", ex); + } + } + } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java index 0f00aa0103..8e95e6550a 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream; import java.net.URI; import java.nio.ByteBuffer; import java.util.Enumeration; +import java.util.concurrent.CompletableFuture; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; @@ -36,7 +37,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -110,7 +110,7 @@ public class JettyXhrTransport extends AbstractXhrTransport implements Lifecycle @Override protected void connectInternal(TransportRequest transportRequest, WebSocketHandler handler, URI url, HttpHeaders handshakeHeaders, XhrClientSockJsSession session, - SettableListenableFuture connectFuture) { + CompletableFuture connectFuture) { HttpHeaders httpHeaders = transportRequest.getHttpRequestHeaders(); SockJsResponseListener listener = new SockJsResponseListener(url, httpHeaders, session, connectFuture); @@ -197,12 +197,12 @@ public class JettyXhrTransport extends AbstractXhrTransport implements Lifecycle private final XhrClientSockJsSession sockJsSession; - private final SettableListenableFuture connectFuture; + private final CompletableFuture connectFuture; private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); public SockJsResponseListener(URI url, HttpHeaders headers, XhrClientSockJsSession sockJsSession, - SettableListenableFuture connectFuture) { + CompletableFuture connectFuture) { this.transportUrl = url; this.receiveHeaders = headers; @@ -273,7 +273,7 @@ public class JettyXhrTransport extends AbstractXhrTransport implements Lifecycle @Override public void onFailure(Response response, Throwable failure) { - if (this.connectFuture.setException(failure)) { + if (this.connectFuture.completeExceptionally(failure)) { return; } if (this.sockJsSession.isDisconnected()) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransport.java index a8fb279b17..f3b2a76547 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransport.java @@ -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. @@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.util.concurrent.CompletableFuture; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; @@ -34,7 +35,6 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseExtractor; @@ -99,7 +99,7 @@ public class RestTemplateXhrTransport extends AbstractXhrTransport { @Override protected void connectInternal(final TransportRequest transportRequest, final WebSocketHandler handler, final URI receiveUrl, final HttpHeaders handshakeHeaders, final XhrClientSockJsSession session, - final SettableListenableFuture connectFuture) { + final CompletableFuture connectFuture) { getTaskExecutor().execute(() -> { HttpHeaders httpHeaders = transportRequest.getHttpRequestHeaders(); @@ -120,7 +120,7 @@ public class RestTemplateXhrTransport extends AbstractXhrTransport { } catch (Exception ex) { if (!connectFuture.isDone()) { - connectFuture.setException(ex); + connectFuture.completeExceptionally(ex); } else { session.handleTransportError(ex); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java index 004e796d2d..0292e6e055 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; @@ -35,8 +36,6 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; @@ -229,16 +228,16 @@ public class SockJsClient implements WebSocketClient, Lifecycle { @Override - public ListenableFuture doHandshake( + public CompletableFuture execute( WebSocketHandler handler, String uriTemplate, Object... uriVars) { Assert.notNull(uriTemplate, "uriTemplate must not be null"); URI uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVars).encode().toUri(); - return doHandshake(handler, null, uri); + return execute(handler, null, uri); } @Override - public final ListenableFuture doHandshake( + public final CompletableFuture execute( WebSocketHandler handler, @Nullable WebSocketHttpHeaders headers, URI url) { Assert.notNull(handler, "WebSocketHandler is required"); @@ -249,7 +248,7 @@ public class SockJsClient implements WebSocketClient, Lifecycle { throw new IllegalArgumentException("Invalid scheme: '" + scheme + "'"); } - SettableListenableFuture connectFuture = new SettableListenableFuture<>(); + CompletableFuture connectFuture = new CompletableFuture<>(); try { SockJsUrlInfo sockJsUrlInfo = new SockJsUrlInfo(url); ServerInfo serverInfo = getServerInfo(sockJsUrlInfo, getHttpRequestHeaders(headers)); @@ -259,7 +258,7 @@ public class SockJsClient implements WebSocketClient, Lifecycle { if (logger.isErrorEnabled()) { logger.error("Initial SockJS \"Info\" request to server failed, url=" + url, exception); } - connectFuture.setException(exception); + connectFuture.completeExceptionally(exception); } return connectFuture; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/Transport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/Transport.java index a4fbdb3b45..c3ab6c7d2f 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/Transport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/Transport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -17,7 +17,9 @@ package org.springframework.web.socket.sockjs.client; import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; @@ -43,7 +45,20 @@ public interface Transport { * @param request the transport request. * @param webSocketHandler the application handler to delegate lifecycle events to. * @return a future to indicate success or failure to connect. + * @deprecated as of 6.0, in favor of {@link #connectAsync(TransportRequest, WebSocketHandler)} */ - ListenableFuture connect(TransportRequest request, WebSocketHandler webSocketHandler); + @Deprecated + default ListenableFuture connect(TransportRequest request, WebSocketHandler webSocketHandler) { + return new CompletableToListenableFutureAdapter<>(connectAsync(request, webSocketHandler)); + } + + /** + * Connect the transport. + * @param request the transport request. + * @param webSocketHandler the application handler to delegate lifecycle events to. + * @return a future to indicate success or failure to connect. + * @since 6.0 + */ + CompletableFuture connectAsync(TransportRequest request, WebSocketHandler webSocketHandler); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/UndertowXhrTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/UndertowXhrTransport.java index 161d99fe96..c38778ed82 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/UndertowXhrTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/UndertowXhrTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -55,7 +56,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -135,14 +135,14 @@ public class UndertowXhrTransport extends AbstractXhrTransport { @Override protected void connectInternal(TransportRequest request, WebSocketHandler handler, URI receiveUrl, HttpHeaders handshakeHeaders, XhrClientSockJsSession session, - SettableListenableFuture connectFuture) { + CompletableFuture connectFuture) { executeReceiveRequest(request, receiveUrl, handshakeHeaders, session, connectFuture); } private void executeReceiveRequest(final TransportRequest transportRequest, final URI url, final HttpHeaders headers, final XhrClientSockJsSession session, - final SettableListenableFuture connectFuture) { + final CompletableFuture connectFuture) { if (logger.isTraceEnabled()) { logger.trace("Starting XHR receive request for " + url); @@ -180,7 +180,7 @@ public class UndertowXhrTransport extends AbstractXhrTransport { private ClientCallback createReceiveCallback(final TransportRequest transportRequest, final URI url, final HttpHeaders headers, final XhrClientSockJsSession sockJsSession, - final SettableListenableFuture connectFuture) { + final CompletableFuture connectFuture) { return new ClientCallback<>() { @Override @@ -231,7 +231,7 @@ public class UndertowXhrTransport extends AbstractXhrTransport { } private void onFailure(Throwable failure) { - if (connectFuture.setException(failure)) { + if (connectFuture.completeExceptionally(failure)) { return; } if (sockJsSession.isDisconnected()) { @@ -374,13 +374,13 @@ public class UndertowXhrTransport extends AbstractXhrTransport { private final XhrClientSockJsSession session; - private final SettableListenableFuture connectFuture; + private final CompletableFuture connectFuture; private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); public SockJsResponseListener(TransportRequest request, ClientConnection connection, URI url, HttpHeaders headers, XhrClientSockJsSession sockJsSession, - SettableListenableFuture connectFuture) { + CompletableFuture connectFuture) { this.request = request; this.connection = connection; @@ -462,7 +462,7 @@ public class UndertowXhrTransport extends AbstractXhrTransport { public void onFailure(Throwable failure) { IoUtils.safeClose(this.connection); - if (this.connectFuture.setException(failure)) { + if (this.connectFuture.completeExceptionally(failure)) { return; } if (this.session.isDisconnected()) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketClientSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketClientSockJsSession.java index b700c0e2ca..6d3bebba61 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketClientSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketClientSockJsSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -19,6 +19,7 @@ package org.springframework.web.socket.sockjs.client; import java.io.IOException; import java.net.InetSocketAddress; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -42,13 +43,23 @@ public class WebSocketClientSockJsSession extends AbstractClientSockJsSession im @Nullable private WebSocketSession webSocketSession; - + /** + * Create a new {@code WebSocketClientSockJsSession}. + * @deprecated as of 6.0, in favor of {@link #WebSocketClientSockJsSession(TransportRequest, WebSocketHandler, CompletableFuture)} + */ + @Deprecated public WebSocketClientSockJsSession(TransportRequest request, WebSocketHandler handler, SettableListenableFuture connectFuture) { super(request, handler, connectFuture); } + public WebSocketClientSockJsSession(TransportRequest request, WebSocketHandler handler, + CompletableFuture connectFuture) { + + super(request, handler, connectFuture); + } + @Override public Object getNativeSession() { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketTransport.java index 6e18e6d809..716a2ab558 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/WebSocketTransport.java @@ -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. @@ -19,17 +19,14 @@ package org.springframework.web.socket.sockjs.client; import java.net.URI; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.Lifecycle; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; @@ -73,9 +70,11 @@ public class WebSocketTransport implements Transport, Lifecycle { return Collections.singletonList(TransportType.WEBSOCKET); } + @Override - public ListenableFuture connect(TransportRequest request, WebSocketHandler handler) { - final SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture connectAsync(TransportRequest request, + WebSocketHandler handler) { + CompletableFuture future = new CompletableFuture<>(); WebSocketClientSockJsSession session = new WebSocketClientSockJsSession(request, handler, future); handler = new ClientSockJsWebSocketHandler(session); request.addTimeoutTask(session.getTimeoutTask()); @@ -85,21 +84,14 @@ public class WebSocketTransport implements Transport, Lifecycle { if (logger.isDebugEnabled()) { logger.debug("Starting WebSocket session on " + url); } - this.webSocketClient.doHandshake(handler, headers, url).addCallback( - new ListenableFutureCallback() { - @Override - public void onSuccess(@Nullable WebSocketSession webSocketSession) { - // WebSocket session ready, SockJS Session not yet - } - @Override - public void onFailure(Throwable ex) { - future.setException(ex); - } - }); + this.webSocketClient.execute(handler, headers, url).whenComplete((webSocketSession, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } + }); return future; } - @Override public void start() { if (!isRunning()) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/XhrClientSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/XhrClientSockJsSession.java index 4c3954aa27..9eb4788dce 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/XhrClientSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/XhrClientSockJsSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -20,6 +20,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -53,7 +54,11 @@ public class XhrClientSockJsSession extends AbstractClientSockJsSession { private int binaryMessageSizeLimit = -1; - + /** + * Create a new {@code XhrClientSockJsSession}. + * @deprecated as of 6.0, in favor of {@link #XhrClientSockJsSession(TransportRequest, WebSocketHandler, XhrTransport, CompletableFuture)} + */ + @Deprecated public XhrClientSockJsSession(TransportRequest request, WebSocketHandler handler, XhrTransport transport, SettableListenableFuture connectFuture) { @@ -67,6 +72,19 @@ public class XhrClientSockJsSession extends AbstractClientSockJsSession { this.sendUrl = request.getSockJsUrlInfo().getTransportUrl(TransportType.XHR_SEND); } + public XhrClientSockJsSession(TransportRequest request, WebSocketHandler handler, + XhrTransport transport, CompletableFuture connectFuture) { + + super(request, handler, connectFuture); + Assert.notNull(transport, "XhrTransport is required"); + this.transport = transport; + this.headers = request.getHttpRequestHeaders(); + this.sendHeaders = new HttpHeaders(); + this.sendHeaders.putAll(this.headers); + this.sendHeaders.setContentType(MediaType.APPLICATION_JSON); + this.sendUrl = request.getSockJsUrlInfo().getTransportUrl(TransportType.XHR_SEND); + } + public HttpHeaders getHeaders() { return this.headers; diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java index ea71fba3bc..2a2c7ec878 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java @@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -35,7 +36,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.context.Lifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.socket.client.WebSocketClient; import org.springframework.web.socket.client.jetty.JettyWebSocketClient; @@ -150,8 +150,8 @@ public abstract class AbstractWebSocketIntegrationTests { return "ws://localhost:" + this.server.getPort(); } - protected ListenableFuture doHandshake(WebSocketHandler clientHandler, String endpointPath) { - return this.webSocketClient.doHandshake(clientHandler, getWsBaseUrl() + endpointPath); + protected CompletableFuture execute(WebSocketHandler clientHandler, String endpointPath) { + return this.webSocketClient.execute(clientHandler, getWsBaseUrl() + endpointPath); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java index 7d172ec4ce..395c54d009 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -19,13 +19,12 @@ package org.springframework.web.socket.client; import java.net.URI; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; import org.springframework.context.Lifecycle; import org.springframework.http.HttpHeaders; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; @@ -112,21 +111,21 @@ public class WebSocketConnectionManagerTests { } @Override - public ListenableFuture doHandshake(WebSocketHandler handler, + public CompletableFuture execute(WebSocketHandler handler, String uriTemplate, Object... uriVars) { URI uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVars).encode().toUri(); - return doHandshake(handler, null, uri); + return execute(handler, null, uri); } @Override - public ListenableFuture doHandshake(WebSocketHandler handler, + public CompletableFuture execute(WebSocketHandler handler, WebSocketHttpHeaders headers, URI uri) { this.webSocketHandler = handler; this.headers = headers; this.uri = uri; - return new ListenableFutureTask<>(() -> null); + return CompletableFuture.supplyAsync(() -> null); } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java index 435a5552a9..a0d751fa41 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java @@ -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. @@ -79,7 +79,7 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { TextMessage message = create(StompCommand.SEND).headers("destination:/app/simple").build(); - try (WebSocketSession session = doHandshake(new TestClientWebSocketHandler(0, message), "/ws").get()) { + try (WebSocketSession session = execute(new TestClientWebSocketHandler(0, message), "/ws").get()) { assertThat(session).isNotNull(); SimpleController controller = this.wac.getBean(SimpleController.class); assertThat(controller.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue(); @@ -98,7 +98,7 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(2, m0, m1, m2); - try (WebSocketSession session = doHandshake(clientHandler, "/ws").get()) { + try (WebSocketSession session = execute(clientHandler, "/ws").get()) { assertThat(session).isNotNull(); assertThat(clientHandler.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue(); } @@ -114,7 +114,7 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(2, m0, m1, m2); - try (WebSocketSession session = doHandshake(clientHandler, "/ws").get()) { + try (WebSocketSession session = execute(clientHandler, "/ws").get()) { assertThat(session).isNotNull(); assertThat(clientHandler.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue(); @@ -133,7 +133,7 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(2, m0, m1); - try (WebSocketSession session = doHandshake(clientHandler, "/ws").get()) { + try (WebSocketSession session = execute(clientHandler, "/ws").get()) { assertThat(session).isNotNull(); assertThat(clientHandler.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue(); String payload = clientHandler.actual.get(1).getPayload(); @@ -153,7 +153,7 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(2, m0, m1, m2); - try (WebSocketSession session = doHandshake(clientHandler, "/ws").get()) { + try (WebSocketSession session = execute(clientHandler, "/ws").get()) { assertThat(session).isNotNull(); assertThat(clientHandler.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue(); String payload = clientHandler.actual.get(1).getPayload(); @@ -175,7 +175,7 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(2, m0, m1, m2); - try (WebSocketSession session = doHandshake(clientHandler, "/ws").get()) { + try (WebSocketSession session = execute(clientHandler, "/ws").get()) { assertThat(session).isNotNull(); assertThat(clientHandler.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue(); String payload = clientHandler.actual.get(1).getPayload(); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java index fba4cf89a7..5abe874d08 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java @@ -19,6 +19,7 @@ package org.springframework.web.socket.messaging; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledFuture; import org.junit.jupiter.api.BeforeEach; @@ -39,7 +40,6 @@ import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.messaging.tcp.TcpConnection; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.MimeTypeUtils; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.socket.BinaryMessage; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.PongMessage; @@ -82,7 +82,7 @@ public class WebSocketStompClientTests { private ArgumentCaptor webSocketHandlerCaptor; - private SettableListenableFuture handshakeFuture; + private CompletableFuture handshakeFuture; @BeforeEach @@ -93,8 +93,8 @@ public class WebSocketStompClientTests { this.stompClient.setStompSession(this.stompSession); this.webSocketHandlerCaptor = ArgumentCaptor.forClass(WebSocketHandler.class); - this.handshakeFuture = new SettableListenableFuture<>(); - given(webSocketClient.doHandshake(this.webSocketHandlerCaptor.capture(), any(), any(URI.class))) + this.handshakeFuture = new CompletableFuture<>(); + given(webSocketClient.execute(this.webSocketHandlerCaptor.capture(), any(), any(URI.class))) .willReturn(this.handshakeFuture); } @@ -104,7 +104,7 @@ public class WebSocketStompClientTests { connect(); IllegalStateException handshakeFailure = new IllegalStateException("simulated exception"); - this.handshakeFuture.setException(handshakeFailure); + this.handshakeFuture.completeExceptionally(handshakeFailure); verify(this.stompSession).afterConnectFailure(same(handshakeFailure)); } @@ -202,7 +202,7 @@ public class WebSocketStompClientTests { accessor.setDestination("/topic/foo"); byte[] payload = "payload".getBytes(StandardCharsets.UTF_8); - getTcpConnection().send(MessageBuilder.createMessage(payload, accessor.getMessageHeaders())); + getTcpConnection().sendAsync(MessageBuilder.createMessage(payload, accessor.getMessageHeaders())); ArgumentCaptor textMessageCaptor = ArgumentCaptor.forClass(TextMessage.class); verify(this.webSocketSession).sendMessage(textMessageCaptor.capture()); @@ -218,7 +218,7 @@ public class WebSocketStompClientTests { accessor.setContentType(MimeTypeUtils.APPLICATION_OCTET_STREAM); byte[] payload = "payload".getBytes(StandardCharsets.UTF_8); - getTcpConnection().send(MessageBuilder.createMessage(payload, accessor.getMessageHeaders())); + getTcpConnection().sendAsync(MessageBuilder.createMessage(payload, accessor.getMessageHeaders())); ArgumentCaptor binaryMessageCaptor = ArgumentCaptor.forClass(BinaryMessage.class); verify(this.webSocketSession).sendMessage(binaryMessageCaptor.capture()); @@ -309,9 +309,9 @@ public class WebSocketStompClientTests { private WebSocketHandler connect() { - this.stompClient.connect("/foo", mock(StompSessionHandler.class)); + this.stompClient.connectAsync("/foo", mock(StompSessionHandler.class)); - verify(this.stompSession).getSessionFuture(); + verify(this.stompSession).getSession(); verifyNoMoreInteractions(this.stompSession); WebSocketHandler webSocketHandler = this.webSocketHandlerCaptor.getValue(); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java index 5968fb591e..2c22903c8c 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java @@ -20,11 +20,11 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketExtension; @@ -56,7 +56,7 @@ public class ClientSockJsSessionTests { private WebSocketHandler handler; - private SettableListenableFuture connectFuture; + private CompletableFuture connectFuture; @BeforeEach @@ -65,7 +65,7 @@ public class ClientSockJsSessionTests { Transport transport = mock(Transport.class); TransportRequest request = new DefaultTransportRequest(urlInfo, null, null, transport, TransportType.XHR, CODEC); this.handler = mock(WebSocketHandler.class); - this.connectFuture = new SettableListenableFuture<>(); + this.connectFuture = new CompletableFuture<>(); this.session = new TestClientSockJsSession(request, this.handler, this.connectFuture); } @@ -223,7 +223,7 @@ public class ClientSockJsSessionTests { protected TestClientSockJsSession(TransportRequest request, WebSocketHandler handler, - SettableListenableFuture connectFuture) { + CompletableFuture connectFuture) { super(request, handler, connectFuture); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java index 25bf90e2b7..4961c96518 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java @@ -19,7 +19,9 @@ package org.springframework.web.socket.sockjs.client; import java.io.IOException; import java.net.URI; import java.time.Instant; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,8 +29,6 @@ import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; import org.springframework.scheduling.TaskScheduler; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; import org.springframework.web.socket.sockjs.transport.TransportType; @@ -50,9 +50,9 @@ public class DefaultTransportRequestTests { private static final Jackson2SockJsMessageCodec CODEC = new Jackson2SockJsMessageCodec(); - private SettableListenableFuture connectFuture; + private CompletableFuture connectFuture; - private ListenableFutureCallback connectCallback; + private BiConsumer connectCallback; private TestTransport webSocketTransport; @@ -62,9 +62,9 @@ public class DefaultTransportRequestTests { @SuppressWarnings("unchecked") @BeforeEach public void setup() throws Exception { - this.connectCallback = mock(ListenableFutureCallback.class); - this.connectFuture = new SettableListenableFuture<>(); - this.connectFuture.addCallback(this.connectCallback); + this.connectCallback = mock(BiConsumer.class); + this.connectFuture = new CompletableFuture<>(); + this.connectFuture.whenComplete(this.connectCallback); this.webSocketTransport = new TestTransport("WebSocketTestTransport"); this.xhrTransport = new TestTransport("XhrTestTransport"); } @@ -75,7 +75,7 @@ public class DefaultTransportRequestTests { DefaultTransportRequest request = createTransportRequest(this.webSocketTransport, TransportType.WEBSOCKET); request.connect(null, this.connectFuture); WebSocketSession session = mock(WebSocketSession.class); - this.webSocketTransport.getConnectCallback().onSuccess(session); + this.webSocketTransport.getConnectCallback().accept(session, null); assertThat(this.connectFuture.get()).isSameAs(session); } @@ -87,12 +87,12 @@ public class DefaultTransportRequestTests { request1.connect(null, this.connectFuture); // Transport error => fallback - this.webSocketTransport.getConnectCallback().onFailure(new IOException("Fake exception 1")); + this.webSocketTransport.getConnectCallback().accept(null, new IOException("Fake exception 1")); assertThat(this.connectFuture.isDone()).isFalse(); assertThat(this.xhrTransport.invoked()).isTrue(); // Transport error => no more fallback - this.xhrTransport.getConnectCallback().onFailure(new IOException("Fake exception 2")); + this.xhrTransport.getConnectCallback().accept(null, new IOException("Fake exception 2")); assertThat(this.connectFuture.isDone()).isTrue(); assertThatExceptionOfType(ExecutionException.class).isThrownBy( this.connectFuture::get) diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java index 0211d3bb90..55932de6fb 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -87,7 +87,7 @@ public class SockJsClientTests { this.sockJsClient.doHandshake(handler, URL).addCallback(this.connectCallback); assertThat(this.webSocketTransport.invoked()).isTrue(); WebSocketSession session = mock(WebSocketSession.class); - this.webSocketTransport.getConnectCallback().onSuccess(session); + this.webSocketTransport.getConnectCallback().accept(session, null); verify(this.connectCallback).onSuccess(session); verifyNoMoreInteractions(this.connectCallback); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/TestTransport.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/TestTransport.java index cc42859be2..7654cc53c3 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/TestTransport.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/TestTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -20,12 +20,13 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; +import org.springframework.lang.Nullable; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; @@ -46,7 +47,8 @@ class TestTransport implements Transport { private TransportRequest request; - private ListenableFuture future; + @Nullable + private CompletableFuture future; public TestTransport(String name) { @@ -67,17 +69,17 @@ class TestTransport implements Transport { } @SuppressWarnings("unchecked") - public ListenableFutureCallback getConnectCallback() { - ArgumentCaptor captor = ArgumentCaptor.forClass(ListenableFutureCallback.class); - verify(this.future).addCallback(captor.capture()); + public BiConsumer getConnectCallback() { + ArgumentCaptor captor = ArgumentCaptor.forClass(BiConsumer.class); + verify(this.future).whenComplete(captor.capture()); return captor.getValue(); } @SuppressWarnings("unchecked") @Override - public ListenableFuture connect(TransportRequest request, WebSocketHandler handler) { + public CompletableFuture connectAsync(TransportRequest request, WebSocketHandler handler) { this.request = request; - this.future = mock(ListenableFuture.class); + this.future = mock(CompletableFuture.class); return this.future; } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java index d45ba3363a..f86353d99f 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -17,6 +17,7 @@ package org.springframework.web.socket.sockjs.client; import java.net.URI; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -25,7 +26,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; @@ -142,7 +142,7 @@ public class XhrTransportTests { @Override protected void connectInternal(TransportRequest request, WebSocketHandler handler, URI receiveUrl, HttpHeaders handshakeHeaders, XhrClientSockJsSession session, - SettableListenableFuture connectFuture) { + CompletableFuture connectFuture) { this.actualHandshakeHeaders = handshakeHeaders; this.actualSession = session;