Exile on Keyboard St. - Blog sur Linux et Debian

Aller au contenu | Aller au menu | Aller à la recherche

mercredi 10 octobre 2018

Des logs claires et utiles en Java

L'utilisateur final d'un logiciel est toujours heureux lorsque ce logiciel produit des logs claires et utiles.

Malheureusement, les développeurs ne font pas toujours suffisamment le nécessaire pour:

  • Ajouter des logs et traces là ou c'est utile
  • Mettre ces logs dans le bon niveau de log
  • Les mettre à jour quand besoin est
  • Faire en sorte que ces logs ne pénalisent pas trop les performances du logiciel

Et pourtant, la deuxième plus grande catégorie d'utilisateurs d'un logicel, c'est .... l'équipe de développement de ce logiciel !!!

On va donc faire un tour des erreurs les plus communément faites en matière de logs, et ce dans le but de s'amélirorer !

1. Pas de logs du tout

La première erreur que je vois trop souvent, c'est l'absence complète de logs dans une méthode ou même dans un composant tout entier. Il est donc impossible en cas de doute(s) sur le fonctionnement de ce code, de savoir ce qu'il se passe, à part se connecter en debug sur le serveur, ce qui n'est évidemment pas possible chez un client.

2. Des logs uniquement en TRACE ou en FINEST

Deuxième erreur trop souvent commise, on a des logs mais seulement dans le niveau de log maximum !

Comme en pratique, il est déconseillé de configurer un logiciel avec un niveau de log maximum, il en résulte que ces logs ne sortiront la plupart du temps pas chez les clients.

Ce cas arrive souvent quand le développeur veut mettre des logs mais il n'est pas certain du niveau de logs qu'il faut choisir. Et comme il développe avec un niveau de log en TRACE, ses logs s'affichent et il est content !

3. Des logs sans suffisamment de contexte

C'est moins fréquent, mais j'ai aussi vu des logs qui ressemblent à ça:

LOGGER.log(Level.INFO, "Creating user");

ou

LOGGER.log(Level.INFO, "Entering method 'needsRestart'");

Des logs comme celles-cis ne donnant aucun contexte, elles ne sont pas vraiment utiles.

Il s'agit en fait de traces utilisées lors du développement ou plus précisément du debug et qui auraient dù être enlevées ensuite.

4. Des logs avec une chaîne de formatage fausse

Le cas le plus courant de chaîne de formatage fausse est l'erreur sur le nombre de paramètres, par exemple:

LOGGER.info("Computing user: {} identifier: {}", user.getName());

Manifestement, il va nous manquer une information dans la log ...

Si à la place on avait:

LOGGER.info("Computing user: {} identifier: {}", getParam("user"), getParam("identifier"), getParam("extra"));

La méthode getParam serait appelée trois fois alors que le troisième paramètre n'est pas utilisé. Cela nous amènera à utiliser un Supplier et les méthodes lambdas avec Java 8.

5. Des logs faites par des anciens développeurs C

Dans les logs en Java, on trouve aussi le résultat de la programmation en C: le développeur Java qui met des String.format(...) partout !

LOGGER.info(String.format("Computing user: %s identifier: %s", getParam("user"), getParam("identifier")));

Ce qui va formater la chaîne même si le niveau de log ne permet pas que le message s'affiche. On en revient aux Suppliers.

Ensuite il est inutile de recourir à String.format(....) puisque Log4j ou java.util.logging formattent très bien le message en convertissant tout en chaînes de caractères.

Parfois le String.format(...) est tout simplement oublié:

LOGGER.info("Computing user: %s identifier: %s", getParam("user"), getParam("identifier"));

Ce qui affiche quelque chose comme:

06:48:36.094 main INFO org.grumpyf0x48.test.Log4j2Test - Computing user: %s identifier: %s

ce qui n'était probablement pas souhaité !

6. Des logs avec des passages de paramètres .... illisibles

