Intégration de WebSocket avec Spring Boot

Ce guide explique comment intégrer la fonctionnalité WebSocket dans une appilcation Spring Boot.

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

  1. 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();
    }
}

  1. 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";
    }
    */
}

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

Étiquettes: Spring Boot WebSocket Java Redis pub/sub

Publié le 3 août à 09h14