Course topics

By WebNest Studio

Spring Boot Tutorial

WebSockets and STOMP

HTTP is request/response: the server can only answer when the client asks. Chat, live notifications, collaborative editing, live dashboards and multiplayer quizzes need the server to push updates the moment something happens. WebSockets provide a persistent, two-way connection between browser and server for exactly this.

Spring Boot supports raw WebSockets, but most applications use STOMP, a simple messaging protocol on top of WebSocket that adds destinations, subscriptions and message routing. This lesson builds a live class chat and per-user notifications with @EnableWebSocketMessageBroker, @MessageMapping and SimpMessagingTemplate, secures it, and scales it across instances with an external broker.

WebSocket, STOMP and SSE

A WebSocket starts as an HTTP request that is "upgraded" to a long-lived TCP connection. STOMP frames on top of it carry a destination such as /topic/class.42, so the server can route messages much like a message broker. If you only need server → client updates (notifications, progress bars, streaming AI answers), Server-Sent Events over plain HTTP are simpler and work through all proxies; choose WebSockets when clients also send frequent messages.

Configuring the Message Broker

With spring-boot-starter-websocket, implement WebSocketMessageBrokerConfigurer on a class annotated with @EnableWebSocketMessageBroker. Register a STOMP endpoint (e.g. /ws) that clients connect to, set the application destination prefix (/app) for messages handled by your controllers, and enable a simple in-memory broker for /topic (broadcast) and /queue (per-user) destinations.

Handling and Sending Messages

@MessageMapping("/chat.send") handles messages sent by clients to /app/chat.send; @SendTo("/topic/chat") broadcasts the return value. SimpMessagingTemplate sends from anywhere in your code — a service, a Kafka listener, a scheduled job — with convertAndSend(destination, payload) or convertAndSendToUser(user, "/queue/notifications", payload) for a single user.

Security

The WebSocket handshake is an HTTP request, so Spring Security authenticates it like any other (session cookie or token). Authorize individual STOMP messages and subscriptions with @EnableWebSocketSecurity and an AuthorizationManager<Message<?>> bean — for example, only enrolled students may subscribe to a class topic. Restrict allowed origins on the endpoint.

Scaling Out

The simple broker lives in one JVM: a user connected to instance A does not receive messages sent on instance B. For multiple instances, use enableStompBrokerRelay with RabbitMQ's STOMP plugin (or ActiveMQ) so all instances share one broker.

Examples

Broker configuration

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOrigins("https://www.webneststudio.co.in", "http://localhost:5173");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");     // client -> @MessageMapping
        registry.enableSimpleBroker("/topic", "/queue");        // server -> subscribers
        registry.setUserDestinationPrefix("/user");
    }
}
Output
Started WebSocket endpoint /ws; simple broker for [/topic, /queue]

Chat controller and server-initiated notifications

Java
public record ChatMessage(String from, String text, Instant sentAt) {}
public record IncomingChat(String text) {}

@Controller
public class ClassChatController {

    // client sends to /app/class/42/chat ; everyone subscribed to /topic/class/42 receives it
    @MessageMapping("/class/{classId}/chat")
    @SendTo("/topic/class/{classId}")
    public ChatMessage chat(@DestinationVariable long classId, IncomingChat in, Principal user) {
        return new ChatMessage(user.getName(), HtmlUtils.htmlEscape(in.text()), Instant.now());
    }
}

@Service
public class GradeNotifier {

    private final SimpMessagingTemplate messaging;

    public GradeNotifier(SimpMessagingTemplate messaging) {
        this.messaging = messaging;
    }

    // called from anywhere, e.g. after an instructor grades an assignment
    public void notifyGraded(String studentUsername, String assignment, int score) {
        messaging.convertAndSendToUser(studentUsername, "/queue/notifications",
            Map.of("type", "GRADED", "assignment", assignment, "score", score));
    }
}
Output
asha sends {"text":"Is @Transactional needed here?"} to /app/class/42/chat
-> all 31 subscribers of /topic/class/42 receive {"from":"asha","text":"Is @Transactional needed here?","sentAt":"..."}

notifyGraded("asha", "JPA homework", 92)
-> only asha's browser receives {"type":"GRADED","assignment":"JPA homework","score":92}

Browser client with @stomp/stompjs

Java
// npm install @stomp/stompjs
import { Client } from '@stomp/stompjs';

const client = new Client({
  brokerURL: 'wss://www.webneststudio.co.in/ws',
  reconnectDelay: 5000,
  onConnect: () => {
    client.subscribe('/topic/class/42', (frame) => {
      const msg = JSON.parse(frame.body);
      console.log(msg.from + ': ' + msg.text);
    });
    client.subscribe('/user/queue/notifications', (frame) => {
      console.log('Notification', JSON.parse(frame.body));
    });
  },
});
client.activate();

function send(text) {
  client.publish({ destination: '/app/class/42/chat', body: JSON.stringify({ text }) });
}
Output
asha: Is @Transactional needed here?
Notification {type: 'GRADED', assignment: 'JPA homework', score: 92}

Authorizing subscriptions and messages

Java
@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {

    @Bean
    AuthorizationManager<Message<?>> messageAuthorization(
            MessageMatcherDelegatingAuthorizationManager.Builder messages) {
        return messages
            .nullDestMatcher().authenticated()                          // CONNECT, DISCONNECT
            .simpSubscribeDestMatchers("/topic/class/*").hasRole("STUDENT")
            .simpDestMatchers("/app/**").authenticated()
            .simpSubscribeDestMatchers("/user/queue/**").authenticated()
            .anyMessage().denyAll()
            .build();
    }
}
Output
Anonymous SUBSCRIBE /topic/class/42 -> ERROR frame: Access denied
Student   SUBSCRIBE /topic/class/42 -> subscribed

Common Mistakes

  • Using WebSockets for one-way server updates where Server-Sent Events would be simpler.
  • Broadcasting user-supplied text without escaping, enabling stored XSS in every connected browser.
  • Relying on the simple broker with multiple instances, so users on different instances miss messages.
  • Leaving setAllowedOrigins("*") on authenticated endpoints.
  • Forgetting that load balancers and proxies must be configured to allow WebSocket upgrades and long idle connections.

Key Points to Remember

  • WebSockets give persistent two-way connections; STOMP adds destinations and subscriptions.
  • @EnableWebSocketMessageBroker configures endpoints, /app prefixes and /topic and /queue brokers.
  • @MessageMapping + @SendTo handle client messages; SimpMessagingTemplate pushes from anywhere.
  • Secure the handshake with Spring Security and messages with @EnableWebSocketSecurity.
  • Use a broker relay (e.g. RabbitMQ STOMP) to scale across instances.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.