J'ai déjà parlé des exceptions dans différents billets (ici, et la). Toutefois, le problème de fond n'ayant pas été expliqué (du moins sur ce blog), voici un billet qui détaille un peu mieux leur fonctionnement.

La première chose à comprendre est que lorsqu'un appel est fait à un (Session) Bean, une transaction est créée puis "committée" lorsque l'exécution de la méthode appelée est terminée. Si une exception imprévue remonte, alors cette transaction sera annulée puis "rollbackée".

Il existe différentes manières de gérer les transactions au sein des Bean via des annotations mais cela fera l'objet d'un autre billet. Ce qui est important de savoir est qu'une exception qui remonte à l'Application Server va par défaut invalider la transaction en cours.

Afin de bien comprendre de quoi il en retourne, je vais traiter ici de trois types d'exception :

  • les Checked Exception
  • les Unchecked Exception
  • les Unchecked Exception en tant qu'Application Exception

Voici le code des différentes classes que je vais utiliser :

// Checked Exception
public class MyException extends Exception { }
// Unchecked Exception
public class MyRuntimeException extends RuntimeException {}
// Unchecked Exception (Application Exception)
@ApplicationException
public class MyRuntimeApplicationException extends RuntimeException {}

Et voici le Session Bean qui permet de lancer celles-ci :

@Stateless
@LocalBean
public class ExceptionBean
{
    public void throwException() throws MyException
    {
        throw new MyException();
    }
 
    public void throwRuntimeException(boolean applicationException)
    {
        if (applicationException) throw new MyRuntimeApplicationException();
        throw new MyRuntimeException();
    }
}

L'idée est donc de voir ce qui se passe lorsque ces méthodes sont appelées depuis un autre Session Bean !

@Stateless
public class FacadeBean implements FacadeBeanLocal
{
    @EJB
    private ExceptionBean exceptionBean;
 
    @Override
    public void testException()
    {
        try
        {
            exceptionBean.throwException();
        }
        catch(Exception exception)
        {
            e.printStackTrace();
        }
    }
}

Concernant la sortie, rien de spécial, la Checked Exception MyException a bien été récupérée (toutefois, la transaction n'est pas invalidée) !

ch.capi.exception.MyException: null at ch.capi.bean.ExceptionBean.throwException

Maintenant qu'en est-il lors de l'appel à la méthode exceptionBean.throwRuntimeException(false) ? Et bien, voici ce que donne la sortie :

javax.ejb.EJBTransactionRolledbackException at com.sun.ejb.containers.BaseContainer.mapLocal3xException(BaseContainer.java:2204)

Rien à voir avec la classe MyRuntimeException et pour cause, celle-ci a été encapsulée dans une EJBException. Cela peut paraître surprenant, mais c'est clairement écrit dans la spécification Java EE : toutes les Unchecked Exception standardes doivent l'être !

Pour que cette encapsulation n'ait pas lieu, il faut utiliser l'annotation @ApplicationException et ainsi obtenir la sortie escomptée :

ch.capi.exception.MyRuntimeApplicationException: null at ch.capi.bean.ExceptionBean.throwRuntimeException

Qu'en est-il des Unchecked Exception autres que celles créées pour le projet (du style java.lang.NullPointerException) ? Et bien il est possible de les définir comme Application Exception dans le fichier de configuration ejb-jar.xml :

<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee" 
         version = "3.1"
         xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd">
   <assembly-descriptor>
    <application-exception>
      <exception-class>java.lang.NullPointerException</exception-class>
      <rollback>true</rollback>
    </application-exception>
   </assembly-descriptor>
</ejb-jar>

Dans tous les cas ci-dessus, la transaction est invalidée. Cela signifie que si par la suite un appel est fait à un autre Bean durant la même transaction, une exception va être lancée. J'ai décrit ce comportement plus en détail dans ce billet.

Il est possible de spécifier qu'une exception ne provoque pas l'invalidation de la transaction (rollback=false), mais cela peut conduire à des effets de bord très problématiques (par exemple lorsqu'on modifie différentes tables au sein d'une base de données).

J'ai mis en annexe un projet NetBeans 6.8 - GlassFish v3 qui permet de tester exactement les comportements décrit ci-dessus !