Implement Eclipse Jetty core HTTP handler adapter

This provides an implementation of an HTTP Handler Adapter that is coded
directly to the Eclipse Jetty core API, bypassing any servlet
implementation.

This includes a Jetty implementation of the spring `WebSocketClient`
interface, `JettyWebSocketClient`, using an explicit dependency to the
jetty-websocket-api.

Closes gh-32097

Co-authored-by: Lachlan Roberts <lachlan@webtide.com>
Co-authored-by: Arjen Poutsma <arjen.poutsma@broadcom.com>
This commit is contained in:
gregw
2024-01-15 19:05:09 +11:00
committed by Simon Baslé
parent b7ec028149
commit 0a60c622cc
33 changed files with 1701 additions and 526 deletions

View File

@@ -20,17 +20,11 @@ import java.nio.ByteBuffer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Frame;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.core.OpCode;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
@@ -40,23 +34,22 @@ import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator;
/**
* Adapts {@link WebSocketHandler} to the Jetty WebSocket API.
* Adapts {@link WebSocketHandler} to the Jetty WebSocket API {@link org.eclipse.jetty.websocket.api.Session.Listener}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
@WebSocket
public class JettyWebSocketHandlerAdapter {
private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]);
public class JettyWebSocketHandlerAdapter implements Session.Listener {
private static final Log logger = LogFactory.getLog(JettyWebSocketHandlerAdapter.class);
private final WebSocketHandler webSocketHandler;
private final JettyWebSocketSession wsSession;
@Nullable
private Session nativeSession;
public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) {
Assert.notNull(webSocketHandler, "WebSocketHandler must not be null");
@@ -65,69 +58,60 @@ public class JettyWebSocketHandlerAdapter {
this.wsSession = wsSession;
}
@OnWebSocketOpen
@Override
public void onWebSocketOpen(Session session) {
try {
this.nativeSession = session;
this.wsSession.initializeNativeSession(session);
this.webSocketHandler.afterConnectionEstablished(this.wsSession);
this.nativeSession.demand();
}
catch (Exception ex) {
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
tryCloseWithError(ex);
}
}
@OnWebSocketMessage
@Override
public void onWebSocketText(String payload) {
Assert.state(this.nativeSession != null, "No native session available");
TextMessage message = new TextMessage(payload);
try {
this.webSocketHandler.handleMessage(this.wsSession, message);
this.nativeSession.demand();
}
catch (Exception ex) {
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
tryCloseWithError(ex);
}
}
@OnWebSocketMessage
@Override
public void onWebSocketBinary(ByteBuffer payload, Callback callback) {
BinaryMessage message = new BinaryMessage(copyByteBuffer(payload), true);
Assert.state(this.nativeSession != null, "No native session available");
BinaryMessage message = new BinaryMessage(BufferUtil.copy(payload), true);
callback.succeed();
try {
this.webSocketHandler.handleMessage(this.wsSession, message);
callback.succeed();
this.nativeSession.demand();
}
catch (Exception ex) {
callback.fail(ex);
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
tryCloseWithError(ex);
}
}
@OnWebSocketFrame
public void onWebSocketFrame(Frame frame, Callback callback) {
if (OpCode.PONG == frame.getOpCode()) {
ByteBuffer payload = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD;
PongMessage message = new PongMessage(copyByteBuffer(payload));
try {
this.webSocketHandler.handleMessage(this.wsSession, message);
callback.succeed();
}
catch (Exception ex) {
callback.fail(ex);
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
}
@Override
public void onWebSocketPong(ByteBuffer payload) {
Assert.state(this.nativeSession != null, "No native session available");
PongMessage message = new PongMessage(BufferUtil.copy(payload));
try {
this.webSocketHandler.handleMessage(this.wsSession, message);
this.nativeSession.demand();
}
else {
callback.succeed();
catch (Exception ex) {
tryCloseWithError(ex);
}
}
private static ByteBuffer copyByteBuffer(ByteBuffer src) {
ByteBuffer dest = ByteBuffer.allocate(src.remaining());
dest.put(src);
dest.flip();
return dest;
}
@OnWebSocketClose
@Override
public void onWebSocketClose(int statusCode, String reason) {
CloseStatus closeStatus = new CloseStatus(statusCode, reason);
try {
@@ -135,19 +119,32 @@ public class JettyWebSocketHandlerAdapter {
}
catch (Exception ex) {
if (logger.isWarnEnabled()) {
logger.warn("Unhandled exception after connection closed for " + this, ex);
logger.warn("Unhandled exception from afterConnectionClosed for " + this, ex);
}
}
}
@OnWebSocketError
@Override
public void onWebSocketError(Throwable cause) {
try {
this.webSocketHandler.handleTransportError(this.wsSession, cause);
}
catch (Exception ex) {
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
if (logger.isWarnEnabled()) {
logger.warn("Unhandled exception from handleTransportError for " + this, ex);
}
}
}
private void tryCloseWithError(Throwable t) {
if (this.nativeSession != null) {
if (this.nativeSession.isOpen()) {
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
}
else {
// Session might be O-SHUT waiting for response close frame, so abort to close the connection.
this.nativeSession.disconnect();
}
}
}
}

View File

@@ -45,7 +45,7 @@ import org.springframework.web.socket.server.HandshakeFailureException;
import org.springframework.web.socket.server.RequestUpgradeStrategy;
/**
* A {@link RequestUpgradeStrategy} for Jetty 11.
* A {@link RequestUpgradeStrategy} for Jetty 12 EE10.
*
* @author Rossen Stoyanchev
* @since 5.3.4