Deprecate ListenableFuture in favor of CompletableFuture

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

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

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

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

View File

@@ -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<WebSocketSession> doHandshake(WebSocketHandler clientHandler, String endpointPath) {
return this.webSocketClient.doHandshake(clientHandler, getWsBaseUrl() + endpointPath);
protected CompletableFuture<WebSocketSession> execute(WebSocketHandler clientHandler, String endpointPath) {
return this.webSocketClient.execute(clientHandler, getWsBaseUrl() + endpointPath);
}

View File

@@ -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<WebSocketSession> doHandshake(WebSocketHandler handler,
public CompletableFuture<WebSocketSession> 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<WebSocketSession> doHandshake(WebSocketHandler handler,
public CompletableFuture<WebSocketSession> execute(WebSocketHandler handler,
WebSocketHttpHeaders headers, URI uri) {
this.webSocketHandler = handler;
this.headers = headers;
this.uri = uri;
return new ListenableFutureTask<>(() -> null);
return CompletableFuture.supplyAsync(() -> null);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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();

View File

@@ -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<WebSocketHandler> webSocketHandlerCaptor;
private SettableListenableFuture<WebSocketSession> handshakeFuture;
private CompletableFuture<WebSocketSession> 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<TextMessage> 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<BinaryMessage> 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();

View File

@@ -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<WebSocketSession> connectFuture;
private CompletableFuture<WebSocketSession> 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<WebSocketSession> connectFuture) {
CompletableFuture<WebSocketSession> connectFuture) {
super(request, handler, connectFuture);
}

View File

@@ -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<WebSocketSession> connectFuture;
private CompletableFuture<WebSocketSession> connectFuture;
private ListenableFutureCallback<WebSocketSession> connectCallback;
private BiConsumer<WebSocketSession, Throwable> 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)

View File

@@ -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);
}

View File

@@ -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<WebSocketSession> future;
public TestTransport(String name) {
@@ -67,17 +69,17 @@ class TestTransport implements Transport {
}
@SuppressWarnings("unchecked")
public ListenableFutureCallback<WebSocketSession> getConnectCallback() {
ArgumentCaptor<ListenableFutureCallback> captor = ArgumentCaptor.forClass(ListenableFutureCallback.class);
verify(this.future).addCallback(captor.capture());
public BiConsumer<WebSocketSession, Throwable> getConnectCallback() {
ArgumentCaptor<BiConsumer> captor = ArgumentCaptor.forClass(BiConsumer.class);
verify(this.future).whenComplete(captor.capture());
return captor.getValue();
}
@SuppressWarnings("unchecked")
@Override
public ListenableFuture<WebSocketSession> connect(TransportRequest request, WebSocketHandler handler) {
public CompletableFuture<WebSocketSession> connectAsync(TransportRequest request, WebSocketHandler handler) {
this.request = request;
this.future = mock(ListenableFuture.class);
this.future = mock(CompletableFuture.class);
return this.future;
}

View File

@@ -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<WebSocketSession> connectFuture) {
CompletableFuture<WebSocketSession> connectFuture) {
this.actualHandshakeHeaders = handshakeHeaders;
this.actualSession = session;