[JAVA] Exemple d'implémentation de programmation réactive Spring WebFlux (de base)

introduction

Dans le développement récent de services Web, afin d'améliorer la réutilisabilité des données back-end, il y a de plus en plus de cas où elles sont utilisées par divers clients en les convertissant en micro-service et en les fournissant comme API Web. Nous allons vous montrer comment organiser ces API backend et API de services externes afin qu'elles soient faciles à gérer sur le frontend, et implémenter de manière réactive la couche intermédiaire (appelée BFF. Backends For Frontends) qui les contrôle avec WebFlux de Spring Boot 2.

Réactif ... qu'est-ce qui est bon?

Les détails de la "programmation réactive" ne sont pas détaillés ici car ils sont détaillés dans d'autres articles, mais elle peut ** facilement implémenter des traitements non bloquants **.

Dans le Spring MVC précédent, un thread est affecté à une demande de traitement, donc dans le BFF (couche d'agrégateur d'API) expliqué cette fois, une API externe est appelée et attend une réponse ** "N'attendez rien. Time "** continuera à bloquer les threads. Par conséquent, vous devez générer un thread chaque fois que vous recevez une autre demande pendant le traitement.

En revanche, WebFlux ne bloque pas le thread du temps d'attente lors de l'appel d'API (= non bloquant) et peut effectuer un autre processus, afin que vous puissiez gérer efficacement les demandes avec un petit nombre de threads. ** **

Cependant, la mise en œuvre réactive est bizarre et vous pouvez avoir du mal à vous y habituer. Dans cet article, je présenterai quelques modèles d'implémentation susceptibles d'être utilisés comme exemples, j'espère donc que cela aidera ceux qui envisagent de commencer à partir de maintenant.

Que faire avec un échantillon

Dans cet exemple, nous allons créer un BFF qui collecte et renvoie l'API Web principale. Par conséquent, j'ai préparé un backend simple qui renvoie rapidement des données fictives, je vais donc l'utiliser.

Clonez à partir de ce qui précède, installez maven, puis démarrez l'application. Si vous accédez à http: // localhost: 8081 / categories et que JSON est renvoyé, il réussit.

Modèles et API gérés dans l'exemple

Tout d'abord, je vais aborder brièvement les modèles back-end gérés par BFF. Le back-end fournit des ressources pour une application de blog simple via l'API Web et a la configuration de modèle suivante.

Composition du modèle de blog

ブログアプリER図.png

** C'est un peu la force brute [^ 1] **, mais comme des API sont fournies pour chacune de ces entités, examinons une implémentation réactive pour combiner et revenir au front-end.

[^ 1]: Ces entités, qui seraient autrement pertinentes en tant que domaines, devraient également être fournies par le backend en tant qu'API d'article unique.

Environnement de développement

Créer un projet

Pour Intellij, suivez Spring initializr,

C'est OK si vous sélectionnez et créez. (Lombok n'est pas obligatoire, mais il est recommandé car il facilite la définition et la génération de POJO. N'oubliez pas d'installer le plugin lombok)

Cliquez ici pour le fichier de construction maven généré.

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>reactor</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>reactor</name>
    <description>Demo project for WebFlux</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Cet exemple de BFF créé cette fois est également dans un état où il peut être exécuté, veuillez donc vous y référer également.

Passons maintenant à la description de l'implémentation.

Appel API réactif

Jusqu'à présent, il était courant d'utiliser RestTemplate pour appeler l'API Web dans Spring MVC. WebFlux prend en charge les appels d'API non bloquants en utilisant WebClient.

Voici un exemple d'implémentation de WebClient. (Simplifié, j'utilise WebClient directement avec @Service au lieu de @Repository)

java:com.example.reactor.service.CategoryService.java



@Service
public class CategoryService {

    private static final String URL_BACKENDS_ROOT = "http://localhost:8081";

    private final WebClient webClient = WebClient.create();

    public Flux<Category> findAll() {
        return webClient.get()
                .uri(URL_BACKENDS_ROOT + "/categories")
                .retrieve()
                .bodyToFlux(Category.class);
    }

    public Mono<Category> read(String categoryId) {
        return webClient.get()
                .uri(URL_BACKENDS_ROOT + "/categories/{categoryId}", categoryId)
                .retrieve()
                .bodyToMono(Category.class);
    }
}

Il faut noter que le résultat du traitement de WebClient de findAll et read est encapsulé avecFlux <~>etMono <~>.

―― Représente plusieurs valeurs de retour telles que Flux <~> ... List <~>. [^ 2] --Mono <~> … Représente une valeur de retour.

[^ 2]: Flux <~> ne représente pas seulement plusieurs valeurs de retour, il prend également en charge le flux d'événements.

Voyons comment le contrôleur gère ces Flux <~> et Mono <~> par motif.

Appel d'API unique

Tout d'abord, examinons une implémentation qui appelle une API.

Traitement de l'image

activity_1.png

Exemple d'implémentation

java:com.example.reactor.controller.BlogController.java


@RestController
@RequestMapping("blog")
public class BlogController {

