dimanche 28 avril 2024

Transition de RestTemplate à WebClient dans Spring Boot : Un Guide Détaillé

Introduction

Avec l’évolution de Spring Boot et de son écosystème, les développeurs se retrouvent souvent confrontés à la tâche de passer de technologies plus anciennes à des technologies plus récentes et plus efficaces. Une transition qui a gagné en popularité est le passage de RestTemplate à WebClient pour effectuer des requêtes HTTP dans les applications Spring Boot.
RestTemplate a été la solution de choix pour l’accès HTTP côté client pendant de nombreuses années, offrant une API bloquante et synchrone.
Cependant, avec le besoin croissant de programmation réactive et non bloquante pour gérer la concurrence avec moins de ressources, RestTemplate a montré ses limites.


Problème

RestTemplate est un client bloquant et synchrone (the RestTemplate class is in maintenance mode).

Cela signifie que le thread qui exécute la requête se bloque jusqu’à ce que l’opération soit terminée, ce qui peut potentiellement conduire à l’épuisement du pool de threads et à une latence plus élevée sous une charge lourde.
De plus, RestTemplate ne supporte pas la programmation réactive, une nécessité croissante dans les écosystèmes basés sur le cloud.
Cela devient particulièrement apparent dans les architectures de microservices où les ressources doivent être utilisées de manière efficiente.

Pros :

  • Bibliothèque de client HTTP sous-jacente interchangeable
  • Supporte une interface HTTP déclarative
  • Hautement configurable
  • Utilisable dans les anciennes versions du framework Spring

Cons :

  • Avoir plusieurs surcharges d'une méthode rend cette bibliothèque difficile à utiliser
  • Le modèle classique de template Spring est démodé
  • Non adapté aux environnements non-bloquants (par exemple, WebFlux)



Solution : WebClient

WebClient, introduit avec Spring 5, est recommandé comme le successeur de RestTemplate. 

WebClient fonctionne sur un paradigme non bloquant et réactif en utilisant Project Reactor, lui permettant de gérer la concurrence avec moins de threads et moins de surcharge, améliorant considérablement l’évolutivité et l’utilisation des ressources.
De plus, WebClient offre une intégration transparente avec JSON via la bibliothèque Jackson, similaire à RestTemplate, mais avec des capacités de traitement améliorées.

Étapes de Transition

Étape 1 : Dépendance

La première étape consiste à inclure la dépendance spring-webflux dans votre pom.xml ou build.gradle :

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

Étape 2 : Remplacer RestTemplate

Ensuite, vous devrez remplacer les utilisations de RestTemplate par WebClient dans l’ensemble de votre code. Voici un exemple : Avant :

RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(url, String.class);

Après :

WebClient webClient = WebClient.create();
Mono<String> resultMono = webClient.get()
        .uri(url)
        .retrieve()
        .bodyToMono(String.class);
String result = resultMono.block();

Étape 3 : Gestion des Réponses

Étant donné que WebClient est réactif, la gestion des réponses change légèrement.
Au lieu d’obtenir directement le résultat, vous travaillez avec des types réactifs comme Mono et Flux. Cela vous permet d’appliquer des transformations et de composer des opérations asynchrones.
Par exemple :

Mono<String> resultMono = webClient.get()
        .uri(url)
        .retrieve()
        .bodyToMono(String.class);

resultMono.subscribe(result -> {
    // Gérer le résultat
}, error -> {
    // Gérer les erreurs
}, () -> {
    // Gérer la complétion
});

Discussion

La transition de RestTemplate à WebClient peut sembler intimidante au début, mais elle offre de nombreux avantages en termes de performance et d’évolutivité.
Il est important de noter que bien que WebClient soit plus performant, il peut nécessiter un changement de paradigme pour ceux qui sont habitués à l’approche bloquante et synchrone de RestTemplate. Cependant, avec une compréhension claire des concepts de la programmation réactive et une pratique régulière, la transition peut être plus facile que prévu.
Les avantages en termes de scalabilité et de performance sont substantiels, surtout dans les architectures modernes de microservices.
En conclusion, la transition vers WebClient ne consiste pas seulement à suivre les dernières tendances ; c’est aussi une manière plus efficace et évolutive d’effectuer des requêtes HTTP.

https://digma.ai/restclient-vs-webclient-vs-resttemplate/

[Spring Boot ] Migrating from RestTemplate to RestClient

 
The Spring Framework team recommends using RestClient for the new Spring MVC project and also provides guidelines to migrate from RestTemlate to RestClient.

https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#_migrating_from_resttemplate_to_restclient

https://digma.ai/restclient-vs-webclient-vs-resttemplate/

samedi 27 avril 2024

[Java ] Les interfaces fonctionnelles Java pour inverser les dépendances

