Les exceptions Java

Les exceptions en Java constituent un mécanisme puissant de traitement des erreurs aussi bien dans une phase d’exécution normale de l’application que dans les phases de débogage. J’aime comparer Java et Pascal ou C. Dans le premier, tout événement anormal (utilisation d’un pointeur null ou un problème d’entrée/sortie par exemple) doit provoquer un arrêt du flot d’exécution… immédiatement ! Alors que dans le second, il peut se passer de nombreuses instructions entre l’origine d’un problème et sa manifestation, rendant le débogage plus difficile.

La perception des exceptions par le programmeur est malheureusement souvent mauvaise. Le programmeur ressent la gestion des exceptions comme une contrainte et s’en débarrasse en la masquant ou en la laissant remonter, par paresse. Cela entraîne un mauvais usage des exception et c’est bien la mauvaise gestion des exceptions en Java par le programmeur qui font dire à certain que java ne sait pas gérer les exceptions.

Pourtant il suffit de respecter quelques principes simples pour tirer le plus grand bénéfice de ce mécanisme de gestion d’erreur.

Une exception doit être rattrapée si on sait comment la traiter

Par traitement on entend une action spécialement prévue dans le cas de l’exception. Par exemple une IOException pourrait être traitée en réessayant un certain nombre de fois ou en demandant à l’utilisateur de changer un paramètre (chemin de fichier par exemple). Mais attention, rattraper une exception ne suffit pas à la traiter correctement. Il ne suffit pas d’encadrer une un appel pouvant lever une exception avec un try catch pour s’assurer d’une bonne gestion. En effet, considérons le code suivant:

instruction1();  // lève une exception
instruction2()

Il ne suffit pas de faire:

try {
  instruction1();
}
catch (Exception e){
  // on gère l'exception
}
instruction2();

Le code précédent assume que la gestion de l’exception doit permettre l’exécution de instruction2(), mais est-ce que la gestion de l’exception le fait réellement ? un simple log sera bien sûr insuffisante, ne vaudrait-il pas mieux ne pas exécuter instruction2() ? Ou bien mettre la 2e instruction dans le bloc try également ? La réponse dépasse le cadre de la gestion des exceptions pour entrer dans celui de la robustesse du code.

Si on ne sait pas comment réagir face à l’exception la solution la plus simple est de ne pas la rattraper.

Sinon on ne rattrape pas et on la déclare

Déclarer l’exception signifie que la méthode doit la porter dans sa signature. C’est le point le plus important. Déclarer l’exception permet de la remonter correctement. Le code appelant qui peut être la méthode main() devra alors gérer l’exception, ce qui nous garantit un traitement correcte de l’exception: au moins par un arrêt du flot d’exécution.

Trop d’exceptions à déclarer: on encapsule

Les programmeurs non habitués à la gestion des exceptions seront alors gênés de devoir déclarer de nombreuses exceptions en signature de méthode.  Il faut alors prévoir une hiérarchie d’exception applicative plus simples qui va permettre d’encapsuler des exceptions plus techniques afin de limiter le nombre d’exception à déclarer dans les méthodes. Le programme va alors devoir rattraper les exceptions (technique ou de base) afin de les encapsuler dans des exceptions applicatives.

Cas des conteneurs d’application

Arrêter le flot d’exécution voire même la JVM est possible quand le programme est seul au monde. Dans le cas d’application web ou de conteneur d’EJB, le code ne doit pas arrêter la JVM car il s’inscrit dans un serveur d’application: il n’est pas seul au monde. Les conteneurs (tel que tomcat) sont bien écrits et une application ne peut pas arrêter la JVM à cause d’une exception qui remonterait toute le pile d’appel. Il y aura un appel qui va rattraper l’exception et en faire quelque chose comme par exemple l’écrire sur la sortie de la JSP. Cela est souvent vu comme une mauvaise chose pour l’utilisateur mais doit être vue comme une bénédiction par le programmeur. C’est la solution minimale qui assure un débogage de l’application.

Empiler ou découpler ?

Un reproche fait aux piles d’exception de Java c’est sa verbosité. C’est un reproche qui peu s’entendre. En effet prenons le cas d’une application multi-tiers où une erreur d’accès à la base de donnée remonte jusqu’à la couche de présentation sous forme d’une pile d’appel qui dévoile toutes les couches de l’application: action de l’utilisateur, appel de service, appel à la couche de persistance et finalement une belle erreur JDBC avec un code obscure en provenance du SGBD. Vous connaissez ce scénario  ? Il arrive quand on laisse filer l’exception ou que l’on encapsule en faisant référence à l’exception original via un appel à :