    @Autowired
    private CategoryService categoryService;

    @GetMapping("/categories")
    public Flux<Category> findCategories() {
        return categoryService.findAll();
    }
}

À première vue, c'est un RestController normal, mais comme la valeur de retour du contrôleur est Flux <~>, il recherche la liste des catégories sans blocage.

Plusieurs appels d'API séquentiels

Ensuite, regardons une implémentation qui appelle les deux API en séquence.

Traitement de l'image activity_1-1.png C'est un flux pour obtenir un utilisateur à partir d'un ID utilisateur et obtenir une liste d'en-têtes d'article pour cet utilisateur.

Exemple d'implémentation

java:com.example.reactor.controller.BlogController.java


@RestController
@RequestMapping("blog")
public class BlogController {

    @Autowired
    private UserService userService;

    @Autowired
    private HeaderService headerService;

    @GetMapping("/headers/find-by-user/{userId}")
    public Mono<HeadersByUser> findHeadersByUserId(@NotNull @PathVariable final String userId) {
        return userService.read(userId)
                .flatMap(user -> headerService.findByUserId(user.getUserId())
                        .collectList()
                        .map(headers -> HeadersByUser.builder()
                                .user(user)
                                .headers(headers)
                                .build()));
    }
}

ʻUserService # read () et HeaderService # findByUserId () `sont implémentés en frappant l'API avec WebClient comme le CategoryService précédent.

Le point ici est que lors de la recherche dans la liste des en-têtes de l'article après l'acquisition d'un utilisateur, le traitement séquentiel est connecté avec flatMap ().

return userService.read(userId)
    .flatMap(user -> /*Prochain processus réactif(Flux/Mono) */ )

De cette façon, si le processus suivant est un processus réactif qui renvoie également Flux / Mono, connectez-le avec flatMap ().

