web-dev-qa-db-fra.com

Spring @SubscribeMapping abonne-t-il vraiment le client à un sujet?

J'utilise Spring Websocket avec STOMP, Simple Message Broker. Dans mon @Controller J'utilise au niveau de la méthode @SubscribeMapping, Qui devrait abonner le client à un sujet afin que le client reçoive ensuite les messages de ce sujet. Disons que le client s'abonne au sujet "chat":

stompClient.subscribe('/app/chat', ...);

Comme le client s'est abonné à "/ app/chat", au lieu de "/ topic/chat", cet abonnement irait à la méthode qui est mappée en utilisant @SubscribeMapping:

@SubscribeMapping("/chat")
public List getChatInit() {
    return Chat.getUsers();
}

Voici ce que Spring ref. dit:

Par défaut, la valeur de retour d'une méthode @SubscribeMapping est envoyée sous forme de message directement au client connecté et ne passe pas par le courtier. Ceci est utile pour implémenter des interactions de message de demande-réponse; par exemple, pour récupérer les données d'application lorsque l'interface utilisateur de l'application est en cours d'initialisation.

D'accord, c'est ce que je voudrais, mais juste partiellement !! Envoi de données init après la souscription, eh bien. Mais qu'en est-il de abonnement? Il me semble que ce qui s'est passé ici est juste un demande-réponse, comme un service. L'abonnement est juste consommé. Veuillez me préciser si tel est le cas.

  • Le client s'est-il abonné à certains endroits, si le courtier n'y participe pas?
  • Si plus tard, je veux envoyer un message aux abonnés au "chat", le client le recevra-t-il? Il ne semble pas que ce soit le cas.
  • Qui réalise vraiment les abonnements? Courtier? Ou quelqu'un d'autre?

Si ici, le client n'est abonné à aucun endroit, je me demande pourquoi nous appelons cela "souscrire"; car le client ne reçoit qu'un seul message et non de futurs messages.

MODIFIER:

Pour vous assurer que l'abonnement a été réalisé, ce que j'ai essayé est le suivant:

côté SERVEUR:

Configuration:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").withSockJS();
    }
}

Contrôleur:

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        System.out.println("inside greeting");
        return new Greeting("Hello, " + message.getName() + "!");
    }

    @SubscribeMapping("/topic/greetings")
    public Greeting try1() {
        System.out.println("inside TRY 1");
        return new Greeting("Hello, " + "TRY 1" + "!");
    }
}

côté client:

...
    stompClient.subscribe('/topic/greetings', function(greeting){
                        console.log('RECEIVED !!!');
                    });
    stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
...

Ce que j'aimerais arriver:

  1. Lorsque le client s'abonne à '/topic/greetings', La méthode try1 Est exécutée.
  2. Lorsque le client envoie un msg à '/app/hello', Il doit recevoir le message de bienvenue qui serait @SendTo '/topic/greetings'.

Résultats:

  1. Si le client s'abonne à /topic/greetings, La méthode try1 Ne peut pas l'attraper.

  2. Lorsque le client envoie un msg à '/app/hello', La méthode greeting a été exécutée et le client a reçu le message de bienvenue. Nous avons donc compris qu'il avait été correctement souscrit à "/topic/greetings".

  3. Mais rappelez-vous que 1. a échoué. Après quelques essais, cela a été possible lorsque le client s'est abonné à '/app/topic/greetings', C'est-à-dire préfixé avec /app (Ceci est compréhensible par la configuration).

  4. Maintenant 1. fonctionne, mais cette fois 2. échoue: lorsque le client envoie un message à '/app/hello', Oui, la méthode greeting a été exécutée, mais le client n'a PAS reçu le message de salutations . (Parce que probablement maintenant le client était abonné au sujet avec le préfixe '/app', Ce qui n'était pas souhaité.)

Donc, ce que j'ai obtenu, c'est 1 ou 2 de ce que j'aimerais, mais pas ces 2 ensemble.

  • Comment y parvenir avec cette structure (configurer correctement les chemins de mappage)?
34
Mert Mertce