public Exception(Throwable cause)

Cette façon de faire permet de conserver l’exception d’origine et ainsi de ne pas perdre la pile d’exécution au moment de la levé de l’exception d’origine. C’est la façon de faire la plus sûr et la plus simple. c’est de l’empilage d’exception. Si on ne souhaite pas remonter dans les couches supérieures (appelantes) la pile d’exécution car on considère que cela n’est pas de leur ressort, on peut découpler i.e simplement lever une exception à destination des couches supérieures sans passer l’exception qui en est la cause mais alors il faut tracer (loguer) la cause de manière complète i.e. loguer la pile d’exécution. Ainsi on résout de manière élégante le problème des piles d’erreurs trop verbeuses dans la log des couches supérieurs tout en gardant dans les log des couches inférieurs la trace complète des exceptions à des fin de debug. Cette technique sera particulièrement efficace quand on choisit de mettre en œuvre des log par couche d’abstraction: une log pour la couche de présentation, une log pour la couche métier et une log pour la couche de persistance… ce qui nous amène à l’épineux problème de la gestion des log et c’est une autre histoire.

 

Analyse dump mémoire JVM sur AIX (ibm)

La JVM d’IBM sur AIX produit un dump mémoire au format non standard. La JVM de Sun (pardon Oracle….) crée un fichier HPROF tandis que la JVM d’IBM crée un fichier PHD (Portable Heap Dump). C’est un peu déconcertant car JVisualVM ne sait pas lire le PHD. La solution est alors d’utiliser Eclipse MAT dont l’ascendance génétique n’est pas étrangère à IBM.

Après avoir téléchargé la dernière version de MAT sur le site //www.eclipse.org/mat/ il faudra tout de même encore ajouter une extension pour qu’il puisse lire les PHD. Dans le menu “install new Software” on devra ajouter le site

//public.dhe.ibm.com/software/websphere/runtimes/tools/dtfj/

On peut également ajouter MAT (Memory Analyzer Tool) en temps que plugin dans un Eclipse standard. Pour cela il faudra ajouter le site suivant dans le menu “install new software”:

//download.eclipse.org/mat/1.4/update-site/

Ensuite pour pouvoir ouvrir des dump de JVM ibm il faut ajouter l’extension vue en premier.

Chainsaw sous Debian 7

Debian 7 Wheezy propose open-jdk 6 comme JVM par défaut. Malheureusement cela pose un problème avec la fenêtre de popup au démarrage de Chainsaw qui propose d’ajouter un récepteur de log. Cette fenêtre reste vide sans aucun moyen de la fermer. Un open-jdk 7 ne change rien  à l’affaire et il faudra ce résoudre à utiliser la JVM Oracle.

Eclipse Luna sous Debian 7 ne démarre pas

En train de tester en parallèle les IDE Netbeans et Eclipse sous Debian 7 Wheezy, on constate rapidement que les versions dans le dépôt ne sont pas les plus récentes. En effet Debian 7 est livré avec java 6. Netbeans et Eclipse sont respectivement disponibles en version 7 et 3.8.

Un petit passage par le gestionnaire de paquet aptitude permet de récupérer java 7 en version openjdk comme la 6 installée par défaut. D’autres jvm sont présentes, il suffit d’aller voir en faisant:

ls -l /usr/lib/jvm

Afin de bénéficier de java 8 il faudra télécharger la JVM chez Oracle. On pourra installer les JDK Oracles n’importe où et ajouter des liens symboliques dans le répertoire /usr/lib/jvm

La dernière version de Netbeans, 8.0.1 est obtenu sous la forme d’un installeur sh qui trouve immédiatement le dernier JDK (sans doute en scannant le répertoire cité précédemment). L’installation du dernier Eclipse, nom de code Luna, version 4.4.1 est un peu plus brute: c’est une simple archive.

Arrive le moment du lancement: Netbeans démarre sans souci, à l’aide du raccourci ajouté sur le bureau. Malheureusement je ne peux pas en dire autant d’Eclipse qui plante au bout d’une dizaine de seconde sur le “splash screen”.  La JVM par défaut sous Debian 7 est l’open-jdk 6, j’ai eu donc l’idée de tester Eclipse avec les autres JVM disponibles sous mon système: java 6 en version oracle, java 7 en open-jdk, en oracle, java 8 de oracle. Pour cela il faut utiliser le fichier eclipse.ini en ajoutant l’option -vm comme par exemple

