Deprecate ListenableFuture in favor of CompletableFuture

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

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

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

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-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<WebSocketSession> doHandshake(WebSocketHandler webSocketHandler,
public CompletableFuture<WebSocketSession> 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<WebSocketSession> doHandshake(WebSocketHandler webSocketHandler,
public final CompletableFuture<WebSocketSession> 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<WebSocketExtension> 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<WebSocketSession> doHandshakeInternal(WebSocketHandler webSocketHandler,
@Deprecated
protected ListenableFuture<WebSocketSession> doHandshakeInternal(WebSocketHandler webSocketHandler,
HttpHeaders headers, URI uri, List<String> subProtocols, List<WebSocketExtension> extensions,
Map<String, Object> 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<WebSocketSession> executeInternal(WebSocketHandler webSocketHandler,
HttpHeaders headers, URI uri, List<String> subProtocols, List<WebSocketExtension> extensions,
Map<String, Object> attributes);

View File

@@ -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<WebSocketSession> 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<WebSocketSession> doHandshake(WebSocketHandler webSocketHandler,
String uriTemplate, Object... uriVariables) {
return new CompletableToListenableFutureAdapter<>(execute(webSocketHandler, uriTemplate, uriVariables));
}
ListenableFuture<WebSocketSession> 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<WebSocketSession> 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<WebSocketSession> 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<WebSocketSession> execute(WebSocketHandler webSocketHandler,
@Nullable WebSocketHttpHeaders headers, URI uri);
}

View File

@@ -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<WebSocketSession> future =
this.client.doHandshake(this.webSocketHandler, this.headers, getUri());
CompletableFuture<WebSocketSession> future =
this.client.execute(this.webSocketHandler, this.headers, getUri());
future.addCallback(new ListenableFutureCallback<WebSocketSession>() {
@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);
}
});

View File

@@ -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.
* <p>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<WebSocketSession> doHandshake(WebSocketHandler webSocketHandler,
String uriTemplate, Object... uriVars) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVars).encode();
return doHandshake(webSocketHandler, null, uriComponents.toUri());
}
@Override
public ListenableFuture<WebSocketSession> doHandshakeInternal(WebSocketHandler wsHandler,
public CompletableFuture<WebSocketSession> executeInternal(WebSocketHandler wsHandler,
HttpHeaders headers, final URI uri, List<String> protocols,
List<WebSocketExtension> extensions, Map<String, Object> 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<WebSocketSession> task = new ListenableFutureTask<>(connectTask);
task.run();
return task;
return FutureUtils.callAsync(connectTask);
}
}

View File

@@ -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<String,Object> 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.
* <p>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<WebSocketSession> doHandshakeInternal(WebSocketHandler webSocketHandler,
protected CompletableFuture<WebSocketSession> executeInternal(WebSocketHandler webSocketHandler,
HttpHeaders headers, final URI uri, List<String> protocols,
List<WebSocketExtension> extensions, Map<String, Object> 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<WebSocketSession> task = new ListenableFutureTask<>(connectTask);
task.run();
return task;
return FutureUtils.callAsync(connectTask);
}
}

View File

