jeudi 11 novembre 2010

Utiliser Glassfish Embedded pour tester ses EJB 3.1

Depuis la sixième version de Java EE, il est possible de tester ses EJB avec l'API  "EJB Embedded Container". Glassfish propose son implémentation intitulée "glassfish-embedded".


Cet article a un double objectif :
  • Présenter "Glassfish Embedded" et en quoi son utilisation facilite grandement le test de ses EJB ;
  • Lever certaines interrogations et fausses rumeurs selon lesquelles il serait impossible de tester ses EJB avec une base de données autre que JavaDB. En effet, "Glassfish Embedded" est fourni avec une source de données JavaDB embarquée accessible via l'entrée JNDI suivante : jdbc/__default. Or, comment faire si je veux tester un EJB qui manipule des entités JPA devant être synchronisées avec un support de persistance tel que MySQL par exemple ?

1/ Configuration du projet

Dans cet article, j'ai utilisé un projet Maven 2 afin de ne pas être dépendant de quelqu'IDE que ce soit. Les dépendances utilisées sont l'API Java EE 6, JUnit et Glassfsih Embedded (cliquez sur l'image pour agrandir) :





Pour que tout fonctionne, il faut bien entendu configurer les dépôts et les plugins (cliquez sur l'image pour agrandir) :





















































L'API Java EE 6 propose également certaines mises à jour des API de base fournies avec Java. Aussi, si l'on souhaite disposer de ces mises à jour, il faut configurer un Profile (cliquez sur l'image pour agrandir) :




2/ Création d'un EJB Hello World

Maintenant que notre projet est configuré, passons à l'implémentation de nos EJB. Commençons par un EJB simplissime : un "Hello World !" :



Vous noterez que notre EJB est un simple POJO annoté. En effet, la version 3.1 a considérablement simplifié l'écriture de ces composants. L'annotation @Stateless signifie que notre EJB est sans état. Un tel composant ne maintient pas d'état conversationnel avec l'appelant du bean. Cela signifie qu'une même instance du bean peut servir plusieurs clients (ou appelants).


3/ Ecriture de notre premier test unitaire


Il ne nous reste plus qu'à créer le test unitaire qui va bien :


















 

Quelques explications maintenant. La première chose à faire est de créer un EJBContainer. Pour cela, il est nécessaire de créer une Map contenant toutes les propriétés correspondant à la configuration de notre conteneur :
  • EJBContainer.APP_NAME : Le nom de notre application, ici unittesting. Ce paramètre est optionnel ;
  • EJBContainer.MODULES : Les modules EJB à charger. Il peut s'agir de répertoires ou d'archives JAR référencées par des objets File. Dans notre cas ici, nous définissons un objet File pointant sur le répertoire "target/classes" (répertoire Maven de nos fichiers .class compilés). Ce paramètre semble obligatoire sinon il sera impossible pour le conteneur de localiser vos EJBs au démarrage. Dans ce cas, un message "ejb.embedded.no_modules_found" sera affiché ;
  • EJBContainer.PROVIDER : Le nom complet de la classe d'implémentation mise à disposition par le fournisseur du conteneur embarqué. C'est cette classe qu'il faut adapter suivant les fournisseurs. Ce paramètre est lui aussi optionnel car si glassfish-embedded-all-3.0.1.jar est dans le classpath, la classe d'implémentation sera automatiquement chargée.

Remarque sur JNDI :
EJBContainer.APP_NAME est un paramètre optionnel. S'il n'est pas paramétré alors les EJBs déployés seront accessibles via leur nom JNDI global (standardisé depuis Java EE 6). Ce dernier est construit de la manière suivante :
  • java:global/classes/NOM_DE_LA_CLASSE_EJB

Si, en revanche, ce paramètre est bien précisé, le nom global JNDI sera alors comme suit :
  • java:global/NOM_APPLICATION/NOM_DE_LA_CLASSE_EJB

Dans notre cas, puisque nous avons précisé le nom de l'application (unittesting), notre bean HelloService est accessible via l'URL JNDI suivante :
  • java:global/unittesting/HelloService

Il existe une autre manière de retrouver un EJB dans l'annuaire JNDI en suffixant l'URL par le nom de la classe d'implémentation :
  • java:global/unittesting/HelloService!fr.sydisnet.blog.embedded.hellocomponent.HelloService

Si maintenant, notre EJB implémente une interface IHelloService (cette dernière spécifiant la méthode String sayHello()), alors ce dernier sera accessible de deux manières :
  • java:global/unittesting/HelloService
  • java:global/unittesting/HelloService!fr.sydisnet.blog.embedded.hellocomponent.IHelloService : Dans ce dernier cas, le nom JNDI est suffixé par le nom complet de l'interface.

Parmi les trois paramètres précédents, seul EJBContainer.MODULES est obligatoire.Vous noterez que deux lignes ont été mises en commentaires :
  • La première configure l'emplacement d'un serveur Glassfish 3.0.1 déjà installé à l'aide de la propriété org.glassfish.ejb.embedded.glassfish.installation.root. Cette propriété permet d'utiliser une installation existante afin de récupérer la configuration de l'instance du serveur. Cela est très utile pour pouvoir utiliser des ressources telles que des sources de données JDBC déjà paramétrées.
  • La deuxième est plus intéressante : Si aucune installation existante de Glassfish 3.0.1 n'est disponible, il est possible de spécifier au conteneur embarqué un fichier domain.xml à l'aide de la propriété org.glassfish.ejb.embedded.glassfish.configuration.file. C'est dans ce fichier que de nouvelles sources de données peuvent être configurées. Je reviendrai ultérieurement sur ce fichier domain.xml lorque nous configurerons une source de données MySQL.

Une fois que notre EJBContainer est créé, il suffit de demander à ce dernier de récupérer le contexte de l'application (variable appContext).

Ensuite, tous les tests (méthodes annotées par @Test) sont exécutés. Le programme se termine lorsque la méthode annotée par @AfterClass est appelée. Dans cette méthode, nous fermons le contexte de l'application, (appContext.close()) et nous arrêtons le conteneur (container.close()).


Enfin, il ne nous reste plus qu'à exécuter notre test unitaire. Mais les choses ne se passent pas comme prévues. En effet, une erreur apparaît :
  • Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.095 sec <<< FAILURE!
    initializationError(fr.sydisnet.blog.embedded.hellocomponent.HelloServiceTest)  Time elapsed: 0.008 sec  <<< ERROR!
    java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/ejb/embeddable/EJBContainer
Que se passe t'il ? Et bien, disons qu'il s'agit d'un problème lié à Maven et à l'API Java EE 6 (merci à DominikDorn). En effet, dans nos dépendances (fichier pom.xml), nous avons déclaré l'API Java EE 6 mais cette dernière est incomplète et cela ne plaît pas à Maven. Pour contourner le problème, il faut déclarer une dépendance complète avant celle de l'API Java EE 6. Cela peut être fait comme suit :































Après correction, le test ne pose plus de problème. Toutefois, un message d'erreur apparaît : "ejb.embedded.location_not_exists". Ce dernier signifie que la propriété org.glassfish.ejb.embedded.glassfish.installation.root n'a pas été passée au conteneur. Puisque nous partons du principe que nous ne disposons d'aucune installation existante de Glassfish, nous n'avons pas à configurer ce paramètre.
Félicitations ! Vous venez d'exécuter votre premier test unitaire mettant en œuvre Glassfish Embedded :)


4/ Configuration d'une source de données MySQL

Comme cela a été évoqué précédemment, il suffit de paramétrer l'emplacement d'un fichier de configuration à l'aide de la propriété org.glassfish.ejb.embedded.glassfish.configuration.file. Ce fichier s'intitule domain.xml. Mais comment créer ce fichier ?

Déplacez-vous à l'emplacement local Maven de votre ordinateur où est stocké le fichier glassfish-embedded-all-ejb-3.0.1.jar. Après avoir copié et décompressé le fichier quelque part, vous trouverez le fichier domain.xml dans le sous répertoire org/glassfish/embed.

Il ne reste plus qu'à copier le fichier à l'emplacement de votre choix et à configurer la propriété org.glassfish.ejb.embedded.glassfish.configuration.file attendue par le conteneur.

Pour ma part, je n'ai pas configuré la propriété en question car j'ai opté pour une deuxième solution. En effet, par défaut, le fichier domain.xml se trouve dans le package org.glassfish.embed.

Cela signifie qu'en plaçant le répertoire domain.xml dans src/main/resources/org/glassfish/embed, nous n'avons plus besoin de configurer la propriété org.glassfish.ejb.embedded.glassfish.configuration.file en question.

Il ne reste plus qu'à configurer un pool de connexions, d'enregistrer ce dernier dans l'annuaire JNDI et de référencer la ressource en question au niveau du conteneur embarqué (cliquez sur l'image pour agrandir) :










































Une fois le fichier domain.xml en place, il ne faut pas oublier de configurer la dépendance au driver MySQL :







































Et voilà ! Notre source de données MySQL est configurée.


5/ Test de la source de données avec Glassfish Embedded


Pour tester notre conteneur embarqué et la source de données configurée, ajoutons le test unitaire comme suit :
















Normalement, vous devriez voir quelque part dans les logs la châine MySQL suivi du numéro de la version de votre SGBD favori :)



















































