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
-
Connection Management
- Implement heartbeat
- Handle reconnection
- Clean up resources
- Monitor connections
-
Security
- Authenticate users
- Authorize messages
- Validate payload
- Use HTTPS
-
Performance
- Limit message size
- Use compression
- Implement throttling
- Monitor memory usage
-
Error Handling
- Handle disconnects
- Implement retry logic
- Log errors
- Send error messages
Common Patterns
- 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
}
}
- 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: