Comment gérer les Exceptions en Java

Définition

Une exception en Java est un événement innatendu (exceptionnel) qui perturbe l’éxécution de votre programme.
Il y a 3 types d’exceptions en java
Checked exception : doivent être traitées avec un try / catch ou en déclarant la méthode avec throws + le type d’exception.
Exemple:

 
public void methodTryCatch()
{
   try
   {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      System.out.println(sdf.parse("207-02-05"));
   }
   catch (ParseException e)
   {
      e.printStackTrace();
   }
}

public void methodThrows() throws IOException
{
   String content = new String(Files.readAllBytes(Paths.get("MyClass.java")));
}

Unchecked exception : ce sont les RuntimeException et cette classe RuntimeException (ou toute classe qui l’étend) n’a pas besoin d’être traitée (try / catch) ou déclarée (trhows). Ce sont souvent des erreurs de programmation ou un mauvais usage d’une API.
Exemple:

 
public void methodRuntimeException()
{
   throw new RuntimeException();
}

Il y a aussi des Exceptions de type Error (java.lang.Error) ; ce sont des erreurs graves qui ne doivent pas être traitées car elles empêchent la JVM de fonctionner correctement. Le code doit être modifié (code refactoring) pour ne plus la rencontrer.

Le problème avec les exceptions

Le cache-misère

Quelle est le problème avec le code suivant ?

public void myMethod(){
   try{
      // something can throw an exception
   } catch(Exception ex){
      ex.printStacktrace();
   }
}

Le problème est que quelque chose de terrible peut s’être passé dans le bloque de code du try et que le programme continue comme si de rien était !
Solution : laissez remonter l’exception ! Le client voudra surement savoir que quelque chose ne se déroule pas comme prévu et fera le nécessaire : archiver le message d’erreur, notifier l’utilisateur final, …

Ci-dessous un autre cas concret que j’ai pu voir : attention ceci est un bon exemple de ce qu’il ne faut pas faire !

public int generateFile(String fileName)
{
    int iReturnCode = -1;

    File file = null;
    FileWriter fileWriter = null;
    BufferedWriter writer = null;

    try
    {
        file = new File(fileName+ ".txt");
    } catch (Exception e)
    {
        return -4;
    }

    try
    {
        fileWriter = new FileWriter(file);
        writer = new BufferedWriter(fileWriter);
    } catch (Exception e)
    {
        try
        {
            writer.close();
        } catch (Exception e1)
        {
        }

        try
        {
            file.delete();
        } catch (Exception e1)
        {

        }

        return -5;
    }

    try
    {
        writer.write("Test");
    } catch (Exception e)
    {
        try
        {
            writer.close();
        } catch (Exception e1)
        {
        }

        try
        {
            file.delete();
        } catch (Exception e1)
        {

        }

        return -6;
    }

    try
    {
        // corps de la méthode avec des try/catch(vide)
        // (^_^;)
    } catch (Exception e)
    {
    }

    return iReturnCode;
}

Le moindre problème qui se produirait lors de l’appel de cette méthode, on ne saura jamais pourquoi et on ne saura rien y faire. Dans le meilleurs des cas, je reçois un entier signé et je n’ai plus qu’à espérer trouver une documentation qui me donnera des informations sur la signification de ce nombre. Pourquoi ne pas renvoyer une exception au lieu d’un entier ?! Cette méthode ne devrait-elle pas retourner un fichier plutôt ?

Le boulet

Les checked exceptions sont souvent utilisées en Java mais leur utilisation devrait être bien plus réfléchie. Par exemple, il n’y a pas de checked exception en C++ ou en C#. Java n’empêche pas d’ajouter des throws Exception a des méthodes qui ne peuvent pas en générer.
Regardez le code suivant :

public List getAllAccounts() throws
    FileNotFoundException, SQLException{
    ...
}

Un appel à cette méthode par un client, l’oblige à traiter ces 2 erreurs même s’il n’a pas de connaissance particulière du fichier à traiter ou de la base de données ; ce qui génère un couplage étroit entre les 2 classes. Il va donc devoir ajouter ‘throws FileNotFoundException, SQLException’ (ou une classe parente) à sa signature ou bien l’attraper avec un try / catch. Même contrainte pour une classe qui étendrait cette classe et voudrait réécrire cette méthode.

Solution : si le client ne peut rien faire avec cette exception, le mieux est de convertir cette checked exception en une unchecked exception :

public void dataAccessCode(){
    try{
        //..some code that throws SQLException
    }catch(SQLException ex){
        throw new RuntimeException(ex);
    }
}

La condition facile

En aucun cas vous ne devez utiliser les exceptions pour intialiser une valeur ; prenez l’exemple suivant :

public static int divide(int a, int b)
{
   try
   {
      int i = a/b;
      return i;
   }
   catch(ArithmeticException ex)
   {
      return 0;
   }
}

Vous devez détecter cette détecter l’erreur avant qu’elle ne se produise :
Les exceptions mal utilisées peuvent ralentir votre programme, en prenant de la mémoire et de la puissance CPU pour créer, retourner (throw) et attraper (catch) des exceptions.

Cas spécifique : Exception dans Spring MVC

Cela fait plusieurs années que je travaille avec Spring en tant que serveur pour renvoyer les réponses aux requêtes Ajax du client. Donc, on a ces requêtes Ajax du côté client qui appelle des URL sur le serveur. Ces demandes sont traitées par un controlleur au niveau de Spring. Celui-ci fait appel à une classe de service qui va lui-même appeler un DAO pour aller chercher les données. Une erreur se produit dans le DAO comment gérer ça ?
Un premier réflexe pourrait être d’utiliser l’objet de retour de la méthode pour gérer les messages d’erreur. Pourquoi est-ce une mauvaise idée ? Premièrement, quand je vois une méthode insertEmployee qui retourne List ou pire un Object : un employee quand ça se passe bien et une List quand ça va mal. Ce n’est pas logique et c’est surtout très lourd si on doit tester le type de retour dans la méthode appelante!
Solution : avec Spring, il existe une annotation @ExceptionHandler à laquelle on peut passer le type d’exception qu’on veut capturer. Si vous déclarez une méthode d’un controlleur avec cette annotation, elle sera automatiquement appelée par le framework Spring qui rencontre une erreur qui n’est pas gérée dans votre code.

@ExceptionHandler(NotAuthorizedException.class)
public ModelAndView handleNotAuthorizedException(NotAuthorizedException notAuthorizedException){
   ...
}

@ExceptionHandler(Exception.class)
public ModelAndView handleException(Exception exception){
   ...
}

Cela vous permettra de centraliser les messages d’erreur à retourner à l’utilisateur et pour les archiver.

Astuce pour Logger

Si vous utilisez un outils de log comme Log4J ou autre, vous pouvez logger votre exception de la manière suivante :

Logger logger = Logger.getInstance(MyClass.class);
try
{
     ...
}
catch(Exception ex)
{
   logger.error("Something bad happens",ex);
   throw new RuntimeException(ex);
}

En résumé :

  • Pour savoir si vous devez utiliser une checked exception ou une unchecked exception, vous devez vous poser la question suivante : est-ce que le client peut faire quelque chose pour corriger cette erreur ? Prenons l’expemple d’une exception java.io.FileNotFoundException : est-ce qu’on mon code est capable de rappeler la méthode avec un fichier valide ? Oui, alors checked exception dans la signature de la méthode ; sinon, et dans la majorité des cas, préférez les unchecked exceptions
  • Essayez d’utiliser des exceptions existantes : NullPointerException, IllegalArgumentException, IllegalStateException, RuntimeException. Le code restera simple (KISS, Keep It Simple, Stupid!). N’ajouter que des nouvelles exceptions si vous avez besoin d’ajouter des nouvelles propriétés à l’exception.
  • Préservez l’encapsulation en convertissant les checked exception en unchecked exception. Vous pouvez documenter votre code avec l’annotation @throws ou avec des tests unitaires.
  • Utilisez le bloque finally pour fermer vos connexions ou vos fichiers lorsque vous recontrez une exception.

Autres sources :

Leave a Reply