Avez-vous déjà envisagé d’utiliser les interfaces fonctionnelles Java pour inverser les dépendances dans vos projets Java ?
Dans cet article, nous allons explorer comment nous pouvons le faire en utilisant trois interfaces clés : Supplier, Consumer et Function.

Supplier
L’interface Supplier est utilisée lorsque vous devez fournir un objet sans nécessiter de paramètres d’entrée.

Voici l’interface Supplier :

public interface Supplier<T>{
   T get();
}

Pour mieux comprendre la nécessité d’utiliser cette interface, jetons un coup d’œil à un peu de code :

public class Logger{
   public void log(String message){
      if(isLogEnabled()){
        write(message);
      }
   }
}

// Utilisation de la classe Logger
public class Controller{
   @Inject 
    Logger logger;

   public void execute(){
      logger.log(generateLogMessage());
   }
}

Dans le code ci-dessus, nous avons une classe Logger responsable de l’écriture des messages de log si la journalisation est activée.
La classe Controller invoque le logger en passant le résultat de la méthode generateLogMessage. Jusqu’à présent, tout semble bien.
Cependant, imaginez si la méthode generateLogMessage implique un traitement lourd ou consomme des ressources significatives, et que la journalisation est désactivée.
Dans de tels cas, des ressources précieuses sont gaspillées car le message de log produit ne sera pas utilisé.

La solution à ce problème est de passer un Supplier à la classe Logger qui renverra le message lorsqu’il est demandé, et le logger appelle simplement la méthode dans le cas où le log est activé, comme suit :

public class Logger{
   public void log(Supplier<String> messageSupplier){
      if(isLogEnabled()){
        write(messageSupplier.get());
      }
   }
}

// Utilisation de la classe Logger
public class Controller{
   @Inject Logger logger;

   public void execute(){
      logger.log(() -> generateLogMessage());
   }
}

Maintenant, la méthode generateLogMessage ne sera exécutée que dans le cas où la méthode get du fournisseur est appelée, puis nous économisons des ressources dans le cas où le log n’est pas activé. De plus, avec ce type de solution utilisant le Supplier, nous avons la flexibilité d’implémenter une logique très complexe pour la journalisation et d’être sûr qu’elle ne sera appelée que lorsque cela est nécessaire.

Function 

L’interface Function vous permet de définir une fonction qui prend un paramètre et produit un résultat. Voici l’interface Function (en omettant certaines méthodes par défaut) :

public interface Function<T, R>{
   R apply(T t);
}

Pour commencer à explorer l’interface Function, examinons une classe responsable du calcul du prix d’un article dans une commande de vente. 

Cette classe prend en entrée des éléments tels que le produit, la quantité et la remise (de 0 à 100) à appliquer :

public class PriceCalculator{
   public BigDecimal calculatePrice(Product product, 
                                    Integer quantity,
                                    BigDecimal discount){
     var grossPrice = product.getUnitPrice()
                             .multiply(BigDecimal.valueOf(quantity));
     var discountAmount = grossPrice.multiply(discount)
                                    .divide(BigDecimal.valueOf(100));
     return grossPrice.minus(discountAmount);
   }
}

// Exemple d'utilisation
var result = priceCalculator(product, 10, BigDecimal.value(10));

Cette classe calcule d’abord le prix brut, applique la remise, puis soustrait celle-ci du prix brut. Maintenant, considérons une nouvelle exigence : effectuer une conversion de devise sur le prix.

Une approche pourrait consister à ajouter directement la logique de conversion de devise à cette classe, introduisant potentiellement des bugs.
Une solution plus robuste consiste à introduire un paramètre Function responsable de la gestion de la conversion de devise.

public class PriceCalculator{
   public BigDecimal calculatePrice(
                        Product product, 
                        Integer quantity, 
                        BigDecimal discount, 
                        Function<BigDecimal,BigDecimal> converterFunction){
     var grossPrice = product.getUnitPrice()
                             .multiply(BigDecimal.valueOf(quantity));
     var discountAmount = grossPrice.multiply(discount)
                                    .divide(BigDecimal.valueOf(100));
     var netPrice = grossPrice.minus(discountAmount);
     return converterFunction.apply(netPrice);
   }
}

// Exemple d'utilisation
var result = priceCalculator(product, 
                             10, 
                             BigDecimal.value(10),
                             netPrice -> netPrice.multiply(CURRENCY_RATE));

L’ajout de cette nouvelle exigence a un impact minimal, et nous avons réussi à inverser la dépendance.
La classe PriceCalculator n’a plus besoin de gérer la conversion de devise ; elle se contente d’appeler la fonction fournie avec le prix net et de renvoyer le résultat.
Cette conception nous permet d’utiliser la même classe PriceCalculator pour convertir n’importe quelle devise sans modifier son code.