-vm
/usr/lib/jvm/jdk1.8.0/bin/java

Rien à faire, Eclipse plante toujours. La pile d’erreur indique un problème avec GTK et finalement c’est le prix à payer quand on ne fait pas du pure Java. Ah ! Eclipse que tous les développeurs ont adoptés parce que SWT, le boîte à outils graphique, faisait du natif là où Swing écrit les widget avec du code Java donc plus lent. Voilà ce qui arrive quand on vend son âme au diable, fini le WORA. C’est un comble pour un développeur Java de devoir modifier son IDE pour lui dire d’utiliser GTK2, solution pour faire fonctionner Eclipse sous Debian 7. Dans le fichier eclipse.ini il faut mettre juste après l’option startup :

--launcher.GTK_version
2

 

Exploitabilité des log

Les mécanismes de log en Java, tel que log4J, sont bien matures et font partie de l’état de l’art des développement informatiques. Les développeurs ont l’habitude de s’en servir en vue de mettre au point les programmes et de corriger les bugs après avoir pu les reproduire en local. En production, les log sont généralement moins verbeuses et ont pour objectif de ne signaler que les problèmes sérieux  qui nécessitent un action de l’exploitant voire une remontés rapide aux équipes de développement pour fourniture d’une correction.

Un aspect de la gestion des log est néanmoins peu adressé. Il s’agit de l’exploitabilité des log quand il s’agit de connaître le comportement de l’application en production. La production, environnement sécurisé n’est pas accessible aux développeurs qui doivent souvent demander les log à l’exploitant de production voire ensuite demander d’augmenter le niveau de log en production pour cibler le problème.

De plus, dans le contexte actuel, d’applications communiquantes, où une fonction est assurée par la collaboration de plusieurs applications, la compréhension des problêmes en production passent par une vue globale des log. La log du logiciel est multi-applicative.

Nous prenons comme exemple l’API de log Lg4J devenu le standard defacto de la log en java avant que Sun n’ajoute un mécanisme de log dans le JDK même. Quels sont les outils à la disposition ? Log4j, Chainsaw et Flume méritent-ils que l’on s’y intéressent de plus près ?

Comment perdre la boule avec Java et Windows Server 2003

L’histoire commence par la mise à jour de Jira 4 vers Jira 6. Ce dernier étant plus gourmand en mémoire, il utilise une JVM configurée pour demander à l’OS quelques 1,1 Go de mémoire (PermGenSpace et Xmx). Ce qui n’est vraiment pas un pré-requis exceptionnel. Mon PC portable professionnel dispose de 8 Go de RAM ! C’était sans compter le contexte du client qui partait à l’origine d’un Jira 3 qui tournait sur une machine avec 1 Go de RAM…

Le passage à Jira 4 n’avait pas posé de soucis “système”, juste une mise à jour par export puis import des données, un peu fastidieuse mais rien de bien méchant. Le passage à Jira 6 s’annonçait comme devant se faire avec encore plus de facilité. Je demande alors à ce que l’on mette plus de mémoire à la machine virtuelle (sous Hyper-V) afin de prendre en considération le fait que Jira 6 nécéssite entre 1 et 2 Go de mémoire disponible.

On passe à 2 Go et bang ! L’application ne se lance pas. Pas de log, rien. Après quelques “echo on” je découvre que la JVM ne se lance pas car impossible d’allouer la mémoire pour le tas. Et quel tas ! voici ce que demandait la JVM:

java -XX:MaxPermSize=384m -Xms384m -Xmx768m

Nous sommes sous Windows Server 2003, je me dis alors que peut-être l’OS ne laisse pas assez de mémoire pour mon programme, qui ne réclasmme que 1,1 Go. On passe alors à 4 Go de RAM et en vérifiant qu’il restait encore 3,7 Go de libre mais rien n’y fait. La JVM ne peut s’initialiser.

Je pars alors dans une approche exploratoire pour découvrir qu’en modifiant les paramètres pour rester sous la barre des 1 Go, la JVM s’initialise et le programme se lance. Tout ce passait comme si la mémoire n’était pas accessible pour la JVM. Quelque pages googles plus tard je découvre les subtilités de Windows Server 2003, OS 32 bits qui partage en 2 le mémoire. Sur 4 Go, 2 pour les appliations et 2 pour l’OS (?). Ce partitionnement peut être changé par un paramétrage dans le fichier boot.ini. Malheureusement ce n’était pas ça.

