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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user