Il existe différentes approches pour répondre à cette exigence sans modifier la classe PriceCalculator. Vous pouvez créer une autre classe qui agit comme une façade en appelant la PriceCalculator puis en effectuant la conversion de devise.
En général, c’est une décision de projet de savoir quelle solution suivre.

Consumer 

L’interface Consumer nous permet de définir une fonction qui prend un paramètre, effectue une tâche spécifique et ne renvoie aucune valeur. 

Voici l’interface Consumer (en omettant certaines méthodes par défaut) :

public interface Consumer<T>{
   void accept(T t);
}

Pour voir un exemple de l’interface Consumer en action, examinons cette classe, qui définit certaines informations dans une entité et la sauvegarde dans la base de données :

public class EntitySaver{
   public void create(Entity entity){
      entity.setCreationDate(new Date());
      database.insert(entity);
   }
}

// Exemple d'utilisation
entitySaver.create(entity);

Maintenant, supposons que nous devons notifier d’autres classes chaque fois qu’une entité est créée, mais que nous ne pouvons pas modifier l’interface de la méthode create.
Dans de tels scénarios, nous pouvons mettre en œuvre le modèle de publication-abonnement, en utilisant l’interface Consumer.
Voici comment nous pouvons y parvenir :

public class EntitySaver{
   private List<Consumer<Entity>> consumerList = new ArrayList<>();

   public void register(Consumer<Entity> consumer){
      consumerList.add(consumer);
   }

   public void create(Entity entity){
      entity.setCreationDate(new Date());
      database.insert(entity);
      consumerList.forEach(consumer -> consumer.accept(entity));
   }
}

// Exemple d'utilisation
entitySaver.register(entity -> log.info(entity));
entitySaver.register(entity -> mailerService.notifyUser(entity));
entitySaver.create(entity);

Dans cette mise en œuvre du modèle de publication-abonnement, nous utilisons l’interface Consumer. La classe EntitySaver maintient maintenant une liste de Consumers et comprend une méthode register pour ajouter des consumers à cette liste. 

Alors que l’interface de la méthode create reste inchangée, nous introduisons une seule ligne de code pour “consommer” l’entité créée en invoquant les consumers enregistrés.

Conclusion 

Les interfaces fonctionnelles Java ont été introduites il y a de nombreuses années, et elles ont apporté un grand changement dans la façon dont nous développions en Java. 

Nous pouvons les utiliser comme des fonctions lambda, mais aussi pour inverser les dépendances, et rendre notre code plus propre. 



[Java] Remplacer StringUtils par Java native 11 et 15

Chaque mise à jour majeure du langage de programmation Java a introduit de nombreuses nouvelles fonctionnalités et améliorations.

De nouvelles méthodes ont été introduites dans la classe String pour mieux répondre aux besoins du développement et améliorer l’efficacité de la programmation.

Examinons certaines de ces nouvelles méthodes et voyons comment elles peuvent remplacer le rôle de StringUtil.

D’après mes tests, je pense que ces méthodes sont disponibles dans les versions de Java ci-dessous :

Java 11 :

repeat(): renvoie une nouvelle chaîne, la chaîne est formée par la chaîne d’origine répétée le nombre de fois spécifié.
isBlank(): vérifie si la chaîne est une séquence de caractères blancs, c’est-à-dire de longueur 0 ou ne contenant que des espaces.
lines(): renvoie un flux de chaînes séparées par des lignes.
strip(): renvoie une nouvelle chaîne formée en supprimant les espaces en début et en fin de la chaîne d’origine.
stripLeading(): renvoie une nouvelle chaîne, la chaîne est la chaîne d’origine après avoir supprimé l’espace de début formé.
stripTrailing(): renvoie une nouvelle chaîne, la chaîne est la chaîne d’origine après avoir supprimé les espaces de fin.

Java 15 :

formatted(): formate la chaîne avec les arguments spécifiés et renvoie la chaîne formatée. translateEscapes(): convertit les séquences d’échappement Java en leurs caractères correspondants et renvoie la chaîne convertie.
transform(): Cette méthode applique une fonction à une chaîne et renvoie le résultat de la fonction. Voyons aussi quelques exemples d’utilisation de ces nouvelles méthodes.

  1. Répétition
String str = "toto";

String repeatedStr = StringUtils.repeat(str, 3);
System.out.println(repeatedStr);
String repeatedStr2 = str.repeat(5);
System.out.println(repeatedStr2);

//résultat
//totototototo
//
totototototototototo
  1. isBlank
String str1 = "";
String str2 = " ";
String str3 = "  \t  ";

System.out.println(StringUtils.isBlank(str1));
System.out.println(StringUtils.isBlank(str2));
System.out.println(StringUtils.isBlank(str3));

