Skip to content

Repository files navigation

Classy Socket

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));
    }
}

Why Classy Socket

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

Features

  • @WebSocketController("/path") — group related message handlers under a path, exactly like @RequestMapping groups 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, the WebSocketSession, the MessagingHub, 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.

Installation

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+.

Quick Start

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.

Core Annotations

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 @MessageHandler methods)
  • the WebSocketSession
  • the MessagingHub
  • any bean available in the Spring application context

Sending messages back: MessagingHub

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 client

Configuration

All 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.

Example project

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:run

Then open http://localhost:8080/page. See that module's own README for details.

Modules

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.

How it works, briefly

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:

  1. Deserializes the incoming JSON envelope using the type field to look up the registered message class (MessageRegistry).
  2. Resolves the WebSocketController whose path matches the session's URI (ControllerRegistry).
  3. Invokes the matching @MessageHandler method, 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.

Requirements & Compatibility

  • Java 17+
  • Spring Boot 4.1+ (via classy-socket-spring-boot-starter)
  • Core classy-socket module: Spring Framework (context + websocket) only, no Boot required

License

See LICENSE.

Contributing

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.

About

A lightweight, message-oriented WebSocket library introduced as a Spring Boot Starter. Sits on top spring-websocket not replacing it.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages