Utilisation des WebSockets sous Angular avec Java Spring Boot
Dans notre article sur la mise en place d’un Batch Spring, j’ai évoqué la possibilité de visualiser la progression d’exécution du Batch en utilisant le protocole WebSocket. Celui-ci permet d’amorcer un canal de communication bi-directionnel entre votre page web et le serveur via un socket TCP. L’avantage d’une telle connexion est de permettre au serveur d’émettre des notifications vers le client Web sans recevoir au préalable une requête de la part du client.
Dans cet article nous allons voir comment mettre en place, émettre et réceptionner un message émis par notre serveur Java pour permettre la lecture en temps réel depuis une application Web sous Angular.
Spring propose bien entendu une solution à l’utilisation de WebSocket. Vous devez d’abord injecter les dépendances suivantes dans votre projet Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
Configuration sous Spring Boot
Dans un premier temps vous devez définir une classe de configuration héritant de WebSocketMessageBrokerConfigurer
, cela permet à Spring Boot d’orienter les appelles utilisant le protocole WebSocket vers les endpoints dédiés de votre application Java.
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp").setAllowedOrigins("*");
}
}
3 notions ici :
setApplicationDestinationPrefixes
: définit le préfixe d’accès aux éventuels contrôleurs que les clients pourront consommer dans votre API.enableSimpleBroker
: définit le préfixe d’accès au flux émis par le broker pour les clients souhaitant s’y inscrire.addEndpoint
: définit le point d’entrée pour le handshake entre le client et le serveur. Cela permet d’établir la connexion ouverte entre les deux services. Remarquez le présence desetAllowedOrigins("*")
permettant de gérer les CORS lors de l’appel.
Contrairement aux API de type REST, les API WebSocket ne permettent pas de passer d’arguments supplémentaires dans les headers de la requête ce qui devient compliqué pour les autorisations. De ce fait il devient nécessaire d’ajouter un filtre supplémentaire à notre configuration de sécurité Spring :
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.cors()
.and()
.headers()
.frameOptions().disable()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/stomp").permitAll() // On autorise l'appel handshake entre le client et le serveur
.anyRequest()
.authenticated();
// @formatter:on
}
Il est maintenant possible d’émettre un appelle de n’importe où depuis notre API auquel les clients peuvent souscrire, il suffit d’ajouter la commande suivante :
messagingTemplate.convertAndSend("/topic/progress", "Hello world");
Le client qui a souscrit au message en provenance du endpoint « topic/progress » de notre API recevra la chaîne « Hello world ».
Dans l’exemple de projet fournit en bas de cet article j’ai ajouté, par commodité, l’envoi d’une notification dans un CommandLineRunner
situé dans le point d’entrée du projet Spring Boot. Ici, j’émet toutes les trois secondes deux valeurs que je pousse ensuite vers le client.
/**
* Generate random numbers publish with WebSocket protocol each 3 seconds.
* @return a command line runner.
*/
@Bean
public CommandLineRunner websocketDemo() {
return (args) -> {
while (true) {
try {
Thread.sleep(3*1000); // Each 3 sec.
progress.put("num1", randomWithRange(0, 100));
progress.put("num2", randomWithRange(0, 100));
messagingTemplate.convertAndSend("/topic/progress", this.progress);
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
Installation des WebSocket sous Angular
Vous avez besoin de deux composants dans votre application Angular pour utiliser les WebSocket de manière simplifiée:
npm i @stomp/ng2-stompjs --save
npm i @types/sockjs-client --save
Le premier composant est une librairie vous permettant de vous connecter à un Broker STOMP via le protocole WebSocket. C’est une surcharge du projet @stomp/stompjs permettant une meilleur compatibilité avec les projets Angular 6+. Le deuxième composant permet simplement d’injecter les définitions de type du client SockJs. Celui-ci est utilisé par @stomp/ng2-stompjs.
Vous devez ensuite fournir une initialisation du service depuis votre application Angular dans les providers du module.
import { RxStompService } from '@stomp/ng2-stompjs';
@NgModule({
[...]
providers: [
RxStompService
],
[...]
})
WebSocket Service
Nous avons maintenant besoin d’un service générique dédié à l’utilisation de WebSocket que l’on pourrait positionner ainsi dans notre projet Angular: src/app/services/websocket.service.ts
. Celui-ci prends en paramètre 3 éléments:
- Une injection du service
RxStompService
permettant l’initialisation d’accès au Broker de messagerie. - Une éventuelle configuration du service
RxStompService
via l’interfaceInjectableRxStompConfig
. - Une classe contenant d’éventuelles options supplémentaires, dont notamment le endpoint du broker.
import { InjectableRxStompConfig, RxStompService } from '@stomp/ng2-stompjs';
import { Observable } from 'rxjs';
import { SocketResponse, WebSocketOptions } from '../models';
/**
* A WebSocket service allowing subscription to a broker.
*/
export class WebSocketService {
private obsStompConnection: Observable<any>;
private subscribers: Array<any> = [];
private subscriberIndex = 0;
private stompConfig: InjectableRxStompConfig = {
heartbeatIncoming: 0,
heartbeatOutgoing: 20000,
reconnectDelay: 10000,
debug: (str) => { console.log(str); }
};
constructor(
private stompService: RxStompService,
private updatedStompConfig: InjectableRxStompConfig,
private options: WebSocketOptions
) {
// Update StompJs configuration.
this.stompConfig = {...this.stompConfig, ...this.updatedStompConfig};
// Initialise a list of possible subscribers.
this.createObservableSocket();
// Activate subscription to broker.
this.connect();
}
private createObservableSocket = () => {
this.obsStompConnection = new Observable(observer => {
const subscriberIndex = this.subscriberIndex++;
this.addToSubscribers({ index: subscriberIndex, observer });
return () => {
this.removeFromSubscribers(subscriberIndex);
};
});
}
private addToSubscribers = subscriber => {
this.subscribers.push(subscriber);
}
private removeFromSubscribers = index => {
for (let i = 0; i < this.subscribers.length; i++) {
if (i === index) {
this.subscribers.splice(i, 1);
break;
}
}
}
/**
* Connect and activate the client to the broker.
*/
private connect = () => {
this.stompService.stompClient.configure(this.stompConfig);
this.stompService.stompClient.onConnect = this.onSocketConnect;
this.stompService.stompClient.onStompError = this.onSocketError;
this.stompService.stompClient.activate();
}
/**
* On each connect / reconnect, we subscribe all broker clients.
*/
private onSocketConnect = frame => {
this.stompService.stompClient.subscribe(this.options.brokerEndpoint, this.socketListener);
}
private onSocketError = errorMsg => {
console.log('Broker reported error: ' + errorMsg);
const response: SocketResponse = {
type: 'ERROR',
message: errorMsg
};
this.subscribers.forEach(subscriber => {
subscriber.observer.error(response);
});
}
private socketListener = frame => {
this.subscribers.forEach(subscriber => {
subscriber.observer.next(this.getMessage(frame));
});
}
private getMessage = data => {
const response: SocketResponse = {
type: 'SUCCESS',
message: JSON.parse(data.body)
};
return response;
}
/**
* Return an observable containing a subscribers list to the broker.
*/
public getObservable = () => {
return this.obsStompConnection;
}
}
Ce service active à son initialisation un observable, createObservableSocket(),
regroupant la liste des éventuelles souscriptions au broker permettant l’envoi simultané à l’ensemble des souscrits des mises à jour du message émis par le serveur.
Le message émis peut ensuite être lu de n’importe où de la manière suivante :
const obs = this.progressWebsocketService.getObservable();
obs.subscribe({
next: this.onNewProgressMsg,
error: (err) => { console.log(err); }
});
Progress WebSocket Service
progressWebsocketService
est une classe héritant de WebSocketService
qui nous permet d’instancier un WebSocket personnalisé.
import { Injectable } from '@angular/core';
import { InjectableRxStompConfig, RxStompService } from '@stomp/ng2-stompjs';
import { WebSocketService } from '../websocket.service';
import { WebSocketOptions } from '../../models';
export const progressStompConfig: InjectableRxStompConfig = {
webSocketFactory: () => {
return new WebSocket('ws://localhost:8080/stomp');
}
};
@Injectable()
export class ProgressWebsocketService extends WebSocketService {
constructor(stompService: RxStompService) {
super(stompService, progressStompConfig, new WebSocketOptions('/topic/progress'));
}
}
Notre client va souscrire au endpoint « /topic/progress » de notre serveur pour écouter les notifications émises en temps réel. On remarque également l’initialisation de l’objet WebSocket
permettant l’utilisation du protocole avec le endpoint « ws://localhost:8080/stomp » correspondant au handshake entre le client et le serveur. Cela permet d’établir une connexion ouverte entre eux.
Pensez à rajouter votre service dans les providers de votre module Angular :
import { RxStompService } from '@stomp/ng2-stompjs';
import { ProgressWebsocketService } from './services/progress.websocket.service';
@NgModule({
[...]
providers: [
ProgressWebsocketService,
RxStompService
],
[...]
})
Conclusion
Nous avons vu dans cet article comment:
- paramétrer notre serveur Java Spring Boot pour intégrer les WebSockets et émettre une notification.
- ajouter un service générique utilisant le protocole WebSocket sous Angular
- personnaliser un service pouvant souscrire à une notification serveur spécifique.
Un projet GitHub est disponible pour consulter l’ensemble du code source : websocket-with-angular
Références
- Projet GitHub @stomp/ng2-stompjs: https://github.com/stomp-js/ng2-stompjs
- Client WebSocket: https://developer.mozilla.org/fr/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications
Si vous rencontrez le problème suivant :
Cela vient d’une mise à jour réalisée par l’auteur du composant stomp-js/rx-stomp. J’ai ouvert une issue sur son compte GitHub : https://github.com/stomp-js/rx-stomp/issues/207
En attendant vous pouvez contourner le problème de cette manière en surchargeant la classe
InjectableRxStompConfig
avec la config suivante :Il vous suffira ensuite de remplacer tous les appels à
InjectableRxStompConfig
avecFixedStompConfig
.