System.out.println(str1.isBlank());
System.out.println(str2.isBlank());
System.out.println(str3.isBlank());

//résultat
//true
//true
//true
//true
//true
//true
  1. lines
String str = "Hello\nWorld";

Arrays.stream(StringUtils.split(str, "\n")).forEach(System.out::println);

Stream<String> lines = str.lines();
lines.forEach(System.out::println);

//résultat
//Hello
//world
//Hello
//world
  1. strip
String str1 = "  abc   ";
String str2 = "\t def \n";

System.out.println(StringUtils.strip(str1));
System.out.println(StringUtils.strip(str2));

System.out.println(str1.strip());
System.out.println(str2.strip());

//résultat
//abc
//def
//abc
//def
  1. stripLeading/stripTrailing
String str1 = "  abc  ";
String str2 = "  def  ";

System.out.println(StringUtils.stripEnd(str1, null));
System.out.println(StringUtils.stripEnd(str2, null));

System.out.println(str1.stripTrailing());
System.out.println(str2.stripTrailing());

System.out.println(StringUtils.stripStart(str1, null));
System.out.println(StringUtils.stripStart(str2, null));

System.out.println(str1.stripLeading());
System.out.println(str2.stripLeading());

//résultat
//  abc
//  def
//  abc
//  def
//abc  
//def  
//abc  
//def 
  1. formatted
String str = "My name is %s, I'm %d years old.";
String formattedStr = str.formatted( "toto", 30);
System.out.println(formattedStr); //résultat //My name is toto
, I'm 30 years old.
  1. translateEscapes
String str = "Hello\\nWorld\\nJava";
String translatedStr = str.translateEscapes();
System.out.println(translatedStr);

//résultat
//Hello
//World
//Java
  1. transform
String str = "Hello World";
String result = str.transform(s -> s + " Java!");
System.out.println(result);

//résultat
//Hello World Java! 

Voilà, c’est tout.
N’hésitez pas à commenter si cela vous a aidé. 

Java : Différences entre findFirst() et findAny() dans l’API Stream de Java


L’API Stream de Java offre deux méthodes terminales intéressantes : findFirst() et findAny().
Ces méthodes sont utilisées pour récupérer des éléments d’un flux, mais elles ont des comportements légèrement différents.
Explorons ces différences et examinons comment elles s’appliquent dans le monde réel.

1. findFirst()

La méthode findFirst() renvoie le premier élément du flux en fonction de l’ordre de rencontre du flux. Voici quelques exemples d’utilisation :

Exemple 1 : Trouver le premier élément d’une liste

List<String> fruits = Arrays.asList("pomme", "banane", "cerise", "datte");
Optional<String> premierFruit = fruits.stream().findFirst();
premierFruit.ifPresent(fruit -> System.out.println("Premier fruit : " + fruit));

Dans cet exemple, findFirst() renverra toujours “pomme” car l’ordre d’itération est déterministe dans une liste.

Exemple 2 : Utilisation avec des objets complexes

Supposons que nous ayons une liste d’objets Personne :

class Personne {
    private String nom;
    // autres propriétés et méthodes
}

List<Personne> personnes = // récupération des données depuis une source
Optional<Personne> premierePersonne = personnes.stream().findFirst();

Dans ce cas, findFirst() renverra la première personne rencontrée dans la liste.

2. findAny()

La méthode findAny() renvoie n’importe quel élément du flux, sans garantie sur lequel sera renvoyé. Cela permet plus de flexibilité dans les flux parallèles. Voici comment l’utiliser :

Exemple 3 : Trouver n’importe quel élément

List<Integer> nombres = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Optional<Integer> nombreQuelconque = nombres.stream().findAny();
nombreQuelconque.ifPresent(n -> System.out.println("Nombre quelconque : " + n));

En mode séquentiel, findAny() renverra probablement le premier élément, mais ce n’est pas garanti. En mode parallèle, il peut renvoyer n’importe quel élément qui termine son traitement en premier.

Exemple 4 : Utilisation dans un contexte réel

Supposons que nous ayons une liste de commandes clients à traiter. Nous voulons simplement récupérer une commande pour la traiter en parallèle :

List<Commande> commandes = // récupération des commandes depuis une base de données
Optional<Commande> commandeQuelconque = commandes.stream().findAny();

Dans ce cas, findAny() nous permet de récupérer rapidement une commande à traiter, sans nous soucier de l’ordre.

Conclusion

En résumé, utilisez findFirst() lorsque l’ordre est important (par exemple, dans les listes) et findAny() lorsque vous avez besoin de flexibilité (surtout dans les flux parallèles). Choisissez judicieusement en fonction du contexte de votre application !