Ce guide explique comment intégrer la fonctionnalité WebSocket dans une appilcation Spring Boot.
- Dépendance Maven
Ajoutez la dépendance suivante à votre fichier pom.xml pour activer la prise en charge de WebSocket.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- Configuration WebSocket
Créez une classe de configuration pour enregistrer le composant nécessaire à la gestion des endpoints WebSocket.
@Configuration
public class WebSocketConfig {
/**
* ServerEndpointExporter est responsable de l'enregistrement automatique
* des beans WebSocket annotés avec @ServerEndpoint.
* @return une instance de ServerEndpointExporter.
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- Implémentation du Handler WebSocket
Développez la logique métier de votre endpoint WebSocket.
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint("/websocket/{userId}") // Définit le chemin d'accès à l'endpoint WebSocket
public class WebSocketHandler {
// La session de connexion pour un client spécifique
private Session currentSession;
// L'identifiant unique du client connecté
private String clientId;
// Stocke toutes les connexions WebSocket actives, mappées par clientId. C'est thread-safe.
private static final ConcurrentHashMap<String, WebSocketHandler> connectedClients = new ConcurrentHashMap<>();
/**
* Appelé lorsqu'une nouvelle connexion WebSocket est établie.
* @param session La session de connexion avec le client.
* @param userId L'identifiant du client extrait du chemin URL.
*/
@OnOpen
public void onConnectionOpen(Session session, @PathParam("userId") String userId) {
this.currentSession = session;
this.clientId = userId;
connectedClients.put(userId, this); // Enregistre le client
log.info("Nouvelle connexion WebSocket établie par l'utilisateur : {}. Connexions actives : {}", userId, connectedClients.size());
}
/**
* Appelé lorsqu'une connexion WebSocket est fermée.
*/
@OnClose
public void onConnectionClose() {
if (connectedClients.containsKey(this.clientId)) {
connectedClients.remove(this.clientId); // Supprime le client
log.info("Connexion WebSocket fermée pour l'utilisateur : {}. Connexions actives restantes : {}", this.clientId, connectedClients.size());
}
}
/**
* Appelé lorsqu'un message est reçu du client.
* @param message Le message reçu du client.
* @param session La session du client.
*/
@OnMessage
public void onMessageReceived(String message, Session session) {
log.info("Message reçu de l'utilisateur {} via la session {} : {}", this.clientId, session.getId(), message);
// Exemple : renvoyer un accusé de réception
try {
session.getBasicRemote().sendText("Serveur a reçu : " + message);
} catch (IOException e) {
log.error("Erreur lors de l'envoi de l'accusé de réception à l'utilisateur {} : {}", this.clientId, e.getMessage());
}
}
/**
* Méthode statique pour envoyer un message à un client spécifique.
* @param message Le message à envoyer.
* @param targetUserId L'identifiant de l'utilisateur destinataire.
*/
public static void sendMessageToUser(String message, String targetUserId) {
if (targetUserId != null && connectedClients.containsKey(targetUserId)) {
WebSocketHandler clientHandler = connectedClients.get(targetUserId);
try {
clientHandler.currentSession.getBasicRemote().sendText(message);
log.debug("Message envoyé à l'utilisateur {} : {}", targetUserId, message);
} catch (IOException e) {
log.error("Échec de l'envoi du message à l'utilisateur {} : {}", targetUserId, e.getMessage());
}
} else {
log.warn("L'utilisateur {} n'est pas connecté ou l'ID est manquant.", targetUserId);
}
}
// --- Section pour l'intégration avec Spring Security (Exemple) ---
// Si vous utilisez Spring Security, vous devrez adapter la récupération de l'utilisateur.
// L'exemple suivant suppose l'existence d'une méthode getUserIdFromPrincipal.
/*
@OnOpen
public void onConnectionOpenWithSecurity(Session session) {
this.currentSession = session;
// Exemple d'obtention de l'ID utilisateur via Spring Security (à adapter)
// String userId = getUserIdFromPrincipal(session.getUserPrincipal());
// this.clientId = userId;
// connectedClients.put(userId, this);
// ... reste de la logique onOpen ...
}
// Méthode fictive pour illustrer l'obtention de l'ID utilisateur
private String getUserIdFromPrincipal(java.security.Principal principal) {
// Implémentez ici la logique pour récupérer l'ID utilisateur à partir du principal de sécurité.
// Par exemple, si vous utilisez AuthenticationPrincipal.
return "placeholderUserId";
}
*/
}
- Partage de Session WebSocket avec Redis (Pub/Sub)
Étant donné que les objets Session WebSocket ne sont pas directement sérialisables pour Redis, une approche courante consiste à utiliesr le modèle de publication/abonnement (Pub/Sub) de Redis pour la communication inter-instances.
4.1. Configuration Redis Pub/Sub
Configurez Redis pour agir comme un broker de messages.
@Configuration
@EnableCaching // Peut être utile pour d'autres configurations Redis
public class RedisMessagingConfig {
public static final String WEBSOCKET_TOPIC_CHANNEL = "chat:websocket:messages";
private final RedisConnectionFactory redisConnectionFactory;
public RedisMessagingConfig(RedisConnectionFactory connectionFactory) {
this.redisConnectionFactory = connectionFactory;
}
/**
* Définit le conteneur d'écoute des messages Redis.
* @param messageListener L'adaptateur pour écouter les messages.
* @return une instance de RedisMessageListenerContainer.
*/
@Bean
RedisMessageListenerContainer redisListenerContainer(MessageListenerAdapter messageListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
// S'abonne au canal spécifié
container.addMessageListener(messageListener, new PatternTopic(WEBSOCKET_TOPIC_CHANNEL));
return container;
}
/**
* Crée un adaptateur d'écoute de messages pour le récepteur Redis.
* @param redisMessageReceiver Le composant qui recevra les messages.
* @return une instance de MessageListenerAdapter.
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisMessageReceiver redisMessageReceiver) {
// Indique la méthode à appeler dans le receiver lorsqu'un message est reçu
return new MessageListenerAdapter(redisMessageReceiver, "handleMessage");
}
}
4.2. Classe Réceptrice des Messages Redis
Implémentez la logique pour traiter les messages reçus de Redis.
@Slf4j
@Component
public class RedisMessageReceiver implements MessageListener {
// Séparateur pour structurer le message (par exemple, "targetUserId✈messageContent")
public static final String MESSAGE_DELIMITER = "✈";
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] body = message.getBody();
if (body != null && body.length > 0) {
String receivedContent = new String(body);
log.debug("Message reçu de Redis : {}", receivedContent);
// Traite le message reçu et le transmet au handler WebSocket approprié
if (receivedContent.contains(MESSAGE_DELIMITER)) {
String[] parts = receivedContent.split(MESSAGE_DELIMITER, 2);
if (parts.length == 2) {
String targetUserId = parts[0];
String messageContent = parts[1];
WebSocketHandler.sendMessageToUser(messageContent, targetUserId);
} else {
log.warn("Format de message Redis invalide : {}", receivedContent);
}
}
}
}
}
4.3. Endpoint de Teest (API REST)
Créez un endpoint pour tester l'envoi de messages via Redis Pub/Sub.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
// Annotations Swagger/OpenAPI peuvent être ajoutées ici si nécessaire
// import io.swagger.annotations.ApiOperation;
// import io.swagger.annotations.ApiParam;
@RestController
public class TestWebSocketController {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public TestWebSocketController(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// @ApiOperation("Tester l'envoi de message WebSocket via Redis")
@PostMapping("/api/test-websocket")
public void testWebSocketMessage(
// @ApiParam("Contenu du message à envoyer")
@RequestParam String message,
// @ApiParam("ID de l'utilisateur destinataire")
@RequestParam String userId) {
String payload = userId + RedisMessageReceiver.MESSAGE_DELIMITER + message;
redisTemplate.convertAndSend(RedisMessagingConfig.WEBSOCKET_TOPIC_CHANNEL, payload);
// Alternativement, si vous retournez une réponse standardisée :
// return Response.ok("Message envoyé au canal WebSocket : " + payload);
}
}
Note : Un outil comme websocket-test.com peut être utilisé pour tester la connectivité WebSocket.