Par défaut, la valeur de retour d'une méthode @SubscribeMapping est envoyée sous forme de message directement au client connecté et ne pas passer par le courtier .

(c'est moi qui souligne)

Ici, la documentation de Spring Framework décrit ce qui se passe avec le message de réponse , pas le message SUBSCRIBE entrant.

Alors pour répondre à vos questions:

  • oui, le client est abonné au sujet
  • oui, les clients abonnés à ce sujet recevront un message si vous utilisez ce sujet pour l'envoyer
  • le courtier de messages est en charge de la gestion des abonnements

En savoir plus sur la gestion des abonnements

Avec le SimpleMessageBroker, l'implémentation du courtier de messages réside dans votre instance d'application. Les inscriptions d'abonnement sont gérées par le DefaultSubscriptionRegistry. Lors de la réception des messages, SimpleBrokerMessageHandler gère SUBSCRIPTION messages et enregistre les abonnements ( voir implémentation ici ).

Avec un "vrai" courtier de messages comme RabbitMQ, vous avez configuré un relais de courtier Stomp qui transmet les messages au courtier. Dans ce cas, les messages SUBSCRIBE sont transmis au courtier, en charge de la gestion des abonnements ( voir implémentation ici ).

Mise à jour - plus sur le flux de messages STOMP

Si vous jetez un oeil à la documentation de référence sur le flux de messages STOMP , vous verrez que:

  • Les abonnements à "/ topic/salutation" passent par le "clientInboundChannel" et sont transmis au courtier
  • Les messages d'accueil envoyés à "/ app/Message d'accueil" passent par le "clientInboundChannel" et sont transmis au GreetingController. Le contrôleur ajoute l'heure actuelle et la valeur de retour est transmise via le "brokerChannel" en tant que message à "/ topic/salutation" (la destination est sélectionnée en fonction d'une convention mais peut être remplacée via @SendTo).

Alors ici, /topic/hello est une destination de courtier; les messages qui y sont envoyés sont directement transmis au courtier. Tandis que /app/hello est une destination d'application et est censé produire un message à envoyer à /topic/hello, sauf si @SendTo dit le contraire.

Maintenant, votre question mise à jour est en quelque sorte différente, et sans un cas d'utilisation plus précis, il est difficile de dire quel modèle est le meilleur pour résoudre ce problème. Voici quelques-uns:

  • vous voulez que le client soit averti chaque fois que quelque chose se produit, de manière asynchrone: ABONNEZ-VOUS à un sujet particulier /topic/hello
  • vous souhaitez diffuser un message: envoyez un message à un sujet particulier /topic/hello
  • vous souhaitez obtenir un retour immédiat sur quelque chose, par exemple pour initialiser l'état de votre application: ABONNEZ-VOUS à une destination d'application /app/hello avec un contrôleur qui répond tout de suite avec un message
  • vous souhaitez envoyer un ou plusieurs messages à n'importe quelle destination d'application /app/hello: utilisez une combinaison de @MessageMapping, @SendTo ou un modèle de messagerie.

Si vous voulez un bon exemple, alors consultez cette application de chat montrant un journal des fonctionnalités de Websocket Spring avec un cas d'utilisation réel .

17
Brian Clozel

Donc, avoir les deux:

  • Utilisation d'un sujet pour gérer l'abonnement
  • Utilisation de @SubscribeMapping sur ce sujet pour fournir une réponse de connexion

ne fonctionne pas comme vous l'avez vécu (ainsi que moi).

La façon de résoudre votre situation (comme je l'ai fait la mienne) est:

  1. Supprimez @SubscribeMapping - cela ne fonctionne qu'avec le préfixe/app
  2. Abonnez-vous au/topic comme vous le feriez naturellement (sans préfixe d'application)
  3. Implémenter un ApplicationListener

    1. Si vous souhaitez répondre directement à un seul client, utilisez une destination utilisateur (voir websocket-stomp-user-destination ou vous pouvez également vous abonner à un sous-chemin d'accès, par exemple/topic/my-id-42, puis vous pouvez envoyer un message à ce sous-sujet (je ne connais pas votre cas d'utilisation exact, le mien est que j'ai des abonnements dédiés et je les répète si je veux faire un broadcast)

    2. Envoyer un message dans votre méthode onApplicationEvent de ApplicationListener dès que vous recevez un StompCommand.SUBSCRIBE

Gestionnaire d'événements d'abonnement:

@Override
  public void onApplicationEvent(SessionSubscribeEvent event) {
      Message<byte[]> message = event.getMessage();
      StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
      StompCommand command = accessor.getCommand();
      if (command.equals(StompCommand.SUBSCRIBE)) {
          String sessionId = accessor.getSessionId();
          String stompSubscriptionId = accessor.getSubscriptionId();
          String destination = accessor.getDestination();
          // Handle subscription event here
          // e.g. send welcome message to *destination*
       }
  }
15
Pauli

J'ai fait face au même problème et j'ai finalement basculé vers la solution lorsque je m'abonne aux deux /topic et /app sur un client, mettant en mémoire tampon tout ce qui a été reçu sur /topic gestionnaire jusqu'à /app- lié va télécharger tout l'historique du chat, c'est ce que @SubscribeMapping Retour. Ensuite, je fusionne toutes les entrées de discussion récentes avec celles reçues sur un /topic - il pourrait y avoir des doublons dans mon cas.

Une autre approche de travail consistait à déclarer

registry.enableSimpleBroker("/app", "/topic");
registry.setApplicationDestinationPrefixes("/app", "/topic");

Évidemment, pas parfait. Mais a fonctionné :)

5

Salut Mert, bien que votre question soit posée il y a plus de 4 ans, mais j'essaierai toujours d'y répondre car je me suis gratté la tête sur le même problème récemment et l'ai finalement résolu.

L'élément clé ici est @SubscribeMapping Est un échange de demande-réponse unique , donc la méthode try1() dans votre contrôleur ne sera déclenché qu'une seule fois juste après l'exécution des codes client

stompClient.subscribe('/topic/greetings', callback)

après cela, il n'y a aucun moyen de déclencher try1() par stompClient.send(...)

Un autre problème ici est que le contrôleur fait partie du gestionnaire de messages d'application, qui reçoit la destination avec le préfixe /app Déchiré, donc pour atteindre @SubscribeMapping("/topic/greetings") vous devez réellement écrire du code client comme celui-ci

stompClient.subscribe('/app/topic/greetings', callback)

puisque conventionnellement topic est mappé avec des courtiers pour éviter toute ambiguïté, je recommande de modifier votre code en

@SubscribeMapping("/greetings")

stompClient.subscribe('/app/greetings', callback)

et maintenant console.log('RECEIVED !!!') devrait fonctionner.

Le document officiel recommande également le scénario de cas d'utilisation de @SubscribeMapping Lors du rendu initial de l'interface utilisateur.

Quand est-ce utile? Supposons que le courtier est mappé sur/topic et/queue, tandis que les contrôleurs d'application sont mappés sur/app. Dans cette configuration, le courtier stocke tous les abonnements à/topic et/queue qui sont destinés à des diffusions répétées, et l'application n'a pas besoin de s'impliquer. Un client peut également s'abonner à une destination/app, et un contrôleur peut renvoyer une valeur en réponse à cet abonnement sans impliquer le courtier sans stocker ou réutiliser l'abonnement (en fait, un échange de demande-réponse unique). Un cas d'utilisation pour cela est de remplir une interface utilisateur avec des données initiales au démarrage.

1
mzoz

Ce n'est peut-être pas totalement lié, mais lorsque je m'abonnais à "app/test", il était impossible de recevoir des messages envoyés à "app/test".

J'ai donc trouvé que l'ajout d'un courtier était le problème (je ne sais pas pourquoi btw).

Voici donc mon code avant:

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic");
    }

Après :

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        // problem line deleted
    }

Maintenant, quand je m'abonne à "app/test", cela fonctionne:

    template.convertAndSend("/app/test", stringSample);

Dans mon cas, je n'en ai pas besoin de plus.

1
FloFlow