A lightweight, message-oriented WebSocket framework for Spring Boot.
Classy Socket sits in the gap between raw spring-websocket — where you're stuck parsing
TextMessage payloads by hand and routing them yourself — and the STOMP protocol, which
brings a full messaging broker's worth of complexity for use cases that don't need it. It
lets you build WebSocket APIs the same way you already build REST APIs with Spring MVC:
annotate a controller, annotate a message, write a method, and let the framework handle
dispatch.
@WebSocketController("/board")
public class KanbanWebSocketController {
@MessageHandler(CreateTask.class)
public void createTask(CreateTask message, MessagingHub hub) {
Task task = kanbanService.createTask(message.title());
hub.broadcast("/board", new TaskCreated(task));
}
}Raw spring-websocket |
STOMP | Classy Socket | |
|---|---|---|---|
| Message routing | Manual, per-handler if/else on payload |
Broker-based, @MessageMapping destinations |
Annotation-driven, type-based dispatch |
| Protocol overhead | None | Full STOMP frame protocol | None — plain JSON over the existing WebSocket protocol |
| Setup | One WebSocketHandler per concern |
Message broker configuration | One annotation on your @SpringBootApplication class |
| Feels like | Servlets | JMS/AMQP | Spring MVC |
@WebSocketController("/path")— group related message handlers under a path, exactly like@RequestMappinggroups REST endpoints.@WebSocketMessage(type = "...")— declare your own message DTOs as plain records; no base class or Jackson annotations required.@MessageHandler(Message.class)— one method per message type. Handler methods can request the incoming message, theWebSocketSession, theMessagingHub, or any Spring bean, in any order.@OnConnect/@OnDisconnect— lifecycle hooks per controller, with the same flexible parameter injection as message handlers.MessagingHub— send messages to a specific session or broadcast to every session connected to a given path, for server-initiated pushes.- Spring Boot starter — auto-configures everything behind
@ConditionalOnMissingBean, so any piece (message mapping, routing, session storage) is swappable with your own bean if you need to.
Classy Socket is published on Maven Central. Add the Spring Boot starter — it pulls in the
core classy-socket library and spring-boot-starter-websocket transitively.
Maven
<dependency>
<groupId>io.github.anubis-iv</groupId>
<artifactId>classy-socket-spring-boot-starter</artifactId>
<version>0.1.0-alpha.1</version>
</dependency>Gradle
implementation 'io.github.anubis-iv:classy-socket-spring-boot-starter:0.1.0'Check the Releases page or the Maven Central listing for the latest version.
Requirements: Java 17+, Spring Boot 4.1+.
1. Enable the library on your application class. Enablement is explicit — Classy Socket never wires itself in automatically.
@SpringBootApplication
@EnableClassySocket
public class KanbanApplication {
public static void main(String[] args) {
SpringApplication.run(KanbanApplication.class, args);
}
}By default, @EnableClassySocket scans the package of the annotated class for @WebSocketMessage
types. Pass basePackages explicitly if your messages live elsewhere:
@EnableClassySocket(basePackages = "com.myapp.messages")2. Define a message. Any plain class or record works — no framework annotations beyond @WebSocketMessage.
@WebSocketMessage(type = "CREATE_TASK")
public record CreateTask(String title) {}
@WebSocketMessage(type = "TASK_CREATED")
public record TaskCreated(Task task) {}3. Write a controller.
@WebSocketController("/board")
public class KanbanWebSocketController {
private final KanbanService kanbanService;
public KanbanWebSocketController(KanbanService kanbanService) {
this.kanbanService = kanbanService;
}
@OnConnect
public void onConnect(WebSocketSession session) {
System.out.println("Client connected: " + session.getId());
}
@OnDisconnect
public void onDisconnect(WebSocketSession session) {
System.out.println("Client disconnected: " + session.getId());
}
@MessageHandler(CreateTask.class)
public void createTask(CreateTask message, MessagingHub hub) {
Task task = kanbanService.createTask(message.title());
hub.broadcast("/board", new TaskCreated(task));
}
}4. Connect from the client. No sub-protocol negotiation, no CONNECT/SUBSCRIBE frames —
just a WebSocket connection and JSON envelopes shaped { "type": "...", "payload": {...} }.
const socket = new WebSocket("ws://localhost:8080/websocket/board");
socket.send(JSON.stringify({
type: "CREATE_TASK",
payload: { title: "Write the README" }
}));
socket.onmessage = (event) => {
const { type, payload } = JSON.parse(event.data);
if (type === "TASK_CREATED") {
// update the UI
}
};That's the whole flow: one annotated class, one annotation per message type, and the dispatcher takes care of deserialization, routing to the right method, and argument resolution — the same mental model as a REST controller.
| Annotation | Applies to | Purpose |
|---|---|---|
@EnableClassySocket |
Application/config class | Turns on the library; required, never implicit |
@WebSocketController(path) |
Class | Groups handlers under a WebSocket path |
@WebSocketMessage(type) |
Class/record | Declares a message type and its wire discriminator |
@MessageHandler(Message.class) |
Method | Handles one message type within a controller |
@OnConnect |
Method | Runs when a client connects to the controller's path |
@OnDisconnect |
Method | Runs when a client disconnects |
Handler, @OnConnect, and @OnDisconnect methods can request, in any order:
- the incoming message (for
@MessageHandlermethods) - the
WebSocketSession - the
MessagingHub - any bean available in the Spring application context
Message handlers return void — replies and pushes go through MessagingHub, injected as a
method parameter like any other bean:
hub.sendToSession(session.getId(), someMessage); // one client
hub.broadcast("/board", someMessage); // every client on a path
hub.sendToAll(someMessage); // every connected clientAll properties are optional and live under the classy-socket prefix.
classy-socket:
path: /websocket/* # mount point for the dispatcher (Ant-style pattern)
allowed-origins:
- https://myapp.example.com
sock-js: false| Property | Default | Description |
|---|---|---|
classy-socket.path |
/websocket/* |
Ant-style pattern the dispatcher is registered on. Must be a wildcard pattern — routing to individual @WebSocketController paths happens internally, not through this property. |
classy-socket.allowed-origins |
[] (all origins) |
Origins allowed to open a handshake against the dispatcher. |
classy-socket.sock-js |
false |
Enables SockJS fallback for browsers/proxies that don't support raw WebSockets. |
Per-path origin restrictions (e.g. different allowed origins per
@WebSocketController) are planned for a future release.
The classy-socket-example module is a small Kanban board built
on Classy Socket — task create/move/rename/delete broadcast live to every connected client.
It includes a static HTML/JS frontend, so it's runnable end-to-end with no separate client
setup.
cd classy-socket-example
mvn spring-boot:runThen open http://localhost:8080/page. See that module's own README
for details.
| Module | Purpose |
|---|---|
classy-socket |
Core library. Depends only on spring-context, spring-websocket, and Jackson — no Spring Boot dependency, usable in a plain Spring application. |
classy-socket-spring-boot-starter |
Spring Boot auto-configuration: binds classy-socket.* properties and wires the core beans. |
classy-socket-example |
Runnable Kanban board demo application. |
Classy Socket registers exactly one WebSocketHandler — its own dispatcher — on the path
pattern from classy-socket.path. Every message from every client passes through that
single handler, which:
- Deserializes the incoming JSON envelope using the
typefield to look up the registered message class (MessageRegistry). - Resolves the
WebSocketControllerwhose path matches the session's URI (ControllerRegistry). - Invokes the matching
@MessageHandlermethod, resolving each parameter (message, session,MessagingHub, or Spring bean) individually (HandlerMethodInvoker).
Because there's a single Spring-managed handler underneath, you get connection pooling,
interceptors, and origin/SockJS configuration for free from spring-websocket — Classy
Socket only adds the routing and annotation layer on top.
- Java 17+
- Spring Boot 4.1+ (via
classy-socket-spring-boot-starter) - Core
classy-socketmodule: Spring Framework (context + websocket) only, no Boot required
See LICENSE.
Issues and pull requests are welcome. If you're proposing a design change rather than a bug fix, please open an issue first to discuss the approach.