Développer un diffuseur de messages dans une application Grails 3 avec WebSocket et STOMP
Imaginez que vous vouliez développer une application web capable de diffuser des messages (comme des nouvelles par exemple) à des utilisateurs qui y souscriraient, et que ces messages apparaissent pratiquement instantanément dans leurs navigateurs, par exemple dans une chronologie graphique.
C'est tout le propos de l'application Grails 3 odelia-grails-broadcaster dont je vous partage le code source, et qui utilise le plugin Spring Websocket Grails Plugin.
Grâce à ce plugin qui facilite l'utilisation du support de STOMP (Simple
Text Oriented Messaging Protocol) sur WebSocket du framework Spring 4 dans une application Grails, on peut échanger facilement des messages entre un serveur et un navigateur web.
De plus, Spring Websocket Grails Plugin vient avec une configuration par défaut (utilisant un simple broker de messages en mémoire) qui permet d'être très vite opérationnel.
odelia-grails-broadcaster comprend un contrôleur, TimelineController
, et deux vues :
- la vue par défaut
index.gsp
, associée à l'action de contrôleurindex
, destinée à présenter les messages émis dans une chronologie visuelle élégante et responsive (grâce à Vertical Timeline) ; - la vue
broadcast.gsp
, qui permet d'afficher un simple formulaire pour saisir les éléments du message à diffuser auprès des abonnés ; lorsque le formulaire est validé, les données sont transmises à l'actionbroadcastMessage
.
Diffuser un message (Post)
L'action broadcastMessage
du contrôleur TimelineController
est chargée de créer une nouvelle instance de la classe métier odelia.grails.broadcaster.Post
avec les données soumises par le formulaire, de la sauvegarder en base de données, puis de la diffuser vers la destination /topic/channel
, au moyen du bean Spring de type SimpMessagingTemplate
, avec l'appel de méthode convertAndSend
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Le second argument passé pour l'appel convertAndSend
est de type String
et correspond à la représentation chaîne de caractères d'un objet Post
sérialisé au format JSON ; les clients qui auront souscrit au topic /topic/channel
recevront une frame STOMP contenant la représentation JSON de l'objet diffusé (payload).
Notez qu'il est aussi possible de transmettre un POJO/POGO qui serait converti automatique en JSON.
Dans l'application, j'ai choisi de personnaliser la conversion au format JSON au moyen du convertisseur grails.converters.JSON
. La manière de convertir un objet de type Post
en JSON est spécifié dans la classe BootStrap.groovy
ainsi :
1 2 3 4 5 6 7 8 9 10 |
|
La classe métier Post
, qui est déclarée dans le fichier source grails-app\domain\odelia\grails\broadcater\Post.groovy
se présente comme ceci :
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Connexion et souscription client
Tout utilisateur qui ouvrira la page web d'URL localhost:8080/timeline
(en supposant une exécution locale du conteneur web, et l'utilisation du port par défaut) avec son navigateur Internet, verra les Posts déjà diffusés, et cette page sera mise à jour automatiquement lors de l'arrivée d'un nouveau message.
C'est le code JavaScript du fichier timeline.js
référencé dans la page qui le permet : en effet, cette page est générée dynamiquement, au moyen de la vue grails-app\view\timeline\index.gsp
qui inclut notamment le tag <asset:javascript src="timeline.js"/>
.
Voici un extrait du fichier grails-app\assets\javascripts\timeline.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Notez tout d'abord que la dépendance à librairie spring-websocket
autorise l'utilisation de STOMP sur WebSocket, au moyen de la librairie SockJS pour la partie WebSocket. Cette dernière va permettre de pallier, entre autres, au manque éventuel du support de WebSocket dans un navigateur donné.
Lors du chargement de la page, le code ci-dessus établit la connexion avec la partie serveur, puis souscrit au topic /topic/channel
, de manière à réagir à la diffusion d'un nouveau Post
; lorsqu'un message parvient, celui-ci est converti au format JSON et ensuite ajouté à notre timeline qui se présente sous la forme d'une liste <ul>
.
La fonction JavaScript addPost
(voir le code source du projet pour avoir le code complet) est chargée de l'ajout du nouvel élément, en utilisant un mécanisme simple de modèle ; il serait bien sûr plus judicieux de mettre en application une librairie plus adaptée à ce type de traitement.
Obtenir la liste initiale
Une question demeure : comment obtenir la liste initiale des Post
s déjà diffusés, lorsqu'un client se connecte pour le première fois ?
C'est là qu'intervient la seconde souscription, à /app/channel.messages
, apparaissant dans le code JavaScript ; en réponse à celle-ci, la partie serveur (la méthode de contrôleur retrievePosts
) renvoie la liste des Post
s sauvegardés, et dans la fonction JavaScript qui traite cette réponse, on peut donc initialiser la timeline avec une série d'appels à addPost
.
Pour la partie serveur, la méthode retrievePosts
du contrôleur TimelineController
se charge de renvoyer la liste des Post
s stockés en base de données :
1 2 3 4 5 6 7 |
|
L'annotation org.springframework.messaging.simp.annotation.SubscribeMapping
sur la méthode retrievePosts
permet simplement d'indiquer que celle-ci doit être appelée lors de la souscription d'un client à /app/channel.messages
. L'implémentation de retrievePosts
consiste donc à retourner la représentation JSON de la liste des Post
s, sous la forme d'une chaîne de caractères.
Quelques remarques concernant retrievePosts
:
- la méthode
retrievePosts
est marquéeprotected
, et n'est donc pas exposée en tant qu'action ; - on recourt à la puissance de GORM pour obtenir la liste des objets persistés
Post
, en appelant la méthode statique synthétiséelistOrderByDate
; - la conversion des éléments
Post
de la collection retournée parlistOrderByDate
est simplifiée par l'application du convertisseur JSON (avec l'opérateur Groovyas
) ; - il est nécessaire de passer par une nouvelle session Hibernate par l'appel à la méthode statique
Post.withNewSession
, tandis que le code qui fait appel à GORM est encapsulé dans la Closure Groovy passée à cette méthode.
Ressources
Pour aller plus loin, je vous renvoie à la documentation de Spring, mais aussi au chapitre Messaging with WebSocket and STOMP du livre Spring in Action, Fourth Edition.
Par ailleurs, le projet spring-websocket-chat est aussi une grande source d'inspiration !