WebSocket in Spring Boot Applications

Implement real-time communication in Spring Boot applications using WebSocket and STOMP messaging protocol.

WebSocket in Spring Boot Applications

This guide covers implementing real-time communication in Spring Boot applications using WebSocket and STOMP messaging protocol.

Video Tutorial

Learn more about Spring Boot WebSocket in this comprehensive video tutorial:

Prerequisites

<dependencies>
    <!-- WebSocket -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
    <!-- STOMP WebSocket -->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>stomp-websocket</artifactId>
        <version>2.3.4</version>
    </dependency>
    
    <!-- SockJS -->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>sockjs-client</artifactId>
        <version>1.5.1</version>
    </dependency>
</dependencies>

Basic WebSocket Configuration

WebSocket Configuration

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}

Message Controller

@Controller
public class ChatController {
    
    @MessageMapping("/chat.send")
    @SendTo("/topic/messages")
    public ChatMessage send(ChatMessage message) {
        return new ChatMessage(
                message.getSender(),
                message.getContent(),
                LocalDateTime.now()
        );
    }
    
    @MessageMapping("/chat.private")
    @SendToUser("/queue/reply")
    public ChatMessage sendToUser(
            @Payload ChatMessage message,
            Principal principal) {
        return new ChatMessage(
                message.getSender(),
                "Private: " + message.getContent(),
                LocalDateTime.now()
        );
    }
}

Message Model

@Data
@AllArgsConstructor
public class ChatMessage {
    private String sender;
    private String content;
    private LocalDateTime timestamp;
}

Advanced WebSocket Features

Security Configuration

@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpDestMatchers("/app/**").authenticated()
                .simpSubscribeDestMatchers("/user/**", "/topic/**").authenticated()
                .anyMessage().denyAll();
    }
    
    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

Custom Channel Interceptor

@Component
public class CustomChannelInterceptor implements ChannelInterceptor {
    
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(
                message, StompHeaderAccessor.class);
        
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            // Authentication/Authorization logic
        }
        
        return message;
    }
    
    @Override
    public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(
                message, StompHeaderAccessor.class);
        
        if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
            // Handle disconnect
        }
    }
}

WebSocket Event Listener

@Component
public class WebSocketEventListener {
    
    private final SimpMessageSendingOperations messagingTemplate;
    
    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        log.info("Received a new web socket connection");
    }
    
    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = (String) headerAccessor.getSessionAttributes().get("username");
        
        if (username != null) {
            log.info("User Disconnected : " + username);
            
            ChatMessage chatMessage = new ChatMessage();
            chatMessage.setSender(username);
            chatMessage.setType(MessageType.LEAVE);
            
            messagingTemplate.convertAndSend("/topic/public", chatMessage);
        }
    }
}

Real-Time Features

Chat Room Implementation

@Service
public class ChatRoomService {
    
    private final SimpMessageSendingOperations messagingTemplate;
    private final Map<String, Set<String>> chatRooms = new ConcurrentHashMap<>();
    
    public void joinRoom(String roomId, String username) {
        chatRooms.computeIfAbsent(roomId, k -> new CopyOnWriteArraySet<>())
                .add(username);
        
        messagingTemplate.convertAndSend(
                "/topic/rooms/" + roomId,
                new ChatMessage("System", username + " joined the room", LocalDateTime.now())
        );
    }
    
    public void leaveRoom(String roomId, String username) {
        Set<String> users = chatRooms.get(roomId);
        if (users != null) {
            users.remove(username);
            
            messagingTemplate.convertAndSend(
                    "/topic/rooms/" + roomId,
                    new ChatMessage("System", username + " left the room", LocalDateTime.now())
            );
        }
    }
    
    public void sendMessage(String roomId, ChatMessage message) {
        messagingTemplate.convertAndSend("/topic/rooms/" + roomId, message);
    }
}

Real-Time Notifications

@Service
public class NotificationService {
    
    private final SimpMessageSendingOperations messagingTemplate;
    
    public void sendGlobalNotification(String message) {
        NotificationMessage notification = new NotificationMessage(
                "GLOBAL",
                message,
                LocalDateTime.now()
        );
        
        messagingTemplate.convertAndSend("/topic/notifications", notification);
    }
    
    public void sendPrivateNotification(String username, String message) {
        NotificationMessage notification = new NotificationMessage(
                "PRIVATE",
                message,
                LocalDateTime.now()
        );
        
        messagingTemplate.convertAndSendToUser(
                username,
                "/queue/notifications",
                notification
        );
    }
}

Error Handling

Global Error Handler

@Component
public class WebSocketErrorHandler implements WebSocketErrorHandler {
    
