web-dev-qa-db-fra.com

Manière correcte de lancer des exceptions avec Reactor

Je suis nouveau sur Project Reactor et sur la programmation réactive en général.

Je travaille actuellement sur un morceau de code similaire à ceci:

Mono.just(userId)
    .map(repo::findById)
    .map(user-> {
        if(user == null){
            throw new UserNotFoundException();
        }
        return user;
    })
    // ... other mappings

Cet exemple est probablement stupide et il existe sûrement de meilleurs moyens de mettre en œuvre cette affaire, mais le problème est le suivant:

Est-ce une erreur d'utiliser une exception throw new Dans un bloc map ou dois-je le remplacer par un return Mono.error(new UserNotFoundException())?

Existe-t-il une différence réelle entre ces deux façons de faire?

17
davioooh

Il y a deux façons de considérer le moyen d'exécuter une exception:

Manipulez votre élément avec Flux/Mono.handle

L’opérateur handle est l’un des moyens permettant de simplifier le traitement d’un élément pouvant entraîner une erreur ou un flux vide.

Le code suivant montre comment nous pouvons l'utiliser pour résoudre notre problème:

Mono.just(userId)
    .map(repo::findById)
    .handle((user, sink) -> {
        if(!isValid(user)){
            sink.error(new InvalidUserException());
        } else if (isSendable(user))
            sink.next(user);
        }
        else {
            //just ignore element
        }
    })

comme on peut le voir, le .handle l'opérateur doit passer BiConsumer<T, SynchronousSink<> afin de gérer un élément. Nous avons ici deux paramètres dans notre BiConsumer. Le premier est un élément de l'amont où le second est SynchronousSink, ce qui nous aide à fournir un élément en aval de manière synchrone. Une telle technique étend la capacité à fournir différents résultats du traitement de notre élément. Par exemple, si l'élément est invalide, nous pouvons fournir une erreur au même SycnchronousSync qui annulera en amont et produira le signal onError en aval. À son tour, nous pouvons "filtrer" en utilisant le même opérateur handle. Une fois que le handle BiConsumer est exécuté et qu'aucun élément n'a été fourni, Reactor considérera cela comme une sorte de filtrage et demandera un élément supplémentaire pour nous. Enfin, si l'élément est valide, nous pouvons simplement appeler SynchronousSink#next et propager notre élément en aval ou appliquer un mappage dessus, nous aurons donc handle comme opérateur map. De plus, nous pouvons utiliser en toute sécurité cet opérateur sans impact sur les performances et fournir une vérification d'éléments complexe telle que la validation d'élément ou l'envoi d'erreur en aval.

Jette avec #concatMap + Mono.error

Une des options permettant de lever une exception lors du mappage consiste à remplacer map par concatMap. Dans son essence, concatMap fait presque la même chose que flatMap. La seule différence est que concatMap n'autorise qu'un sous-flux à la fois. Un tel comportement simplifie beaucoup l’implémentation interne et n’affecte pas les performances. Nous pouvons donc utiliser le code suivant pour générer une exception de manière plus fonctionnelle:

Mono.just(userId)
    .map(repo::findById)
    .concatMap(user-> {
        if(!isValid(user)){
            return Mono.error(new InvalidUserException());
        }
        return Mono.just(user);
    })

Dans l'exemple ci-dessus, en cas d'utilisateur non valide, nous renvoyons une exception en utilisant Mono.error. La même chose que nous pouvons faire pour flux en utilisant Flux.error:

Flux.just(userId1, userId2, userId3)
    .map(repo::findById)
    .concatMap(user-> {
        if(!isValid(user)){
            return Flux.error(new InvalidUserException());
        }
        return Mono.just(user);
    })

Remarque, dans les deux cas, nous retournons cold stream qui n'a qu'un seul élément. Dans Reactor, plusieurs optimisations améliorent les performances dans le cas où le flux renvoyé est un flux froid scalaire. Ainsi, il est recommandé d’utiliser Flux/Mono concatMap + .just, empty, error lorsque nous avons besoin de mappages plus complexes, pouvant aboutir à return null ou throw new ....

Attention! Ne jamais vérifier l'élément entrant sur la nullité. Le projet Reactor n'enverra jamais de valeur null car il enfreint les spécifications des flux réactifs (voir règle 2.1 ) . Ainsi, dans le cas où repo.findById renvoie null, Reactor lève une exception NullPointerException à votre place.

Attendez, pourquoi concatMap est-il meilleur que flatMap?

Essentiellement, flatMap est conçu pour fusionner des éléments provenant de plusieurs sous-flux exécutés simultanément. Cela signifie que flatMap doit avoir des flux asynchrones en dessous afin de pouvoir potentiellement traiter des données sur plusieurs threads ou qu'il puisse s'agir de plusieurs appels réseau. Par la suite, de telles attentes ont un impact important sur la mise en œuvre. flatMap devrait donc être en mesure de gérer les données provenant de plusieurs flux (Threads) (signifie l’utilisation de structures de données simultanées). un autre flux (signifie une allocation de mémoire supplémentaire pour Queues pour chaque sous-flux) et ne viole pas les règles de spécification de Reactive Streams (signifie une implémentation très complexe). En comptant tous ces faits et le fait que nous remplaçons une opération simple map (qui est synchrone) sur le moyen plus pratique de lancer une exception en utilisant Flux/Mono.error (qui ne change pas la synchronicité d’exécution) fait que nous n’avons pas besoin d’un opérateur aussi complexe et que nous pouvons utiliser beaucoup plus simple concatMap qui est conçu pour la gestion asynchrone d’un seul flux à la fois et a quelques optimisations pour gérer les flux scalaires et froids.

Lève une exception en utilisant switchOnEmpty

Donc, une autre approche pour lancer une exception lorsque le résultat est vide est l'opérateur switchOnEmpty. Le code suivant montre comment nous pouvons utiliser cette approche:

Mono.just(userId)
    .flatMap(repo::findById)
    .switchIfEmpty(Mono.error(new UserNotFoundExeception()))

Comme on peut le voir, dans ce cas repo::findById devrait avoir Mono sur User comme type de retour. Par conséquent, dans le cas où une instance User ne serait pas trouvée, le flux de résultats serait vide. Ainsi, Reactor appellera une alternative Mono, spécifiée comme paramètre switchIfEmpty.

Jetez votre exception telle quelle

Cela pourrait être considéré comme un code moins lisible ou une mauvaise pratique, mais vous pouvez lancer votre exception telle quelle. Ce modèle viole la spécification des flux réactifs, mais le réacteur capturera une exception levée pour vous et le propagera sous la forme du signal onError à votre aval.

À emporter

  1. Utilisation .handle opérateur pour assurer le traitement des éléments complexes
  2. Utilisez concatMap + Mono.error lorsque nous devons lancer une exception lors du mappage, mais cette technique convient mieux aux cas de traitement d’éléments asynchrones.
  3. Utilisez flatMap + Mono.error quand nous avons déjà eu flatMap en place
  4. Null en tant que type de retour est interdit donc au lieu de null dans votre aval map vous obtiendrez inattendu onError avec NullPointerException
  5. Utilisez switchIfEmpty dans tous les cas où vous devez envoyer un signal d'erreur si le résultat de l'appel d'une fonction spécifique s'est terminé avec le flux vide
31
Oleh Dokuka