@@ -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<StompSession> 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<StompSession> 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<StompSession> 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<StompSession> 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<StompSession> 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<StompSession> 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<StompSession> 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<StompSession> 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<WebSocketSession>,
private class WebSocketTcpConnectionHandlerAdapter implements BiConsumer<WebSocketSession, Throwable>,
WebSocketHandler, TcpConnection<byte[]> {
private final TcpConnectionHandler<byte[]> 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<Void> send(Message<byte[]> message) {
public CompletableFuture<Void> sendAsync(Message<byte[]> message) {
updateLastWriteTime();
SettableListenableFuture<Void> future = new SettableListenableFuture<>();
CompletableFuture<Void> 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();

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,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<WebSocketSession> connectFuture;
private final CompletableFuture<WebSocketSession> connectFuture;
private final Map<String, Object> 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<WebSocketSession> connectFuture) {
this(request, handler, connectFuture.completable());
}
protected AbstractClientSockJsSession(TransportRequest request, WebSocketHandler handler,
CompletableFuture<WebSocketSession> 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()) {

View File

@@ -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<WebSocketSession> connect(TransportRequest request, WebSocketHandler handler) {
SettableListenableFuture<WebSocketSession> connectFuture = new SettableListenableFuture<>();
public CompletableFuture<WebSocketSession> connectAsync(TransportRequest request, WebSocketHandler handler) {
CompletableFuture<WebSocketSession> 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<WebSocketSession> 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<WebSocketSession> connectFuture);
CompletableFuture<WebSocketSession> connectFuture);
// InfoReceiver methods

View File

@@ -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<WebSocketSession> 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<WebSocketSession> 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<WebSocketSession>, Runnable {
@SuppressWarnings("deprecation")
private class ListenableConnectCallback implements ListenableFutureCallback<WebSocketSession>, Runnable {
private final WebSocketHandler handler;
@@ -190,7 +216,7 @@ class DefaultTransportRequest implements TransportRequest {
private final AtomicBoolean handled = new AtomicBoolean();
public ConnectCallback(WebSocketHandler handler, SettableListenableFuture<WebSocketSession> future) {
public ListenableConnectCallback(WebSocketHandler handler, SettableListenableFuture<WebSocketSession> 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<WebSocketSession, Throwable> {
private final WebSocketHandler handler;
private final CompletableFuture<WebSocketSession> future;
private final AtomicBoolean handled = new AtomicBoolean();
public CompletableConnectCallback(WebSocketHandler handler, CompletableFuture<WebSocketSession> 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);
}
}
}
}

View File

@@ -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<WebSocketSession> connectFuture) {
CompletableFuture<WebSocketSession> 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<WebSocketSession> connectFuture;
private final CompletableFuture<WebSocketSession> connectFuture;
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public SockJsResponseListener(URI url, HttpHeaders headers, XhrClientSockJsSession sockJsSession,
SettableListenableFuture<WebSocketSession> connectFuture) {
CompletableFuture<WebSocketSession> 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()) {

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.
@@ -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<WebSocketSession> connectFuture) {
final CompletableFuture<WebSocketSession> 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);

View File

@@ -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<WebSocketSession> doHandshake(
public CompletableFuture<WebSocketSession> 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<WebSocketSession> doHandshake(
public final CompletableFuture<WebSocketSession> 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<WebSocketSession> connectFuture = new SettableListenableFuture<>();
CompletableFuture<WebSocketSession> 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;
}

View File

@@ -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<WebSocketSession> connect(TransportRequest request, WebSocketHandler webSocketHandler);
@Deprecated
default ListenableFuture<WebSocketSession> 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<WebSocketSession> connectAsync(TransportRequest request, WebSocketHandler webSocketHandler);
}

View File

@@ -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<WebSocketSession> connectFuture) {
CompletableFuture<WebSocketSession> connectFuture) {
executeReceiveRequest(request, receiveUrl, handshakeHeaders, session, connectFuture);
}
private void executeReceiveRequest(final TransportRequest transportRequest,
final URI url, final HttpHeaders headers, final XhrClientSockJsSession session,
final SettableListenableFuture<WebSocketSession> connectFuture) {
final CompletableFuture<WebSocketSession> connectFuture) {
if (logger.isTraceEnabled()) {
logger.trace("Starting XHR receive request for " + url);
@@ -180,7 +180,7 @@ public class UndertowXhrTransport extends AbstractXhrTransport {
private ClientCallback<ClientExchange> createReceiveCallback(final TransportRequest transportRequest,
final URI url, final HttpHeaders headers, final XhrClientSockJsSession sockJsSession,
final SettableListenableFuture<WebSocketSession> connectFuture) {
final CompletableFuture<WebSocketSession> 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<WebSocketSession> connectFuture;
private final CompletableFuture<WebSocketSession> connectFuture;
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public SockJsResponseListener(TransportRequest request, ClientConnection connection, URI url,
HttpHeaders headers, XhrClientSockJsSession sockJsSession,
SettableListenableFuture<WebSocketSession> connectFuture) {
CompletableFuture<WebSocketSession> 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()) {

View File

@@ -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<WebSocketSession> connectFuture) {
super(request, handler, connectFuture);
}
public WebSocketClientSockJsSession(TransportRequest request, WebSocketHandler handler,
CompletableFuture<WebSocketSession> connectFuture) {
super(request, handler, connectFuture);
}
@Override
public Object getNativeSession() {

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.
@@ -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<WebSocketSession> connect(TransportRequest request, WebSocketHandler handler) {
final SettableListenableFuture<WebSocketSession> future = new SettableListenableFuture<>();
public CompletableFuture<WebSocketSession> connectAsync(TransportRequest request,
WebSocketHandler handler) {
CompletableFuture<WebSocketSession> 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<WebSocketSession>() {
@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()) {

View File

@@ -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<WebSocketSession> 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<WebSocketSession> 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;

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;