Skip to content

6. Version 2 : Architecture OpenEJB / JPA

6.1. Introduction aux principes du portage

Nous présentons ici les principes qui vont gouverner le portage d'une application JPA / Spring / Hibernate vers une application JPA / OpenEJB / EclipseLink. On attendra le paragraphe 6.2 pour créer les projets Maven.

6.1.1. Les deux architectures

L'implémentation actuelle avec Spring / Hibernate

L'implémentation à construire avec OpenEJB / EclipseLink

6.1.2. Les bibliothèques des projets

  • les couches [DAO] et [metier] ne sont plus instanciées par Spring. Elles le sont par le conteneur OpenEJB.
  • les bibliothèques du conteneur Spring et sa configuration disparaissent au profit des bibliothèques du conteneur OpenEJB et sa configuration.
  • les bibliothèques de la couche JPA / Hibernate sont remplacées par celles de la couche JPA / EclipseLink

  • le fichier [META-INF/persistence.xml] configurant la couche JPA devient le suivant :
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pam-openejb-ui-metier-dao-jpa-eclipselinkPU" transaction-type="JTA">
    <!-- entités JPA -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- le fournisseur JPA est EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- propriétés provider -->
    <properties>
      <property name="eclipselink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • ligne 3 : les transactions dans un conteneur EJB sont de type JTA (Java Transaction API). Elles étaient de type RESOURCE_LOCAL avec Spring.
  • ligne 9 : l'implémentation JPA utilisée est EclipseLink
  • lignes 5-7 : les entités gérées par la couche JPA
  • lignes 11-13 : propriétés du provider EclipseLink
  • ligne 12 : à chaque exécution, les tables seront créées

Les caractéristiques JDBC de la source de données JTA utilisée par le conteneur OpenEJB seront précisées par le fichier de configuration [conf/openejb.conf] suivant :

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<openejb>
  <Resource id="Default JDBC Database">
    JdbcDriver com.mysql.jdbc.Driver
    JdbcUrl jdbc:mysql://localhost:3306/dbpam_eclipselink
    UserName root
  </Resource>
</openejb>
  • ligne 3 : on utilise l'id Default JDBC Database lorsqu'on travaille avec un conteneur OpenEJB embarqué (embedded) dans l'application elle-même.
  • ligne 5 : nous utilisons une base MySQL [dbpam_eclipselink]

6.1.4. Implémentation de la couche [DAO] par des EJB

  • Les classes implémentant la couche [DAO] deviennent des EJB. Prenons l'exemple de la classe [CotisationDao] :

L'interface [ICotisationDao] dans la version Spring était la suivante :

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
  // créer une nouvelle cotisation
  Cotisation create(Cotisation cotisation);
  // modifier une cotisation existante
  Cotisation edit(Cotisation cotisation);
  // supprimer une cotisation existante
  void destroy(Cotisation cotisation);
  // chercher une cotisation particulière
  Cotisation find(Long id);
  // obtenir tous les objets Cotisation
  List<Cotisation> findAll();

}

L'EJB va implémenter cette même interface sous deux formes différentes : une locale et une distante. L'interface locale peut être utilisée par un client s'exécutant dans la même JVM, l'interface distante par un client s'exécutant dans une autre JVM.

L'interface locale :

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}
  • ligne 6 : l'interface [ICotisationDaoLocal] hérite de l'interface [ICotisationDao] pour en reprendre toutes les méthodes. Elle n'en ajoute pas de nouvelles.
  • ligne 5 : l'annotation @Local en fait une interface locale pour l'EJB qui l'implémentera.

L'interface distante :

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}
  • ligne 6 : l'interface [ICotisationDaoRemote] hérite de l'interface [ICotisationDao] pour en reprendre toutes les méthodes. Elle n'en ajoute pas de nouvelles.
  • ligne 5 : l'annotation @Remote en fait une interface distante pour l'EJB qui l'implémentera.