Je commençais à sécher et à me retourner contre l’environnement de virtualisation. Le problême n’étant pas reproductible ailleurs: soit c’est l’OS, soit c’est la virtualisation. Que nenni ! il s’agit en fait d’une régression introduite en 2009 par un patch de sécurité de Microsoft (956572) qui du jour au lendemain avait privé les JVM de mémoire, du moi,s sous Windows Server 2003. Imaginez-vous votre serveur Tomcat avec 1024 Mo en Xmx qui marche bien puis qui ne marche plus sauf à la brider à 512 Mo en Xmx….. Heureusement que Microsoft a fourni un patch qui corrige le problème (kb971812)

Mais comme mon client n’avait que 1 Go de RAM et une JVM qui ne demandait que peu de ressource mémoire, le régression était passée inaperçue et aucun correctif installé. Voilà comment prendre quelques cheveux blancs.

Lambda expression en Java 8

Lambda calculus is a formal system for functio...
Lambda calculus is a formal system for function definition, function application and recursion introduced by Alonzo Church in the 1930s. (Photo credit: Wikipedia)

Java 8 qui est sur le point de sortir nous promet encore plus de programmation fonctionnelle.

Le parcours  du  tutoriel en ligne justifie cela par l’efficacité de l’approche. Les lambdas expressions permettent en effet de décrire des fonctions anonymes, sans nom et raccrochées à aucun objet. Sans les lambda expression le langage oblige alors à créer une classe (anonyme si nécessaire) et une méthode.  Inutile et superflu parait-il…

Supposons  que l’on veuille pouvoir passer un comportement à une méthode. Comme on n’avait pas de pointeur sur fonction en Java jusque là, on devait définir une interface puis l’implémenter et passer une instance implémentante à la méthode.

Une interface:

public interface Calculator {
        int function(int x); 
}

Une implémentation:

public class InterfaceImplementor implements Calculator {
    Override
    public int function(int x) {
        return 222;
    }
}

définition de la méthode paramétrable par une interface fonctionnelle:

 int calcul(Calculator c){
        return c.function(0);
  }

L’appel se fait alors par un code (encore du code) du genre:

calcul(new InterfaceImplementor());

On peut éviter de définir une classe en créant une instance d’une classe anonyme:

