Walk through the lifecycle callbacks of a WebSocketHandler / TextWebSocketHandler.
answer
- established -> message -> transportError -> closed
- register in established, remove in closed
- TextWebSocketHandler.handleTextMessage(payload String)
- supportsPartialMessages() for big/streamed payloads
- singleton handler; state in session, not fields
basics
~20 safterConnectionEstablished runs when a client connects; handleMessage (or handleTextMessage in TextWebSocketHandler) runs per incoming frame; handleTransportError on I/O errors; afterConnectionClosed when the socket closes. You typically track sessions in the first and remove them in the last.
solid answer
~40 sThe raw WebSocketHandler interface defines four lifecycle hooks. afterConnectionEstablished(session) fires once the handshake succeeds and the WebSocketSession is open — register it (e.g. put it in a concurrent map) here. handleMessage(session, message) fires for every inbound frame; AbstractWebSocketHandler dispatches by type, and TextWebSocketHandler overrides handleTextMessage(session, TextMessage) for you, with handleBinaryMessage / handlePong for others. handleTransportError(session, throwable) fires on a transport-level failure; you usually log and may close. afterConnectionClosed(session, CloseStatus) fires exactly once when the connection ends for any reason — deregister the session here so you don't leak. There's also supportsPartialMessages(): return true to receive large messages in fragments instead of assembling them (avoids buffering huge payloads). Keep no per-session state in the singleton handler; use session.getAttributes() or an external map keyed by session.getId().
code
java · 31 lines@Component
public class ChatHandler extends TextWebSocketHandler {
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.put(session.getId(), session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws IOException {
String payload = message.getPayload();
for (WebSocketSession s : sessions.values()) {
if (s.isOpen()) {
s.sendMessage(new TextMessage(payload)); // naive broadcast
}
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable ex) throws IOException {
if (session.isOpen()) session.close(CloseStatus.SERVER_ERROR);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session.getId()); // prevent leaks
}
}go deeper
Name the four callbacks and that handleTextMessage carries the text payload.
Explain register-in-established / remove-in-closed, the singleton threading model, and guarding isOpen() before send.
Discuss partial messages, per-session threading serialization vs cross-session concurrency, and not blocking container threads.
Reason about backpressure, offloading work to executors, and why naive broadcast loops don't scale (send buffering, slow consumers).
## The interface and the convenience base classes `WebSocketHandler` is the raw interface with five methods: - `afterConnectionEstablished(WebSocketSession session)` - `handleMessage(WebSocketSession session, WebSocketMessage<?> message)` - `handleTransportError(WebSocketSession session, Throwable exception)` - `afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)` - `boolean supportsPartialMessages()` You rarely implement it directly. `AbstractWebSocketHandler` provides `handleMessage` that **dispatches by frame type** to `handleTextMessage`, `handleBinaryMessage`, and `handlePongMessage`. `TextWebSocketHandler` extends it and, importantly, **rejects binary messages by default** (closes the session with `CloseStatus.NOT_ACCEPTABLE`). `BinaryWebSocketHandler` is the mirror image. ## Lifecycle in order 1. **`afterConnectionEstablished`** — called once, after the HTTP upgrade completes and the session is OPEN. Good place to: add the session to a registry (`Map<String, WebSocketSession>`), send a welcome frame, or read handshake attributes you stored via a `HandshakeInterceptor`. The `session.getId()` is unique per connection. 2. **`handleTextMessage(session, TextMessage message)`** — called for each inbound text frame. `message.getPayload()` is the `String`. This is where your protocol logic lives: parse, act, and optionally `session.sendMessage(new TextMessage(...))`. It may be called many times over the connection's life. 3. **`handleTransportError`** — a lower-level I/O/protocol error (not an exception you threw in business logic — those propagate differently). Typical action: log and `session.close(CloseStatus.SERVER_ERROR)` if still open. 4. **`afterConnectionClosed(session, CloseStatus status)`** — called exactly once when the socket closes: client left, network dropped, or you called `close()`. **Always deregister here** to avoid session leaks. The `CloseStatus` code tells you why (1000 normal, 1001 going away, 1006 abnormal, etc.). ## Threading model For a given session, container threads normally invoke these callbacks **serially** (one message processed before the next for that session), but *different* sessions run on *different* threads concurrently. So the handler singleton must be thread-safe across sessions. Long/blocking work inside a callback ties up a container thread and delays that session's next message — offload heavy work to an executor and send results asynchronously. ## supportsPartialMessages() Default `false`: Spring buffers and reassembles a full message before calling your handler. Return `true` and your handler receives message *fragments* (`message.isLast()` tells you when the last one arrives). Use it for very large payloads/streaming to avoid holding the whole message in memory. You then assemble or stream-process fragments yourself. ## Gotchas - **State in fields**: the handler is a singleton; per-connection data in instance fields corrupts across users. Use `session.getAttributes()` or a keyed concurrent map. - **Forgetting to remove sessions** in `afterConnectionClosed` → memory leak and sends to dead sockets. - **Sending after close**: `sendMessage` on a closed session throws; guard with `session.isOpen()`. - **TextWebSocketHandler closing on binary**: if a client sends binary frames unexpectedly, the connection drops — subclass the right base or override the binary handler. - **Blocking in a callback** stalls that session's message pipeline and can exhaust container threads under load.
- Where do you store per-connection state such as the authenticated user?In session.getAttributes() (a Map you can populate during the handshake via a HandshakeInterceptor, or in afterConnectionEstablished), or in an external map keyed by session.getId(). Never in handler instance fields, because the handler is a shared singleton.
- What does supportsPartialMessages() returning true change?Spring stops buffering the whole message and delivers fragments to your handler; message.isLast() marks the final fragment. It lets you process very large or streaming payloads without holding the entire message in memory.
saying these in an interview costs you the question
- Claiming callbacks for one session run concurrently (they are serialized per session)
- Keeping per-user state in handler instance fields
- Forgetting afterConnectionClosed cleanup, leaking sessions
- Thinking TextWebSocketHandler happily accepts binary frames