Inversement, au lieu de Flux / Mono, si vous voulez effectuer un post-traitement normal (remplissage avec le modèle de réponse dans l'exemple), connectez-vous avec map ().

return headerService.findByUserId(user.getUserId())
    .collectList()
    .map(headers -> /*Traitement normal suivant(Non-flux/Mono) */ )

Dans l'exemple, afin de renvoyer l'utilisateur acquis et l'en-tête d'article ensemble dans un seul objet, convertissez Flux <Header> en Mono <List <Header >> avec collectList () et ce qui suit HeadersByUser est défini dans le modèle.

java:com.example.reactor.resource.HeadersByUser.java


@Data
@Builder
public class HeadersByUser implements Serializable {
    private User user;
    private List<Header> headers;
}

En connectant Mono <HeadersByUser> à la ** valeur de retour du contrôleur de cette manière, un traitement séquentiel est exécuté pour la première fois **.

Au fait, Comme mentionné ci-dessus, l'implémentation de POJO est très simple car getter / setter est automatiquement généré par l'annotation @ Data de lombok. @ Builder est facile à gérer avec les expressions lambda car vous pouvez écrire les paramètres initiaux des propriétés au moment de l'instanciation dans la chaîne de méthodes.

À l'aide du générateur de lombok, instancier tout en initialisant la valeur sur une ligne


HeadersByUser model =
    HeadersByUser.builder().user(user).headers(headers).build();

Il existe également d'autres fonctions utiles. Pour plus de détails, reportez-vous à Cet article.

Plusieurs appels d'API parallèles

Examinons maintenant une implémentation qui appelle deux API en parallèle et attend.

Traitement de l'image activity_2.png Obtenez des informations sur la catégorie et la liste des en-têtes d'articles en parallèle depuis l'ID de catégorie

Exemple d'implémentation

java:com.example.reactor.controller.BlogController.java



@RestController
@RequestMapping("blog")
public class BlogController {

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private HeaderService headerService;

    @GetMapping("/headers/find-by-category/{categoryId}")
    public Mono<HeadersWithCategory> findHeadersByCategoryId(@NotNull @PathVariable final String categoryId) {
        return Mono.zip(
                    categoryService.read(categoryId), // T1
                    headerService.findByCategoryId(categoryId).collectList() // T2
                )
                .map(tuple2 -> {
                    final Category category = tuple2.getT1();
                    final List<Header> headers = tuple2.getT2();
                    return HeadersWithCategory.builder()
                            .category(category)
                            .headers(headers)
                            .build();
                });
    }
}

«CategoryService # read ()» et «HeaderService # findByCategoryId ()» sont des implémentations qui utilisent WebClient comme d'habitude et retournent «Mono » et «Flux

».

Lors du traitement en parallèle,

Mono.zip(Traitement mono 1,Traitement mono 2,...)

Est utilisé. Et chaque résultat de traitement réactif peut être obtenu à partir de l'objet Tuple2.

    .map(tuple2 -> {
        final Category category = tuple2.getT1();
        final List<Header> headers = tuple2.getT2();
        ...

Si le nombre de processus parallèles augmente à trois ou quatre, les tuples correspondants seront également «Tuple3», «Tuple4», etc., mais l'utilisation est la même. Vous pouvez obtenir le résultat du traitement en utilisant getT1 () ... T5 () correspondant à l'ordre spécifié dans Mono.zip ().

Dans l'exemple, le modèle HeadersWithCategory qui récapitule les catégories acquises et la liste des en-têtes d'article est défini et renvoyé ensemble à la fin du traitement parallèle.

java:com.example.reactor.resource.HeadersWithCategory.java



@Data
@Builder
public class HeadersWithCategory implements Serializable {
    private Category category;
    private List<Header> headers;
}

Appel d'API de combinaison séquentielle et parallèle

Enfin, jetons un coup d'œil à l'implémentation des appels d'API qui combinent séquentiel et parallèle.

activity_1-2.png Il s'agit d'un flux pour obtenir l'en-tête de l'article à partir de l'ID d'article, puis obtenir le contenu de l'article et la liste des commentaires.

java:com.example.reactor.controller.BlogController.java


@RestController
@RequestMapping("blog")
public class BlogController {

    @Autowired
    private HeaderService headerService;

    @Autowired
    private BodyService bodyService;

    @Autowired
    private CommentService commentService;

    @GetMapping("/entries/{entryId}")
    public Mono<Entry> getEntry(@NotNull @PathVariable Long entryId) {
        return headerService.read(entryId)
                .flatMap(header -> Mono.zip(
                            bodyService.read(header.getEntryId()), // T1
                            commentService.findByEntryId(header.getEntryId()).collectList() // T2
                        )
                        .map(tuple2 -> {
                            final Body body = tuple2.getT1();
                            final List<Comment> comments = tuple2.getT2();
                            return Entry.builder()
                                    .header(header)
                                    .body(body)
                                    .comments(comments)
                                    .build();
                        })
                );
    }
}

Il n'y a pas de supplément particulier. C'est une combinaison simple du traitement séquentiel et parallèle conventionnel.

La définition du modèle «Entrée» de la réponse est la suivante.

java:com.example.reactor.resource.Entry.java


@Data
@Builder
public class Entry implements Serializable {
    public Header header;
    public Body body;
    public List<Comment> comments;
}

Vérifiez le fonctionnement de l'échantillon que vous avez fait

Cet échantillon BFF peut être trouvé sur API Aggregator Sample (réacteur): GitHub. Après le clonage et l'installation de maven, vous pouvez vérifier l'opération avec le point de terminaison suivant en démarrant l'application. (Bien sûr, veuillez également démarrer backends)

échantillon point final
Appel d'API unique http://localhost:8080/blog/categories
Plusieurs appels d'API séquentiels http://localhost:8080/blog/headers/find-by-user/qiitaro
Plusieurs appels d'API parallèles http://localhost:8080/blog/headers/find-by-category/java
Appel d'API de combinaison séquentielle et parallèle http://localhost:8080/blog/entries/1

Points à noter

Enfin, je résumerai les points à prendre en compte lors de la mise en œuvre.

N'utilisez pas block ()

Avec block (), vous obtenez une implémentation synchrone familière comme celle-ci: Cependant, cette implémentation "bloquera et attendra que le traitement revienne" pour le traitement asynchrone.

    @GetMapping("/headers/find-by-user/{userId}")
    public Mono<HeadersByUser> findHeadersByUserId(@NotNull @PathVariable final String userId) {
        
        User user = userService.read(userId).block();
        
        List<Header> headers =
                headerService.findByUserId(user.getUserId())
                        .collectList()
                        .block();
        
        HeadersByUser response =
                HeadersByUser.builder()
                        .user(user)
                        .headers(headers)
                        .build();
        
        return Mono.just(response);
    }

N'utilisez pas WebFlux car il abandonnera le plus grand avantage.

En passant, si le serveur est Netty, le traitement à l'aide de block () n'est pas pris en charge, donc une erreur se produira en premier lieu. Veuillez noter qu'il est disponible pour Tomcat. (Si vous le configurez selon la procédure de cet article, ce sera Netty)

Le traitement réactif est connecté à la fin (valeur de retour du contrôleur)

J'avais l'intention d'écrire en connectant les processus séquentiellement ou en parallèle, mais lorsque j'essaye de le déplacer, cela peut ne pas fonctionner du tout. Dans un tel cas, vérifiez si le processus réactif qui renvoie "Flux / Mono" est correctement connecté au processus suivant.

Même s'il n'y a pas de valeur de retour, il est nécessaire de renvoyer le type Mono <Void> à la valeur de retour du contrôleur et de le connecter.


C'est la fin de cette édition de base. BFF en tant qu'agrégateur d'API peut être créé dans une certaine mesure par la méthode ci-dessus, mais il sera nécessaire d'en tenir compte un peu plus pour les opérations réelles telles que la gestion des erreurs, les tentatives et la gestion de session.

J'expliquerai cela dans les entrées suivantes et suivantes. Merci beaucoup.

Recommended Posts

Exemple d'implémentation de programmation réactive Spring WebFlux (de base)
Ressources de programmation de base