calcul(new Calculator(){

            @Override
            public int function(int x) {
               return 999;
            }

Le JDK 8 arrive pour éviter encore plus de code en proposant de définir une lambda expression c’est-à-dire une méthode anonyme sans classe. Pour pouvoir tester cette future nouvelle fonctionnalité il faut télécharger le JDK 8 (non finalisé) et Netbean 7.4 (non finalisé) pour pouvoir écrire:

calcul((x) -> 555);

Il y a là une petite complexité (pardon nouveauté) sous le clavier. Le compilateur autorise ici 2 raccourcis: pas d’interface, pas de méthode.  La simple définition d’une fonction qui répond au typage de la méthode de l’interface Calculator suffit pour que le compilateur câble tout correctement. On peut se dire que c’est une bonne chose mais là où ça se complique c’est que Java 8 offre la possibilité d’utiliser une méthode existante sans passer par la case refactoring pour faire rentrer la méthode existante dans le moule d’une interface. Ainsi on peut utiliser n’importe quelle méthode ayant la bonne signature fonctionnelle: dans notre exemple int -> int. L’appel ressemble alors à cela:

calcul(ecxistingObject::existingMethod);

Voici le code complet permettant de voir l’ensemble des évolutions du langage abordées dans ce billet:

Une interface pour la manière de faire “old school”:

package lambda;

public interface Calculator {
        int function(int x);   
}

et son implémentation:

package lambda;

public class InterfaceImplementor implements Calculator{
    @Override
    public int function(int x) {   
       return 222;
    }
}

Une classe existante dont on peut réutiliser une méthode sans avoir à implémenter l’interface vue plus haut:

package lambda;

public class ExistingClass {
 private int i = 777;

 public int uneFonction(int x) {
   return i;
 }
}

Et les différentes manières de passer une fonction à une méthode:

package lambda;

public class Lambda {
 /**
  * @param args the command line arguments
  */
 public static void main(String[] args) {
   // TODO code application logic here

   Lambda l = new Lambda();
   int i = 0;

   // old school, plain class creation

   i = l.calcul(new InterfaceImplementor());
   System.out.println(i);

   // old school, anomymous class

   i = l.calcul(new Calculator(){
           @Override
       public int function(int x) {
         return 999;
       }
     });
   System.out.println(i);

   // anonymous method, lambda expression

   i  = l.calcul((x) -> 555);
   System.out.println(i);

   // method reference !

   ExistingClass existingObject = new ExistingClass();
   i = l.calcul(existingObject::uneFonction);
   System.out.println(i);
 }

 int calcul(Calculator c){
   return c.function(0);
 }
}

Dont l’exécution donne:

run:
 222
 999
 555
 777
 BUILD SUCCESSFUL (total time: 1 second)

Code d’initialisation en Java

Coding
Coding (Photo credit: Omer van Kloeten)

La lecture du tutoriel Java mérite que l’on s’y attarde, que ce soit pour le débutant ou bien pour celui qui vise l’excellence. Certains aspects du langage fort intéressants y sont bien expliqués, comme le code d’initialisation. On le trouve il est vrai rarement dans le code d’application métier mais plutôt dans le code de framework ou d’application standalone qui doivent initialiser de la “plomberie” comme une connexion de base de donnée ou l’activation d’un service de nommage. Mais il parait qu’il n’y a rien de tel que la lecture du code d’un bon framework pour apprendre à faire du bon code alors attardons nous sur les mécanisme offert pas le langage en matière d’initialisation.

Dans le tutoriel on distingue 3 types de code d’initialisation:

  • le code d’initialisation static
  • le code d’initialisation d’instance
  • le code d’initialisation de variable

L’initialisation statique pourra être utilisée pour s’occuper de choses globales à l’application et l’initialisation d’instance pour des aspect liée à l’instance comme par exemple pour faire un suivi des instances en utilisant une référence faible (weak reference). Le tutoriel présente une exemple de code d’initialisation de variable par une méthode final arguant que le contraire peu poser des problèmes. Peut-être… Un petit bout de code permet de voir comment tout cela s’articule.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package initinstancemenber;

/**
 *
 * @author thierry
 */
public class InitInstanceMenber {

    private int i = initMyIMember();

    {
        System.out.println("instance init code");
    }

    static {
        System.out.println("class init code");
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        InitInstanceMenber a = new InitInstanceMenber();
        System.out.println("i=" + a.i);
        a= new InitInstanceMenber(){
            @Override
            protected int initMyIMember(){
                System.out.println("overriden init member");
                return 1;
            }
        };
        System.out.println("i=" + a.i);
    }

    protected int initMyIMember(){
        System.out.println("init member");
        return 0;
    }
}

Dont voici l’exécution:

run:
class init code
init member
instance init code
i=0
overriden init member
instance init code
i=1
BUILD SUCCESSFUL (total time: 0 seconds)

Les étiquettes en Java

English: A snippet of code in the Java program...
English: A snippet of code in the Java programming language written to illustrate syntax highlighting of keywords. (Photo credit: Wikipedia)

Les premières tasses de thé glacé passées, le tutoriel Java arrive sur les instructions de contrôle (branchement et boucle).  A l’époque où je développais couramment en Java, je n’utilisait que très peu les instruction break ou continue dans les boucles. L’utilisation d’étiquette était pour ainsi dire absente du code que je produisais. Pourquoi ? Tout d’abord parce qu’on a appris que les étiquettes (label) étaient utilisés dans d’autre langage pour faire des sauts (goto) et que cela était contraire à la programmation structurée, que l’on devait avoir un point d’entrée et un point de sortie unique, que cela simplifiait ainsi le code etc, etc. Et puis sans doute parce que la complexités des algorithmes de l’informatique de gestion ne nécessitait pas l’optimisation du flot de contrôle pour éviter des tours de boucle superflus.

Mais bien sûr si la performance est un critère comme par exemple en informatique financière, alors il vaut mieux sortir d’une boucle dès qu’on a fini le travail. Est-ce pour autant sacrifier le beauté du code et sa maintenabilité ? Non, car les étiquettes en Java ne servent qu’à nommer les blocs de boucles et on ne peut pas les mettre ailleurs pour faire des sauts dans tous les sens. Le code reste donc structuré. L’extrait de code suivant montre un exemple d’utilisation d’étiquette:

        premierNiveau:
        for (int i=0;i<100;i++){
            deuxiemeNiveau:
            for (int j=0;j<i*100;j++){
                boolean mustBreak = false;
                boolean mustContinueOutsideLoop = false;
                // code that decide if we must break or continue
                if (mustBreak){
                    break deuxiemeNiveau;
                }
                if (mustContinueOutsideLoop){
                    continue premierNiveau;
                }
                // very long running code here
            }
        }