La suite !

Comme nous l'avons vu, il est facile d'utiliser "Glassfish Embedded" pour tester unitairement ses EJB. De plus, intégrer une source de données vers une base de données quelconque ne demande pas beaucoup plus de travail.

Puisqu'une DataSource MySQL est maintenant configurée, on peut facilement développer des entités JPA (bean annotés avec @Entity) manipulées par un EntityManager. Comment ? En configurant  une unité de persistance utilisant notre source de données "jdbc/test" dans un fichier "persistence.xml".

Cela fera l'objet d'un prochain article. [Note : Je pense faire un autre tutorial mettant en œuvre Arquillian].


Enfin, pour information, sachez que Glassfish Embedded ne s'arrête pas à de simples tests. En effet, ce dernier intègre un ORB client (gestionnaire d'objets distribués basé sur CORBA) ce qui signifie qu'il peut tout-à-fait se connecter à des EJB distants. En revanche, il ne permet pas d'héberger lui-même de tels composants.

11 commentaires:

  1. Merci, ça m'a bien aidé dans le cadre d'un big project d'entreprise

    RépondreSupprimer
  2. Bonjour, pourrai je savoir comment je fais avec l'ide glassfish sans maven ?

    Merci d'avance

    RépondreSupprimer
  3. Bonjour, pouvez-vous m'indiquer l'IDE que vous souhaitez utiliser... Netbeans ou Eclipse ?

    RépondreSupprimer
    Réponses
    1. Bonjour sydisnet,

      pourrai-je savoir comment faire avec Eclipse et glassfish sans maven ?

      Merci d'avance

      Supprimer
  4. Bonjour,
    merci pour votre article, ça marche bien sauf dans le cas suivant: j'ai un ejb (stateless) qui contient également des annotations @WebService. Et le container me donne un erreur du genre:

    Aborting, Failed to start container org.glassfish.webservices.WebServicesContainer

    et du coup, les tests ne passent pas.
    Est-ce que c'est rédhibitoire, la co-existence des deux ?

    voici un bout du code:

    @Stateless
    @Remote(MRIAccountingUserInformationService.class)
    @Local(MRIAccountingUserInformationService.class)
    @WebService(serviceName="MRIAccountingUserInformation", portName="MRIAccountingUserInformationPort")
    @SOAPBinding(style=Style.DOCUMENT, use=SOAPBinding.Use.LITERAL, parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
    public class MRIAccountingUserInformationBean implements MRIAccountingUserInformationService {
    /**
    * The entity manager that accesses the accounting mysql database.
    */
    @PersistenceContext(unitName="accounting")
    private EntityManager manager;

    etc.

    RépondreSupprimer
    Réponses
    1. Bonjour,

      L'API "EJB Embedded Container" ne gère pas les services Web. C'est un sous-ensemble d'un conteneur EJB.

      En revanche, il est tout-à-fait possible de tester une application mettant en œuvre des EJB, des beans CDI et des services Web, à l'aide d'Arquillian et d'une installation complète de Glassfish.

      Supprimer
  5. Bonjour sydisnet,
    excellent article!
    Mais avec le JNDI: java:global/NOM_APPLICATION/NOM_DE_LA_CLASSE_EJB
    ca n'a pas fonctionné pour moi par contrre avec:
    java:global/NOM_APPLICATION/CLASSES/NOM_DE_LA_CLASSE_EJB
    (un /CLASSES/ entre le nom de l'application et celui de l'EJB) ca a marché.
    Merci 1000 et 1 fois.

    Meziane

    RépondreSupprimer
    Réponses
    1. Bonjour,

      Merci beaucoup :) En effet, vous avez raison. Depuis Glassfish 3.1 et +, (l'article a été écrit initialement pour Glassfish 3.0.1), il faut rajouter "/CLASSES" entre le nom de l'application et le nom de la classe d'implémentation du bean.

      Supprimer
  6. Bonjour sydisnet,
    j'ai un probleme qui me tracasse énormément:
    Je travaille sur un projet JEE, pour lequel j'ai concu la base de données, j'ai ecrit ou pour dire vrai j'ai adapté les entities générées par Netbeans à partir de la BDD.
    J'ai ecrit et testé les ensuite les beans ou ejbs.
    Enfin j'ai ecrit un singleton webservice dans lequel les autres ejbs sont injectés.
    Dans ce webservice toute les opérations CRUD fonctionnent à merveille.
    Au niveau du client (Standalone Java SE Application) par contre ca coince: les objets recus sont "détachés" ou sans identificateur (clé primaire)!!
    Les simples opérations de lecture (Read) fonctionnent que ce soit une liste ou ById, Insert (create) fonctionne aussi.
    Mais update et remove ne fonctionnent pas: au lieu d'un update, l'objet en édition est inséré avec une autre clé primaire.
    J'ai cru avoir compris qu'avec EJB3.1 on a plus besoin des DTOs (Data Transfer Objects).
    Au fait Netbeans a généré des DTOs, classes-miroirs de mes entities (avec les mêmes noms) mais sans clés primaires, les autres fields comme string.
    Dans mon client, je suis obligé par Netbeans d'utilisés ces "pseudo?" DTOs au lieu de mes entities.
    J'ai essayé de tricher en introduisant les clés primaires et en changeant le WSDL: pas de chance!
    J'ai l'impression qu'il me manque (au moins) une pièce au puzzle!
    It will be great If you could help me anyhow!
    Car je coince et je ne sais plus comment faire!
    Merci d'avance.
    Meziane

    RépondreSupprimer
    Réponses
    1. Ouuuuuuuuuuuf!
      Problème résolu: Netbeans a (auto)géneré des DTOs sans Ids, je ne sais pour quelle raison.
      Ca m'a couté beaucoup de temps: EJBs surout dnas la version 3.1 sont trés faciles à comprendre en théorie, en pratique les IDEs (Netbeans, Eclipse, JDeveloper) qui essayent de nous faciliter la tâche nous rendent le travail, sans le vouloir, amer.

      Meziane

      Supprimer