La couche [DAO] est implémentée par un EJB implémentant les deux interfaces (ce n'est pas obligatoire) :

1
2
3
4
5
6
@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CotisationDao implements ICotisationDaoLocal, ICotisationDaoRemote {

  @PersistenceContext
  private EntityManager em;
  • ligne 1 : l'annotation @Stateless qui fait de la classe un EJB
  • ligne 2 : l'annotation @TransactionAttribute qui fait que chaque méthode de la classe s'exécutera au sein d'une transaction.
  • ligne 5 : l'annotation @PersistenceContext qui injecte dans la classe [CotisationDao] l'EntityManager de la couche JPA. Elle est identique à ce qu'on avait dans la version Spring.

Lorsque l'interface locale de la couche [DAO] est utilisée, le client de cette interface s'exécute dans la même JVM.

Ci-dessus, les couches [metier] et [DAO] échangent des objets par référence. Lorsqu'une couche change l'objet partagé, l'autre couche voit ce changement.

Lorsque l'interface distante de la couche [DAO] est utilisée, le client de cette interface s'exécute habituellement dans une autre JVM.

Ci-dessus, les couches [metier] et [DAO] échangent des objets par valeur (sérialisation de l'objet échangé). Lorsqu'une couche change un objet partagé, l'autre couche ne voit ce changement que si l'objet modifié lui est renvoyé.

6.1.5. Implémentation de la couche [metier] par un EJB

  • La classe implémentant la couche [metier] devient elle aussi un EJB implémentant une interface locale et distante. L'interface initiale [IMetier] était la suivante :
package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
  // obtenir la feuille de salaire
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // liste des employés
  List<Employe> findAllEmployes();
}

On crée une interface locale et une interface distante à partir de l'interface précédente :

1
2
3
4
5
6
7
package metier;

import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{
}
1
2
3
4
5
6
7
package metier;

import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{
}

L'EJB de la couche [metier] implémente ces deux interfaces :

@Stateless()
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote {

  // référence sur les couches [DAO] locales
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;
  • lignes 1-2 : définissent un EJB dont chaque méthode s'exécute dans une transaction.
  • ligne 7 : une référence sur l'interface locale de l'EJB [CotisationDao].
  • ligne 6 : l'annotation @EJB demande à ce que le conteneur EJB injecte une référence sur l'interface locale de l'EJB [CotisationDao].
  • lignes 8-11 : on refait la même chose pour les interfaces locales des EJB [EmployeDao] et [IndemniteDao].

Au final, lorsque l'EJB [Metier] sera instancié, les champs des lignes 7, 9 et 11 seront initialisés avec des références sur les interfaces locales des trois EJB de la couche [DAO]. On fait donc ici l'hypothèse que les couches [metier] et [DAO] s'exécuteront dans la même JVM.

6.1.6. Les clients des EJB

Dans le schéma ci-dessus, pour communiquer avec la couche [metier], la couche [ui] doit obtenir une référence sur l'interface distante de l'EJB de la couche [metier].

Dans le schéma ci-dessus, pour communiquer avec la couche [metier], la couche [ui] doit obtenir une référence sur l'interface locale de l'EJB de la couche [metier]. La méthode pour obtenir ces références diffère d'un conteneur à l'autre. Pour le conteneur OpenEJB, on pourra procéder comme suit :

Référence sur l'interface locale :

1
2
3
4
5
6
7
8
9
    // on configure le conteneur Open EJB embarqué
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // initialisation du contexte JNDI avec les propriétés précédentes
    InitialContext initialContext = new InitialContext(properties);
    // instanciation couches DAO
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
  • lignes 2-5 : le conteneur OpenEJB est initialisé.
  • ligne 5 : on a un contexte JNDI (Java Naming and Directory Interface) qui permet d'obtenir des références sur les EJB. Chaque EJB est désigné par un nom JNDI :
  • (suite)
    • pour l'interface locale on ajoute Local au nom de l'EJB (lignes 7-9)
    • pour l'interface distante on ajoute Remote au nom de l'EJB

Avec Java EE 5, ces règles changent selon le conteneur EJB. C'est une difficulté. Java EE 6 a introduit une notation JNDI portable sur tous les serveurs d'applications.

Le code précédent récupère des références sur les interfaces locales des EJB via leurs noms JNDI. Nous avons dit précédemment que celles-ci pouvaient également être obtenues via l'annotation @EJB. On pourrait donc vouloir écrire :

@EJB
private IemployeDaoLocal employeDaoLocal ;

L'annotation @EJB n'est honorée que si elle appartient à une classe chargée par le conteneur EJB. Ce sera le cas de la classe [Metier] par exemple. Le code ci-dessus, lui appartiendra à une classe console qui ne sera pas chargée par le conteneur EJB. On est donc obligés d'utiliser les noms JNDI des EJB.

Ci-dessous, le code pour avoir une référence sur l'interface distante de l'EJB [Metier] :

1
2
3
4
5
6
7
8
    // on configure le conteneur Open EJB embarqué
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
    // initialisation du contexte JNDI du conteneur EJB
    InitialContext initialContext = new InitialContext(properties);

    // instanciation couche métier distante
metier = (IMetierRemote) initialContext.lookup("MetierRemote");

6.2. Travail pratique

On se propose de porter l'application Netbeans Spring / Hibernate vers une architecture OpenEJB / EclipseLink.

L'implémentation actuelle avec Spring / Hibernate

L'implémentation à construire avec OpenEJB / EclipseLink

Si elle n'existe pas, créez la base MySQL [dbpam_eclipselink]. Si elle existe, supprimez toutes ses tables. Créez une connexion Netbeans vers cette base comme il a été décrit paragraphe 6.2.1.

6.2.2. Configuration initiale du projet Netbeans

  • charger le projet Maven [mv-pam-spring-hibernate]
  • créer un nouveau projet Maven Java [mv-pam-openejb-eclipselink] [1]
  • dans l'onglet [Files] [2], créer un dossier [conf] [3] sous la racine du projet
  • placer dans ce dossier, le fichier [openejb.conf] [4] suivant :
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<openejb>
  <Resource id="Default JDBC Database">
    JdbcDriver com.mysql.jdbc.Driver
    JdbcUrl jdbc:mysql://localhost:3306/dbpam_eclipselink
    UserName root
  </Resource>
</openejb>
  • créer le dossier [src / main/ resources/ META-INF] [5]
  • y mettre le fichier [persistence.xml] [6] suivant :

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbpam_eclipselinkPU" transaction-type="JTA">
    <!-- le fournisseur JPA est EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- entités Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- propriétés provider EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • ligne 12 : on demande des logs fins à EclipseLink,
  • ligne 13 : les tables seront créées à l'instanciation de la couche JPA,
  • ajouter les bibliothèques OpenEJB, EclipseLink ainsi que le pilote JDBC de MySQL au fichier [pom.xml] du projet :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-pam-openejb-eclipselink</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-pam-openejb-eclipselink</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>openejb-core</artifactId>
      <version>4.0.0</version>
    </dependency>                
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>javax.persistence</artifactId>
      <version>2.0.3</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
  
  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
  </repositories>
  
</project>
  • lignes 18-22 : la dépendance OpenEJB,
  • lignes 30-39 : les dépendances EclipseLink,
  • lignes 41-44 : la dépendance du pilote JDBC de MySQL

6.2.3. Portage de la couche [DAO]

Nous allons faire le portage de la couche [DAO] par copie de paquetages du projet [mv-pam-spring-hibernate] vers le projet [mv-pam-openejb-eclipselink].

  • copier les packages [dao, exception, jpa]
 

Les erreurs signalées ci-dessus viennent du fait que la couche [DAO] copiée utilise Spring et que les bibliothèques Spring ne font plus partie du projet.

6.2.3.1. L'EJB [CotisationDao]

Nous créons les interfaces locale et distante du futur EJB [CotisationDao] :

L'interface locale ICotisationDaoLocal :

1
2
3
4
5
6
7
package dao;

import javax.ejb.Local;

@Local
public interface ICotisationDaoLocal extends ICotisationDao{
}

Pour avoir les bons import de paquetages, faire [clic droit sur le code / Fix Imports].

L'interface distante ICotisationDaoRemote :

1
2
3
4
5
6
7
package dao;

import javax.ejb.Remote;

@Remote
public interface ICotisationDaoRemote extends ICotisationDao{
}

Puis nous modifions la classe [CotisationDao] pour en faire un EJB :

...
import javax.persistence.PersistenceContext;
import jpa.Cotisation;

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CotisationDao implements ICotisationDaoLocal, ICotisationDaoRemote {

  @PersistenceContext
  private EntityManager em;
...  

L'import que faisait cette classe sur le framework Spring disparaît. Faire un [Clean and Build] du projet :

En [1], il n'y a plus d'erreurs sur la classe [CotisationDao].

6.2.3.2. Les EJB [EmployeDao] et [IndemniteDao]

On répète la même démarche pour les autres éléments de la couche [DAO] :

  • interfaces IEmployeDaoLocal, IEmployeDaoRemote dérivées de IEmployeDao
  • EJB EmployeDao implémentant ces deux interfaces
  • interfaces IIndemniteDaoLocal, IIndemniteDaoRemote dérivées de IIndemniteDao
  • EJB IndemniteDao implémentant ces deux interfaces

Ceci fait, il n'y a plus d'erreurs dans le projet [2].

6.2.3.3. La classe [PamException]

La classe [PamException] reste ce qu'elle était à un détail près :

package exception;

import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class PamException extends RuntimeException {

  // code d'erreur
  private int code;
...

La ligne 5 est ajoutée. Pour avoir les bons import faire [Fix imports].

Pour comprendre l'annotation de la ligne 5, il faut se rappeler que chaque méthode des EJB de notre couche [DAO] :

  • s'exécute dans une transaction démarrée et terminée par le conteneur EJB
  • lance une exception de type [PamException] dès que quelque chose se passe mal

Lorsque la couche [metier] appelle une méthode M de la couche [DAO], cet appel est intercepté par le conteneur EJB. Tout se passe comme s'il y avait une classe intermédiaire entre la couche [metier] et la couche [DAO], ici appelée [Proxy EJB], interceptant tous les appels vers la couche [DAO]. Lorsque l'appel à la méthode M de la couche [DAO] est interceptée, le Proxy EJB démarre une transaction puis passe la main à la méthode M de la couche [DAO] qui s'exécute alors dans cette transaction. La méthode M se termine avec ou sans exception.

  • si la méthode M se termine sans exception, l'exécution revient au proxy EJB qui termine la transaction en la validant par un commit. Le flux d'exécution est ensuite rendu à la méthode appelante de la couche [metier]
  • si la méthode M se termine avec exception, l'exécution revient au proxy EJB qui termine la transaction en l'invalidant par un rollback. De plus il encapsule cette exception dans un type EJBException. Le flux d'exécution est ensuite rendu à la méthode appelante de la couche [metier] qui reçoit donc une EJBException. L'annotation de la ligne 5 ci-dessus empêche cette encapsulation. La couche [metier] recevra donc une PamException. De plus, l'attribut rollback=true indique au proxy EJB que lorsqu'il reçoit une PamException, il doit invalider la transaction.

6.2.3.4. Test de la couche [DAO]

Notre couche [DAO] implémentée par des EJB peut être testée. Nous commençons par copier le package [dao] de [Test Packages] du projet [mv-pam-springhibernate] dans le projet en cours de construction [1] :

Nous ne conservons que le test [JUnitInitDB] qui initialise la base avec quelques données [2]. Nous renommons la classe [ JUnitInitDbLocal] [3]. La classe [JUnitInitDBLocal] utilisera l'interface locale des EJB de la couche [DAO].

Nous modifions tout d'abord la classe [JUnitInitDBLocal] de la façon suivante :

public class JUnitInitDBLocal {

  static private IEmployeDaoLocal employeDao = null;
  static private ICotisationDaoLocal cotisationDao = null;
  static private IIndemniteDaoLocal indemniteDao = null;

  @BeforeClass
  public static void init() throws Exception {
    // on configure le conteneur Open EJB embarqué
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // initialisation du contexte JNDI avec les propriétés précédentes
    InitialContext initialContext = new InitialContext(properties);
    // instanciation couches DAO locales
    employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
}

...
  • lignes 3-5 : des références sur les interfaces locales des EJB de la couche [DAO]
  • ligne 7 : @BeforeClass annote la méthode exécutée au démarrage du test JUnit
  • lignes 10-13 : initialisation du conteneur OpenEJB. Cette initialisation est propriétaire et change avec chaque conteneur EJB.
  • ligne 13 : on a un contexte JNDI (Java Naming and Directory Interface) qui permet d'accéder aux EJB via des noms. Avec OpenEJB, l'interface locale d'un EJB E est désignée par ELocal et l'interface distante par ERemote.
  • lignes 15-17 : on demande au contexte JNDI, une référence sur les interfaces locales des EJB [EmployeDao, CotisationDao, IndemniteDao].

On construit le projet (Build), on lance le serveur MySQL si besoin est, on exécute le test JUnitInitDBLocal. On rappelle que le fichier [persistence.xml] a été configuré pour recréer les tables à chaque exécution. Avant l'exécution du test, il est préférable de supprimer les éventuelles tables de la base MySQL [dbpam_eclipselink].

  • en [1], dans l'onglet [Services], on supprime les tables de la connexion Netbeans établie au paragraphe 6.2.1.
  • en [2], la base [dbpam_eclipselink] n'a plus de tables
  • en [3], le projet est construit
  • en [4], le test JUnitInitDBLocal est exécuté
  • en [5], le test a été réussi
  • en [6], on rafraîchit la connexion Netbeans
  • en [7], on voit les 4 tables créées par la couche JPA. Le test avait pour but de les remplir. On visualise le contenu de l'une d'entre-elles
  • en [8], le contenu de la table [EMPLOYES]

Le conteneur OpenEJB a affiché des logs dans la console :

Infos - PersistenceUnit(name=dbpam_eclipselinkPU, provider=org.eclipse.persistence.jpa.PersistenceProvider) - provider time 396ms
Infos - Jndi(name=CotisationDaoLocal) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao!dao.ICotisationDaoLocal) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=CotisationDaoRemote) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao!dao.ICotisationDaoRemote) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/CotisationDao) --> Ejb(deployment-id=CotisationDao)
Infos - Jndi(name=EmployeDaoLocal) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao!dao.IEmployeDaoLocal) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=EmployeDaoRemote) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao!dao.IEmployeDaoRemote) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/EmployeDao) --> Ejb(deployment-id=EmployeDao)
Infos - Jndi(name=IndemniteDaoLocal) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao!dao.IIndemniteDaoLocal) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=IndemniteDaoRemote) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao!dao.IIndemniteDaoRemote) --> Ejb(deployment-id=IndemniteDao)
Infos - Jndi(name=global/classpath.ear/mv-pam-openejb-eclipselink/IndemniteDao) --> Ejb(deployment-id=IndemniteDao)
Infos - existing thread singleton service in SystemInstance() org.apache.openejb.cdi.ThreadSingletonServiceImpl@624a240d
Infos - OpenWebBeans Container is starting...
Infos - Adding OpenWebBeansPlugin : [CdiPlugin]
Infos - All injection points were validated successfully.
Infos - OpenWebBeans Container has started, it took [70] ms.
Infos - Created Ejb(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
Infos - Created Ejb(deployment-id=EmployeDao, ejb-name=EmployeDao, container=Default Stateless Container)
Infos - Created Ejb(deployment-id=CotisationDao, ejb-name=CotisationDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=EmployeDao, ejb-name=EmployeDao, container=Default Stateless Container)
Infos - Started Ejb(deployment-id=CotisationDao, ejb-name=CotisationDao, container=Default Stateless Container)
Infos - Deployed Application(path=D:\data\istia-1112\netbeans\glassfish\mv-pam\tmp\mv-pam-openejb-eclipselink\classpath.ear)
  • lignes 2-3 : les deux noms JNDI de l'EJB [CotisationDaoLocal],
  • lignes 4-5 : les deux noms JNDI de l'EJB [CotisationDaoRemote],
  • lignes 7-8 :les deux noms JNDI de l'EJB [EmployeDaoLocal],
  • lignes 9-10 :les deux noms JNDI de l'EJB [EmployeDaoRemote],
  • lignes 12-13 :les deux noms JNDI de l'EJB [IndemniteDaoLocal],
  • lignes 14-15 :les deux noms JNDI de l'EJB [EmployeDaoRemote].

Nous refaisons le même test, en utilisant cette fois-ci l'interface distante des EJB.

En [1], la classe [JUnitInitDBLocal] a été dupliquée (copy / paste) dans [JUnitInitDBRemote]. Dans cette classe, nous remplaçons les interfaces locales par les interfaces distantes :

public class JUnitInitDBRemote {

  static private IEmployeDaoRemote employeDao = null;
  static private ICotisationDaoRemote cotisationDao = null;
  static private IIndemniteDaoRemote indemniteDao = null;

  @BeforeClass
  public static void init() throws Exception {
    // on configure le conteneur Open EJB embarqué
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
     // initialisation du contexte JNDI avec les propriétés précédentes
    InitialContext initialContext = new InitialContext(properties);
    // instanciation couches DAO distantes
    employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
}

Ceci fait, la nouvelle classe de test peut être exécutée. Auparavant, avec la connexion Netbeans [dbpam_eclipselink], supprimez les tables de la base [dbpam_eclipselink].

 

Avec la connexion Netbeans [dbpam_eclipselink], vérifiez que la base a été remplie.

6.2.4. Portage de la couche [metier]

Nous allons faire le portage de la couche [metier] par copie de packages du projet [mv-pam-spring-hibernate] vers le projet [mv-pam-openejb-eclipselink].

Les erreurs signalées ci-dessus [1] viennent du fait que la couche [metier] copiée utilise Spring et que les bibliothèques Spring ne font plus partie du projet.

6.2.4.1. L'EJB [Metier]

Nous suivons la même démarche que celle décrite pour l'EJB [CotisationDao]. Nous créons tout d'abord en [2] les interfaces locale et distante du futur EJB [Metier]. Elles dérivent toutes deux de l'interface initiale [IMetier].

1
2
3
4
5
6
7
8
package metier;

import javax.ejb.Local;

@Local
public interface IMetierLocal extends IMetier{

}
1
2
3
4
5
6
7
8
package metier;

import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{

}

Ceci fait, en [3] nous modifions la classe [Metier] afin qu'elle devienne un EJB :

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote {

  // références sur la couche [DAO] locale
  @EJB
  private ICotisationDaoLocal cotisationDao = null;
  @EJB
  private IEmployeDaoLocal employeDao = null;
  @EJB
  private IIndemniteDaoLocal indemniteDao = null;

  // obtenir la feuille de salaire
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
          double nbHeuresTravaillées, int nbJoursTravaillés) {
    // on récupère les informations liées à l'employé
...
  • ligne 1 : l'annotation @Stateless fait de la classe un EJB
  • ligne 2 : chaque méthode de la classe s'exécutera dans une transaction
  • ligne 3 : l'EJB [Metier] implémente les deux interfaces locale et distante que nous venons de définir
  • ligne 7 : l'EJB [Metier] va utiliser l'EJB [CotisationDao] via l'interface locale de celui-ci. Cela signifie que les couches [metier] et [DAO] doivent s'exécuter dans la même JVM.
  • ligne 6 : l'annotation @EJB fait en sorte que le conteneur EJB injecte lui-même la référence sur l'interface locale de l'EJB [CotisationDao]. L'autre façon que nous avons rencontrée est d'utiliser un contexte JNDI.
  • lignes 8-11 : le même mécanisme est utilisé pour les deux autres EJB de la couche [DAO].

6.2.4.2. Test de la couche [metier]

Notre couche [metier] implémentée par un EJB peut être testée. Nous commençons par copier le package [metier] de [Test Packages] du projet [mv-pam-spring-hibernate] dans le projet en cours de construction [1] :

  • en [1], le résultat de la copie
  • en [2], on supprime le 1er test
  • en [3], le test restant est renommé [JUnitMetierLocal]

La classe [JUnitMetierLocal] devient la suivante :

public class JUnitMetierLocal {

// couche métier locale
  static private IMetierLocal metier;

  @BeforeClass
  public static void init() throws NamingException {
    // on configure le conteneur Open EJB embarqué
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
    // initialisation du contexte JNDI avec les propriétés précédentes
    InitialContext initialContext = new InitialContext(properties);

    // instanciation couches DAO locales
    IEmployeDaoLocal employeDao = (IEmployeDaoLocal) initialContext.lookup("EmployeDaoLocal");
    ICotisationDaoLocal cotisationDao = (ICotisationDaoLocal) initialContext.lookup("CotisationDaoLocal");
    IIndemniteDaoLocal indemniteDao = (IIndemniteDaoLocal) initialContext.lookup("IndemniteDaoLocal");
    // instanciation couche métier locale
    metier = (IMetierLocal) initialContext.lookup("MetierLocal");

    // on vide la base
...
}
  • ligne 4 : une référence sur l'interface locale de l'EJB [Metier]
  • lignes 8-12 : configuration du conteneur OpenEJB identique à celle faite dans le test de la couche [DAO]
  • lignes 15-19 : on demande au contexte JNDI de la ligne 12, des références sur les 3 EJB de la couche [DAO] et sur l'EJB de la couche [metier]. Les EJB de la couche [DAO] vont servir à initialiser la base, l'EJB de la couche [metier] à faire des tests de calculs de salaire.

L'exécution du test [JUnitMetierLocal] donne le résultat suivant [1] :

En [2], on duplique [JUnitMetierLocal] en [JUnitMetierRemote] pour tester cette fois-ci l'interface distante de l'EJB [Metier]. Le code de [JUnitMetierRemote] est modifié pour utiliser cette interface distante. Le reste ne change pas.

public class JUnitMetierRemote {

  // couche métier distante
  static private IMetierRemote metier;

  @BeforeClass
  public static void init() throws NamingException {
    // on configure le conteneur Open EJB embarqué
    Properties properties = new Properties();
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
    // initialisation du contexte JNDI avec les propriétés précédentes
    InitialContext initialContext = new InitialContext(properties);

    // instanciation couches DAO distantes
    IEmployeDaoRemote employeDao = (IEmployeDaoRemote) initialContext.lookup("EmployeDaoRemote");
    ICotisationDaoRemote cotisationDao = (ICotisationDaoRemote) initialContext.lookup("CotisationDaoRemote");
    IIndemniteDaoRemote indemniteDao = (IIndemniteDaoRemote) initialContext.lookup("IndemniteDaoRemote");
    // instanciation couche métier distante
    metier = (IMetierRemote) initialContext.lookup("MetierRemote");

    // on vide la base
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
    // on la remplit
    Indemnite indemnite1=new Indemnite(1,1.93,2,3,12);
    Indemnite indemnite2=new Indemnite(2,2.1,2.1,3.1,15);
    indemnite1=indemniteDao.create(indemnite1);
    indemnite2=indemniteDao.create(indemnite2);
    employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }
}
  • lignes 4 et 19 : on utilise l'interface distante de l'EJB [Metier].
  • lignes 15-17 : on utilise les interfaces distantes de la couche [DAO]
  • lignes 34-35 : parce qu'avec les interfaces distantes, les objets échangés entre le client et le serveur le sont par valeur, il faut récupérer le résultat rendu par la méthode create(Indemnite i). Ce n'était pas obligatoire avec les interfaces locales où là les objets sont passés par référence.

Ceci fait, le projet peut être construit et le test [JUnitMetierRemote] exécuté :

  

6.2.5. Portage de la couche [console]

Nous allons faire le portage de la couche [console] par copie de packages du projet [mv-pam-spring-hibernate] vers le projet [mv-pam-openejb-eclipselink].

Les erreurs signalées ci-dessus [1] viennent du fait que la couche [metier] copiée utilise Spring et que les bibliothèques Spring ne font plus partie du projet. En [2], la classe [Main] est renommée [MainLocal]. Elle utilisera l'interface locale de l'EJB [Metier].

Le code de la classe [MainLocal] évolue de la façon suivante :

  public static void main(String[] args) {
    // données locales
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
...
    // des erreurs ?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
    // c'est bon - on peut demander la feuille de salaire à la couche [métier]
    IMetierLocal metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
      // on configure le conteneur Open EJB embarqué
      Properties properties = new Properties();
      properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
      // initialisation du contexte JNDI avec les propriétés précédentes
      InitialContext initialContext = new InitialContext(properties);
      // instanciation couche métier locale
      metier = (IMetierLocal) initialContext.lookup("MetierLocal");
      // calcul de la feuille de salaire
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : " + ex.toString());
      return;
    }
    // affichage détaillé
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
....

Les modifications se situent dans les lignes 13-25. C'est la façon d'avoir une référence sur la couche [metier] qui change (lignes 17-22). Nous n'expliquons pas le nouveau code qui a déjà été vu dans des exemples précédents. Une fois ces modifications faites, le projet ne présente plus d'erreurs (cf [3]).

Nous configurons le projet pour qu'il soit exécuté avec des arguments [1] :

Pour que l'application console s'exécute normalement, il faut qu'il y ait des données dans la base. Pour cela, il faut modifier le fichier [META-INF/persistence.xml] :


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence              http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="dbpam_eclipselinkPU" transaction-type="JTA">
    <!-- le fournisseur JPA est EclipseLink -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <!-- entités Jpa -->
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <!-- propriétés provider EclipseLink -->
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/>
      <!--
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      -->
    </properties>
  </persistence-unit>
</persistence>

La ligne 14 qui faisait que les tables de la base de données étaient recréées à chaque exécution est mise en commentaires. Le projet doit être reconstruit (Clean and Build) pour que cette modification soit prise en compte. Ceci fait, on peut exécuter le programme. Si tout va bien, on obtient un affichage console analogue au suivant :

.......
INFO - Created EJB(deployment-id=IndemniteDao, ejb-name=IndemniteDao, container=Default Stateless Container)
INFO - Deployed Application(path=classpath.ear)
[EL Info]: 2009-09-30 15:09:21.109--ServerSession(16658781)--EclipseLink, version: Eclipse Persistence Services - 1.1.2.v20090612-r4475
[EL Info]: 2009-09-30 15:09:21.937--ServerSession(16658781)--file:/C:/temp/09-09-28/pam-console-metier-dao-openejb-eclipselink-0910/build/classes/-jpa login successful
Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé : 
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations : 
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités : 
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire : 
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

BUILD SUCCESSFUL (total time: 4 seconds)

Nous avons utilisé ici l'interface locale de la couche [metier]. Nous utilisons maintenant son interface distante dans une seconde classe console :

En [1], la classe [MainLocal] a été dupliquée dans [MainRemote]. Le code de [MainRemote] est modifié pour utiliser l'interface distante de la couche [metier] :

// c'est bon - on peut demander la feuille de salaire à la couche [metier]
    IMetierRemote metier = null;
    FeuilleSalaire feuilleSalaire = null;
    try {
      // on configure le conteneur Open EJB embarqué
...
      // instanciation couche métier distante
      metier = (IMetierRemote) initialContext.lookup("MetierRemote");
      // calcul de la feuille de salaire
      feuilleSalaire = metier.calculerFeuilleSalaire(args[0], nbHeuresTravaillées, nbJoursTravaillés);
    } catch (PamException ex) {
...
    } catch (Exception ex) {
...
    }

Les modifications sont faites aux lignes 2 et 8. Le projet est configuré [2] pour exécuter la classe [MainRemote]. Son exécution donne les mêmes résultats que précédemment.

6.3. Conclusion

Nous avons montré comment porter une architecture Spring / Hibernate vers une architecture OpenEJB / EclipseLink.

L'architecture Spring / Hibernate

L'architecture OpenEJB / EclipseLink

Le portage a pu se faire sans trop de difficultés parce que l'application initiale avait été structurée en couches. Ce point est important à comprendre.