Introduction à Spring Reactor et Spring Webflux
Introduction
Ce chapitre présente brièvement les nouveautés réactives de Spring. Les applications réactives avec Spring sont traitées plus en détail dans l’ouvrage Java Spring - Construisez vos applications réactives avec une architecture microservices en environnement Jakarta EE (2e édition) aux Éditions ENI.
L’objectif de la programmation réactive est de simplifier l’écriture d’applications asynchrones qui, au lieu d’utiliser le mécanisme classique d’un volumineux pool de threads bloquants, utilise une boucle et un nombre minimum de threads grâce à un moteur basé sur un framework NIO comme Netty.
Nous plaçons dans une file d’attente des traitements rapides à effectuer, et ils se font au fil de l’eau. Il devient alors possible de modéliser ces traitements sous la forme de flux et de propagations de changements. Nous pouvons gérer des flux statiques avec des tableaux ou dynamiques avec des émetteurs et récepteurs d’événements comme dans le design pattern observateur.
Dans un contexte avec des serveurs au sein d’un cluster élastique, ce type de traitement « sériel » permet de savoir si la charge sur un serveur est en adéquation avec la puissance du cluster via la backpressure qui mesure le volume du flux...
Spring Reactor
1. Présentation
Historiquement nous avions RxJava 1 et 2 pour faire de la programmation réactive.
Spring a décidé d’implémenter Reactor, sa propre version de 4e génération d’un moteur réactif afin de pouvoir utiliser au mieux les technologies actuelles.
Reactor est particulièrement bien adapté à Spring. Il peut facilement s’interfacer avec RxJava et avec l’équivalent du JDK9 : java.util.concurrent.Flow.
Une application réactive est caractérisée par ces particularités :
-
Réactif : fournit des temps de réponse rapides et cohérents.
-
Résilient : reste réactif en cas de panne et doit se rétablir opérationnellement.
-
Élastique : reste réactif et maintient sa réactivité et peut s’adapter à diverses charges de travail.
-
Message Driven : communique à l’aide de messages asynchrones.
Les applications réactives ne sont pas plus rapides que les applications traditionnelles. Elles ont par contre un comportement beaucoup mieux maîtrisé et beaucoup plus prévisible quand le serveur vient à saturer. Bien que l’application Java soit saturée, le système hôte reste parfaitement accessible. Les applications sont plus intuitives à programmer et mieux structurées grâce à la programmation fonctionnelle.
Nous avons d’un côté le Publisher qui produit un ou plusieurs éléments qui sont ensuite consommés par un Consumer. Il faut considérer que ces éléments échangés sont des événements.
Il est à noter que la programmation est asynchrone. Le code n’est plus exécuté de façon séquentielle. Deux traitements de flux qui se succèdent dans le code sont lancés en parallèle. Nous n’attendons pas la fin du traitement d’un flux pour passer au flux suivant. Paradoxalement, le système ne lance qu’un traitement à la fois dans sa file d’attente. Il faudra donc synchroniser les flux.
Il existe deux types de Publisher :
-
Celui qui émet 0 ou 1 élément :
le Mono : reactor.core.publisher.Mono<T>...
WebFlux
Nous utiliserons généralement Reactor dans le cadre d’applications WebFlux.
En effet les API réactives sur des flux d’événements statiques sont souvent remplaçables par les streams traditionnels. WebFlux est l’équivalent réactif de Spring MVC et utilise le moteur Reactor en interne.
Concevoir de zéro une application WebFlux est complexe. Depuis peu, JHipster permet de générer des applications complètes l’utilisant, et son utilisation peut faire gagner beaucoup de temps.
Pour utiliser Spring WebFlux, il faut ajouter la dépendance Maven :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Un exemple complet généré avec JHipster est disponible dans les exemples téléchargeables de l’ouvrage. Les détails le concernant sont étudiés dans un chapitre dédié.
1. Définition du terme réactif
Les notions de réactivité dans WebFlux sont de plusieurs ordres. Plutôt que d’allouer ou associer via un pool un thread à une requête qui gère l’intégralité du traitement de la requête jusqu’à sa réponse, nous découpons le traitement en petits éléments qui se placent dans une file d’attente et s’exécutent au plus tôt.
Nous traitons cet élément dans un environnement asynchrone non bloquant.
Serveur Spring MVC |
Serveur Spring WebFlux |
Dans un serveur Spring MVC, nous traitons les requêtes tant qu’il y a des threads disponibles. Quand ces derniers viennent à manquer, nous faisons attendre le client HTTP. Le client attend une connexion disponible. |
Dans un serveur Spring WebFlux, nous ne bloquons pas les clients. S’il survient trop d’appels, nous saturons également le système, mais à un autre endroit, au niveau de la boucle d’événements. |
La différence se situe principalement dans le fait que nous n’avons pas de threads bloqués qui attendent le résultat d’une étape un peu longue.
Nous pourrions croire que nous avons l’équivalent en utilisant...
Client réactif
Nous pouvons utiliser la classe WebClient qui a été introduite dans Spring 5 et qui est l’équivalent du RestTemplate traditionnel. C’est un client non bloquant prenant en charge les flux réactifs et qui permet de récupérer les données des endpoints fournis par le contrôleur WebFlux. La classe WebClient va remplacer le RestTemplate qui va devenir déprécié sous peu.
Créons un simple UtilisateurWebClient :
public class UtilisateurWebClient { WebClient client =
WebClient.create("http://localhost:8080");
// ...
}
Récupération d’une seule ressource :
Mono<Utilisateur> utilisateurMono = client.get()
.uri("/utilisateurs/{id}", "1")
.retrieve()
.bodyToMono(Utilisateur.class);
utilisateurMono.subscribe(System.out::println);
Récupération d’une collection :
Flux<Utilisateur> utilisateursFlux = client.get()
.uri("/utilisateurs")
.retrieve()
.bodyToFlux(Utilisateur.class);
employeeFlux.subscribe(System.out::println);
Tests avec WebFlux
Spring a mis à disposition un certain nombre d’outils pour faciliter les tests.
1. Tests unitaires
Les tests unitaires correspondent aux tests de bas niveau.
a. Tests unitaires avec des applications réactives
Nous pouvons utiliser le traditionnel Mockito.
Pour une classe :
public class UtilisateurService {
public Mono<Utilisateur> getUtilisateurById(Integer utilisateurId) {
return webClient
.get()
.uri("http://localhost:8080/utilisateur/{id}", utilisateurId)
.retrieve()
.bodyToMono(Utilisateur.class);
}
}
Nous pouvons écrire le test unitaire suivant :
@ExtendWith(MockitoExtension.class)
public class UtilisateurServiceTest {
@Test
void givenUtilisateurId_whenGetUtilisateurById_thenReturnUtilisateur() {
Integer utilisateurId = 100;
Utilisateur mockUtilisateur = new Utilisateur(1, "Toto", 51);
when(webClientMock.get())
.thenReturn(requestHeadersUriSpecMock);
when(requestHeadersUriMock.uri("/utilisateur/{id}", utilisateurId))
.thenReturn(requestHeadersSpecMock);
when(requestHeadersMock.retrieve())
.thenReturn(responseSpecMock);
when(responseMock.bodyToMono(Utilisateur.class))
.thenReturn(Mono.just(mockUtilisateur));
Mono<Utilisateur> utilisateurMono =
utilisateurService.getUtilisateurById(utilisateurId);
StepVerifier.create(utilisateurMono)
.expectNextMatches(utilisateur -> utilisateur.getNom()
.equals("Toto"))
.verifyComplete();
}
}
b. Utilisation de MockWebServer
MockWebServer a été mis au point par l’équipe Squaere dont vous trouverez le travail sur le site https://github.com/square/okhttp/tree/master/mockwebserver.
Le MockWebServer est un serveur web scriptable permettant de tester les clients HTTP, par l’envoi de requêtes et la vérification...
Server Site Event avec Spring
Il faut souvent, dans nos applications, faire des rafraîchissements pour des appels asynchrones. Il existe pour cela plusieurs technologies. La plus simple est le pooling qui consiste à interroger régulièrement le serveur pour connaître l’état d’avancement d’un traitement. Une autre évolution est le long pooling qui consiste à garder ouverte la connexion HTTP, mais ces deux techniques sont dispendieuses au niveau du réseau. Une évolution existe avec les webhooks et le Publish-Subscribe. Nous avons ensuite les WebSockets qui sont bidirectionnelles. Nous avons aussi le Server-Sent Event, ou SSE en abrégé, qui est une norme HTTP permettant à une application web de gérer un flux d’événements unidirectionnel et de recevoir des mises à jour chaque fois que le serveur émet des données. Cette solution est souvent la mieux adaptée dans un contexte web qui ne requiert pas l’aspect temps réel des WebSockets. Nous sommes alors en léger différé, mais cela est parfois soutenable.
La version Spring 4.2 supportait déjà le SSE, mais, à partir de Spring 5, il existe un moyen plus idiomatique et pratique de le gérer via un flux et un élément ServerSiteEvent grâce aux API réactives de Spring WebFlux. On crée...
Pour aller plus loin
L’adoption des applications réactives est très lente. Ce sont a priori les mêmes freins que pour l’utilisation du Domain Driven Design, des architectures hexagonales, de l’Event Sourcing etc. Pour le moment, il est souvent jugé plus simple de travailler de façon traditionnelle, même si cela implique plus de serveurs.
Spring met à disposition un ensemble complet de fonctionnalités, fonctionnant avec Java et Kotlin. Les serveurs les plus innovants sont en Kotlin, utilisent les API réactives et fonctionnent avec des Flux dans le Cloud. Les applications réactives utilisent souvent des bases Kafka et Cassandra et sont basées sur des événements.
Le nombre de serveurs devient un critère important quand on utilise un Cloud Amazon, Google, Azure ou autre, car on cherche à réduire le nombre de machines.
Points clés
Points à retenir :
-
L’adoption des applications réactives est lente.
-
Reactor est un moteur qui permet de programmer des applications réactives.
-
WebFlux est l’équivalent de Spring MVC dans le monde réactif.
-
WebFlux support la backpressure.
-
Pour faire un web service réactif il faut une base de données qui supporte les API réactives.
-
Spring Data peut être utilisé pour les repository réactifs.
-
R2DBC peut être utilisé pour les bases de données SQL réactives supportées.