    @Override
    public void handleError(WebSocketSession session, Throwable exception) {
        log.error("WebSocket Error: ", exception);
        
        if (session.isOpen()) {
            try {
                session.close(CloseStatus.SERVER_ERROR);
            } catch (IOException e) {
                log.error("Error closing WebSocket session", e);
            }
        }
    }
}

Message Error Handler

@ControllerAdvice
public class WebSocketExceptionHandler {
    
    private final SimpMessageSendingOperations messagingTemplate;
    
    @MessageExceptionHandler
    public void handleException(Exception ex, Principal principal) {
        ErrorMessage errorMessage = new ErrorMessage(
                "Error processing message: " + ex.getMessage(),
                LocalDateTime.now()
        );
        
        messagingTemplate.convertAndSendToUser(
                principal.getName(),
                "/queue/errors",
                errorMessage
        );
    }
}

Monitoring and Metrics

WebSocket Metrics

@Component
public class WebSocketMetrics {
    
    private final MeterRegistry registry;
    
    public WebSocketMetrics(MeterRegistry registry) {
        this.registry = registry;
    }
    
    public void recordConnection() {
        registry.counter("websocket.connections").increment();
    }
    
    public void recordDisconnection() {
        registry.counter("websocket.disconnections").increment();
    }
    
    public void recordMessageSent() {
        registry.counter("websocket.messages.sent").increment();
    }
    
    public void recordMessageReceived() {
        registry.counter("websocket.messages.received").increment();
    }
}

Best Practices

  1. Connection Management

    • Implement heartbeat
    • Handle reconnection
    • Clean up resources
    • Monitor connections
  2. Security

    • Authenticate users
    • Authorize messages
    • Validate payload
    • Use HTTPS
  3. Performance

    • Limit message size
    • Use compression
    • Implement throttling
    • Monitor memory usage
  4. Error Handling

    • Handle disconnects
    • Implement retry logic
    • Log errors
    • Send error messages

Common Patterns

  1. Pub/Sub Pattern
@Service
public class PubSubService {
    
    private final SimpMessageSendingOperations messagingTemplate;
    
    public void publish(String topic, Object message) {
        messagingTemplate.convertAndSend("/topic/" + topic, message);
    }
    
    @MessageMapping("/subscribe/{topic}")
    public void subscribe(@DestinationVariable String topic,
                         @Header("simpSessionId") String sessionId) {
        // Handle subscription
    }
}
  1. Room Management
@Service
public class RoomManager {
    
    private final Map<String, Room> rooms = new ConcurrentHashMap<>();
    
    public void createRoom(String roomId, String owner) {
        rooms.put(roomId, new Room(roomId, owner));
    }
    
    public void joinRoom(String roomId, String username) {
        Room room = rooms.get(roomId);
        if (room != null) {
            room.addUser(username);
            notifyRoomUsers(roomId);
        }
    }
    
    private void notifyRoomUsers(String roomId) {
        Room room = rooms.get(roomId);
        if (room != null) {
            messagingTemplate.convertAndSend(
                    "/topic/rooms/" + roomId + "/users",
                    room.getUsers()
            );
        }
    }
}

Client-Side Implementation

JavaScript Client

class WebSocketClient {
    constructor(url) {
        this.socket = new SockJS(url);
        this.stompClient = Stomp.over(this.socket);
        this.subscriptions = new Map();
    }
    
    connect(username, onConnect, onError) {
        this.stompClient.connect(
            {
                username: username
            },
            frame => {
                console.log('Connected: ' + frame);
                this.subscribeToUser(username);
                if (onConnect) onConnect(frame);
            },
            error => {
                console.log('Error: ' + error);
                if (onError) onError(error);
            }
        );
    }
    
    subscribeToUser(username) {
        this.subscribe('/user/queue/reply', message => {
            console.log('Received private message:', message);
        });
        
        this.subscribe('/user/queue/errors', error => {
            console.log('Received error:', error);
        });
    }
    
    subscribe(destination, callback) {
        const subscription = this.stompClient.subscribe(destination, message => {
            const payload = JSON.parse(message.body);
            callback(payload);
        });
        
        this.subscriptions.set(destination, subscription);
    }
    
    send(destination, message) {
        this.stompClient.send(destination, {}, JSON.stringify(message));
    }
    
    disconnect() {
        if (this.stompClient !== null) {
            this.stompClient.disconnect();
        }
    }
}

Conclusion

Effective WebSocket implementation in Spring Boot requires:

  • Proper configuration
  • Security measures
  • Error handling
  • Performance optimization
  • Monitoring and metrics

For more Spring Boot topics, check out: