Programmation réactive

Découvrir la programmation réactive

Afin d'illustrer la programmation réactive, nous allons utiliser le framework reactor.

Travail à faire :
  • Ajouter les dépendances MAVEN suivantes :
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-core</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
    
  • Créer, dans src/main/java, un package myboot.app6.
  • Créer, dans src/test/java, un package myboot.app6.test.

Traitement d'une donnée unique

Dans la programmation réactive, les actions sont toutes asynchrones et déclenchées par l'arrivée d'une donnée à traiter. Cette donnée peut être unique (ou absente) ou bien multiple. Dans Reactor, les données uniques sont codées par la classe Mono. Voici un exemple 

package myboot.app6.test;

import org.junit.jupiter.api.Test;

import reactor.core.publisher.Mono;

public class testReactor {

    @Test
    public void testMono() {
        System.out.println("-- test Mono");
        Mono.just(10)//
                .subscribe(System.out::println);
    }

}

L'installation d'un consommateur de donnée (avec la méthode subsribe) active le traitement et la donnée 10 est transmise à println.

Travail à faire :
  • Lire la documentation de Mono et tester la méthode log.
    ...
            Mono.just(10)//
                    .log()//
                    .subscribe(System.out::println);
    ...
    
  • La méthode block termine la chaîne des traitements asynchrones et renvoie la valeur résultante. C'est donc une action bloquante. Tester cette possibilité.
  • Nous pouvons maintenant appliquer un traitement (asynchrone) sur cette donnée avec la méthode map :
    ...
            Mono.just(10)//
                    .map(i -> i + 10)//
                    .subscribe(System.out::println);
    ...
    
  • Nous pouvons aussi faire en sorte que le traitement change le type de la donnée transmise :
    ...
            Mono.just(10)//
                    .map(v -> v + 10)//
                    .map(v -> "Message " + v)//
                    .map(v -> v.toUpperCase())//
                    .subscribe(System.out::println);
    ...
    
  • Nous pouvons nous abonner à des événements intéressants. Essayer les méthodes doFirst, doOnNext et doAfterTerminate.

Traitement de données multiples

Dans Reactor, les données multiples sont traitées par la classe Flux. Voici trois exemples :

Flux.just(1, 2, 3)//
        .subscribe(System.out::println);

Flux.range(1, 10)//
        .subscribe(System.out::println);

Mono.just(5)//
        .repeat(10)//
        .subscribe(System.out::println);
Travail à faire :
  • Tester la méthode filter. Utiliser les méthodes bloquantes blockFirst et blockLast pour développer des tests unitaires. Utiliser ensuite collectList et block.
  • Tester les méthodes all et any. Utiliser la méthode block pour développer des tests unitaires.
  • Utiliser ensuite sort et distinct pour filtrer un flux.
  • Agrandir ensuite un flux avec concatWith et concatWithValues.
  • Ajouter un traitement (map) qui va freiner le flux pour observer les effets sur les étapes suivantes.
  • Avec (flatMap) ajouter la production d'un flux.
  • Tester la création d'un buffer avec buffer(n), bufferUntil et bufferUntilChanged
  • Avec zipWith créer un flux qui résulte du traitement de deux autres flux. Quel est le résultat si les flux n'ont pas la même taille ?
  • Avec zip (méthode statique) agréger deux (ou trois) autres flux. Quel est le résultat ?
  • Utiliser les méthodes then et thenMany pour enchaîner la fin des traitements d'un Flux avec d'autres traitements sur un autre flux.

Traitement des erreurs

Travail à faire :
  • Générer un flux d'entiers entre 1 et 20. Ajouter un traitement qui va générer un erreur sur les entiers entre 10 et 15. Quel est le résultat ?
  • Utiliser la méthode onErrorContinue pour capter l'erreur.

Utiliser Spring Data avec Redis

Installer Redis sur les machines de la faculté

Travail à faire : Nous allons lancer redis dans un conteneur docker. Suivez la procédure ci-dessous sur une machine de la faculté :
  • Lancez le gestionnaire de machines virtuelles.
  • Choisissez le menu Fichier / Nouvelle machine virtuelle.
  • Choisissez Importer depuis le partage AMU.
  • Choisissez jullien.c.1 / docker+minikube.
  • Nommez votre VM redis-vm.
  • Lancez votre VM redis-vm.
  • Connectez-vous en utilisant le login qamu et le mot de passe qamu.
  • Créez un conteneur qui va héberger le serveur REDIS. Ce serveur écoute les requêtes qui arrivent sur le port 6379.
docker run --name my-redis -p 6379:6379 -d redis --requirepass "mypass"
  • Vérifiez le conteneur :
docker ps -f name=my-redis
  • Récupérez et notez l'adresse IP de redis-VM (sans doute 192.*) avec
ip addr

Installer Redis sans docker

Travail à faire : Si vous travaillez sur votre machine sans une infrastructure docker, commencez, dans une autre fenêtre, à charger et à compiler la dernière version de Redis (à partir des sources).

Utiliser Spring-Data

Enrichir la configuration de votre projet :
  • Créez le package myboot.app6.repo.
  • Ajoutez la dépendance ci-dessous :
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • Ajouter les lignes ci-dessous à votre fichier application.properties :
# config du serveur Redis
spring.data.redis.host=<adresse IP de redis-vm>
spring.data.redis.port=6379
spring.data.redis.password=mypass
spring.data.redis.database=0
spring.data.redis.timeout=60000
  • Enrichissez la configuration avec l'activation de Spring-Data-Redis :
package myboot.app6.repo;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

import myboot.Starter;

@Configuration
@EnableRedisRepositories(basePackageClasses = Starter.class)
public class ConfigRedis {       

}
POJO, dépôt et test :
  • Créez un POJO afin de représenter la donnée à sauvegarder :
package myboot.app6.repo;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@RedisHash("Student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {

    public enum Gender {
        MALE, FEMALE
    }

    @Id
    private String id;
    private String name;
    private Gender gender;
    private int grade;
}
  • Définissez ensuite le dépôt Spring-Data :
package myboot.app6.repo;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends CrudRepository<Student, String> {
}
  • Testez ce composant avec le code ci-dessous :
package myboot.app6.test;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import myboot.app6.repo.Student;
import myboot.app6.repo.StudentRepository;

@SpringBootTest
public class TestRedis {

    @Autowired
    StudentRepository repo;

    @Test
    public void testSaveStudent() {
        final String key = "Eng2015001";

        Student student = new Student(key, "John Doe", Student.Gender.MALE, 1);
        repo.save(student);

        assertEquals(student.getName(), repo.findById(key).get().getName());
    }
}
Le client Redis en ligne de commande :
  • Lancez le client dans un conteneur (c'est un shell qui accepte des commandes) :
docker run -it --name my-redis-cli \
   --link my-redis:redis --rm redis redis-cli -h redis -p 6379 -a mypass
  • Vous pouvez ainsi suivre les modifications de la base avec la commande MONITOR.
  • Vérifier l'insertion des données dans la base Redis (commandes KEYS * et HGETALL Student:Eng2015001).
  • Enrichissez votre test avec des essais de suppressions et de listage.