On voit aussi des développeurs qui veulent logger beaucoup de choses dans le même message, et qui ont la hantise du plantage, et qui donc non content de passer 8 paramètres au message de log (et ce tout sur la même ligne temps qu'à faire), font une vérification pour chacun (ou presque) comme: (name == null) ? "no name supplied" : name.

C'est parfaitement inutile puisque dans ce cas Log4J affichera simplement "null" sans se planter. En outre ce genre de message compliqué est difficile à lire et à maintenir, donc à bannir.

Voilà, j'imagine qu'il y a des cas d'erreurs que j'oublie, n'hésitez pas à faire un commentaire si c'est le cas.

dimanche 9 septembre 2018

Tester une API REST directement depuis le Swagger Hub

Les moyens de tester une API REST ne manquent pas.

Mais pendant que l'on définit celle-ci, si on le fait avec le Swagger Hub, il n'est vraiment pas besoin de se compliquer la vie.

En effet, le Swagger Hub qui permet de définir le fichier yaml ou json de l'API, mais aussi de générer les stubs clients et/ou serveur de l'API permet aussi de tester une installation de ladite API.

Il suffit pour cela de:

  1. Se rendre sur la page de votre API, par exemple: https://app.swaggerhub.com/apis/<user>/<api>/<api-version>
  2. Renseigner le nom du serveur de test qui "hoste" votre API
  3. Installer l'Add-On pour Firefox CORS Everywhere afin que le Swagger Hub puisse contacter votre serveur de test
  4. Sélectionner en bas à droite de l'écran l'option: "Routing requests via browser"
  5. Cliquer sur l'URL voulue puis sur "Try it out"
  6. Remplir les champs et enfin cliquer sur "Execute"

De cette façon, vous pouvez faire très facilement des tests manuels sur la permière version de votre API et éventuellement de corriger les problèmes. Et vous n'avez écrit ni généré aucun code pour ce faire !

lundi 6 août 2018

Java: Bien utiliser le Builder pattern

Quand on maintient du code Java, on tombe malheureusement assez souvent sur des méthodes aux signatures complexes comme celle-ci:

public int myMethod(String id, String name, Boolean secure, Object obj, Object backupObj, boolean restart)

Et il n'est pas rare d'avoir encore plus de paramètres !

La situation se dégrade encore quand pour "simplifier", on surcharge la méthode précédente en:

public int myMethod(String id, String name, Boolean secure, Object obj, Object backupObj)

quand restart vaut false puis en:

public int myMethod(String id, String name, Boolean secure, Object obj)

quand en plus backupObj est null.

On peut avoir ainsi 3 à 5 signatures différentes pour la même méthode. Et cela devient vite unsupportable quand on veut refactorer ces méthodes, parce qu'elles sont référencées n fois dans le code de production et également dans les tests.

Si le développeur qui a créé la première méthode avait créé un object et l'avait passé en paramètre de sa méthode, on n'en serait pas là !

Imaginons maintenant la méthode suivante qui permet de créer un utilisateur:

public long create(final String firstName, final String name, final int age)

Elle retourne l'identifiant de l'utilisateur créé. Supposons que l'age n'est pas obligatoire et que l'on souhaite anticiper l'ajout de nouveaux champs pour créer un utilisateur.

On va alors plutôt procéder comme suit:

public long create(final User user)

La classe User commencera comme ceci:

public class User
{
    private long id;
    private String firstName;
    private String lastName;
    private int age;
}

Mais pour créer un utilisateur à passer en paramètre de la méthode create, on va créer un Builder pour construire l'objet User.

Pour cela, on peut installer le Plugin Builder Generator. Un autre plugin est également disponible: Inner Builder.

Après redémarrage d'IDEA, un clic-droit puis Generate propose maintenant l'option "Builder" et on a alors la boite de dialogue suivante:

Capture-CreateBuilder.png

Ici on a choisi un "innerBuilder" afin que le Builder soit une classe interne de l'objet lui même.

On choisit ensuite la liste des champs à inclure, on ne prends pas l'id puisqu'il ne sera pas calculé par l'appelant de la méthode create mais bien par celle-ci:

Capture-Select_Fields_to_Be_Available_in_Builder.png

Et on obtient:


    public static final class UserBuilder
    {
        private String firstName;
        private String lastName;
        private int age;

        private UserBuilder()
        {
        }

        public static UserBuilder anUser()
        {
            return new UserBuilder();
        }

        public UserBuilder withFirstName(String firstName)
        {
            this.firstName = firstName;
            return this;
        }

        public UserBuilder withLastName(String lastName)
        {
            this.lastName = lastName;
            return this;
        }

        public UserBuilder withAge(int age)
        {
            this.age = age;
            return this;
        }

        public User build()
        {
            User user = new User();
            user.lastName = this.lastName;
            user.age = this.age;
            user.firstName = this.firstName;
            return user;
        }
    }

On crée ensuite notre User comme suit:

        final User user = UserBuilder
                    .anUser()
                    .withFirstName("Brian")
                    .withLastName("Kernighan")
                    .build();

Cette façon de faire permet:

  • de faire évoluer l'objet User en changeant un minimum de code
  • de valider les différents champs dans la méthode build

Et les signatures des méthodes ne changent pas.

On peut même éviter de dupliquer les champs de la classe User dans la classe UserBuilder en procédant comme ceci:

public static final class UserBuilder
    {
        private User user;

        private UserBuilder()
        {
            user = new User();
        }

        public static UserBuilder anUser()
        {
            return new UserBuilder();
        }

        public UserBuilder withFirstName(String firstName)
        {
            this.user.firstName = firstName;
            return this;
        }

        public UserBuilder withLastName(String lastName)
        {
            this.user.lastName = lastName;
            return this;
        }

        public UserBuilder withAge(int age)
        {
            this.user.age = age;
            return this;
        }

        public User build()
        {
            return user;
        }
    }

Mais notre générateur ne sait pas le faire :-(

- page 1 de 2