ECOM: un mini-projet Enterprise Java Beans

DESS-GI, Option Système et réseaux - année 2001-2002
Fabienne Boyer, Sacha Krakowiak
Complément : Didier Donsez
1. Objectifs du projet
2. Description du projet
   2.1 Programmation de composants EJB
   2.2 Composants de l'application ecom
   2.3 Gestion de la persistance des composants
3. Travail à réaliser
   3.1 Installation d'un serveur Jonas et de l'application ecom
   3.2 Etude d'un cas de base
   3.3 Travail sur l'application ecom
   3.4 Expérimentations supplémentaires
   3.5 Une solution

1. Objectifs du projet

L'objectif pédagogique du mini-projet est de faire pratiquer aux étudiants la programmation répartie d'une application à base de composants logiciels répartis, transactionnels et persistants. Plus précisément, le projet est réalisé sur une plate-forme EJB(Enterprise Java Beans), qui respecte la norme J2EE de Sun, considérée maintenant un standard assez largement accepté dans le domaine de la programmation de serveurs d'applications à base de composants. Outre la connaissance de ce standard, ce sont également des problèmes majeurs intervenant dans le développement d'une application répartie qui seront appréhendés par les étudiants, tels que les transactions distribuées ou la persistance de composants répartis. Dans ce projet, les étudiants resteront néanmoins utilisateurs des solutions fournies à ces problèmes par le standard J2EE.

2. Description du projet

Le projet consiste à étudier et à compléter une application de commerce électronique, dont la mise en oeuvre utilise des composants Java répartis appelés EJB (Enterprise Java Beans). La plate-forme d'exécution sous-jacente qui est utilisée dans ce projet est la plate-forme Jonas, qui est disponible en OpenSource au travers du groupe ObjectWeb.
 

2.1 Programmation de composants EJB

La première étape de votre travail consiste à lire (rapidement) la documentation de programmation fournie par Jonas. Pour concevoir une application EJB, il faut identifier les principaux beans qui vont la composer. Ce travail impose de choisir la catégorie de chaque bean : session ou entité. A la différence d'un bean session, un bean entité possède une image persistente dans une base de donnée. Pour chaque bean, il faut définir les classes ou les fichiers suivants : Les parties suivantes reviennent sur des aspects importants intervenants dans la programmation des beans, relatifs aux principes d'architecture, à la gestion des communications inter-beans et à la gestion des transactions.

Principe d'architecture
Le schéma suivant illustre le principe d'architecture utilisé pour gérer la notion de composant (bean). Tout bean n'est jamais accédé directement par un client (qui peut être également un bean). L'accès à une méthode métier d'un bean passe par un objet d'interposition (d'interface EJBObject), qui se charge de gérer les propriétés associées au bean. La classe d'un objet d'interposition est automatiquement généré par le compilateur jonas, en fonction de l'interface du bean auquel il est associé.

Tout bean entité ou session (interface EntityBean / SessionBean) possède une méthode setEntityContext qui permet de positionner un contexte courant, depuis lequel on peut récupérer une référence vers l'objet d'interposition courant. Tout objet d'interposition (interface EJBObject) fournit des méthodes (getHandle(), getPrimaryKey()) permettant de manipuler différentes sortes de références vers le bean (voir ci-après).
 

Références distribuées vers les beans
Pour comprendre comment mettre en place des communications entre des beans, ou entre un client et un bean donné, on peut consulter la documentation d'assemblage fournie par Jonas. Lorsqu'un client (qui peut être un bean) souhaite communiquer avec un bean B, il doit posséder une référence distribuée vers B. Une référence distribuée vers un bean est en fait une référence remote (au sens de java) vers son objet d'interposition.
Pour obtenir une référence distribuée, les scénarios suivants sont possibles :

La séquence suivante illustre le fonctionnement décrit dans le troisième cas. Dans cette séquence, on veut récupérer la référence distribuée d'un bean d'interface Account étant donné sa primary key. On utilise un contexte pour récupérer une référence sur un home. Un contexte peut aussi être utilisé pour récupérer des informations sur la session en cours (nom du client par exemple), ou bien pour consulter des variables d'environnement.
 
// Crée un contexte
initialContext = new InitialContext();
// Récupère une référence sur un Home
AccountHome home = null;
 try {
     home = (AccountHome)PortableRemoteObject.narrow(
           initialContext.lookup("AccountHome"),
           AccountHome.class);
 } catch (Exception e) {
     System.err.println("Cannot lookup AccountHome: " +e);
     System.exit(2);
 }
// Récupère un bean identifié par sa PK (clé primaire)
Account clientAccount = (Account)accountHome.findByPrimaryKey(clientAccountPK);
Lorsqu'une référence vers un bean B1 doit être stockée dans un bean persistant B2, alors soit B2 conserve un handler vers B1, soit B2 conserve la clé primaire de B1 ou bien une valeur équivalente. Selon le cas, les valeurs conservées dans la base seront lisibles ou non. Remarquons qu'aussi bien le handler que la clé primaire d'un bean B peuvent être obtenues via la référence distribuée de B (méthodes getHandle() et getPrimaryKey()).
Une référence distribuée vers un bean B reste valide durant toute l'exécution du serveur EJB qui héberge B. Même si ce serveur met en oeuvre une gestion de cache de beans, les références distribuées désignent en réalité les objets d'interception associés aux beans. Ces objets d'interception ne sont jamais déchargés.

Clé primaire
Tout bean entité possède une clé primaire permettant de le désigner de manière unique. La composition d'une clé primaire est définie par le programmeur, soit au travers d'une classe explicite nommée XXXBeanPK, soit en utilisant une classe Java standart telle que java.lang.Integer, ou java.lang.String par exemple.
Dans le premier cas, les champs de la classe XXXBeanPK doivent être un sous-ensemble des champs du bean (classe XXXBean). C'est de cette manière que Jonas pourra savoir, étant donné un bean, quelle est sa clé primaire.
Dans le deuxième cas (PK de classe Java standard), la configuration associé au bean XXX devra définir quel est le champs du bean (champs de la classe XXXBean) qui correspond à la clé primaire.

Persistance
Un bean entité peut être de catégorie "container-managed persistence" ou bien "entity-managed persistence". Dans le premier cas, sa persistance est automatiquement gérée par le support Jonas, en fonction de règles de "mapping" avec une base de données fournies par le programmeur sous une forme déclarative. Dans le second cas, c'est au programmeur de gérer explicitement la persistance des beans dans ses programmes, en implémentant les méthodes de création, chargement, etc (ejbCreate, ejbLoad, ejbStore,...). Dans le projet ecom, nous allons utiliser des beans de catégorie "container-managed persistence". Des informations plus précises concernant la programmation de ce type de beans sont données en 2.3.

Transactions
Un bean donné peut nécessiter de s'exécuter de manière transactionnelle. Selon le cas, toutes ses méthodes ou bien seulement une partie devront être exécutées au sein d'une transaction. L'association d'un comportement transactionnel à un bean est réalisé par la définition du descripteur de déploiement du bean. Ce descripteur de déploiement permet d'attacher un attribut transactionnel à chaque méthode du bean. Les différents attributs disponibles ainsi que la sémantique qui leur est associée sont décrits dans la documentation de programmation de Jonas (p 14).

Dans certains cas, une transaction peut être commencée au niveau d'un client. Dans ce cas, la création, la validation et la destruction de la transaction ne peuvent pas être gérés de manière implicite par Jonas. La séquence suivante montre comment procéder.

UserTransaction utx = (UserTransaction) PortableRemoteObject.narrow(
                                      initialContext.lookup("javax.transaction.UserTransaction"),
                                      UserTransaction.class);
utx.begin();
...
utx.commit();      // or utx.rollback();

2.2 Composants de l'application de commerce électronique

L'application considérée utilise quatre types d'EJB, gérant respectivement des comptes utilisateurs (EJB de type Account), des produits (type Product), des magasins (type ProductStore), et des caddies virtuels (type Cart). Seuls les EJB de type Cart ne sont pas persistants.

EJB de type Account
Un EJB de type Account gère un compte client ou magasin. Les méthodes métier qu'il fournit sont les suivantes.
   - public void deposit(double amount) : dépose une somme d'argent sur le compte.
   - double withdraw(double amount) : retire une somme d'argent du compte.
   - double balance() : retourne la somme d'argent disponible sur le compte.

EJB de type Product
Un EJB de type Product gère un produit d'un magasin. Les méthodes métier qu'il fournit sont les suivantes.
   - int getReference() : retourne la référence (identificateur unique) du produit
   - String getName(): retourne le nom du produit
   - double getPrice() : retourne le prix du produit
   - int getProductStore() : retourne la référence du magasin qui vend ce produit

EJB de type ProductStore
Un EJB de type ProductStore gère un magasin. Les méthodes métier qu'il fournit sont les suivantes.
   - Vector getProducts() : retourne la liste des produits vendus par le magasin
   - String getName() : retourne le nom du produit.
   - int getReference() : retourne la référence (identificateur unique) du magasin
   - int getAccountId() : retourne le compte du magasin

EJB de type Cart
Un EJB de type Cart gère un caddie d'un client connecté à l'application. Les méthodes métier qu'il fournit sont les suivantes.
   - addProduct(ProductPK productPK) : ajoute un produit dans le caddie.
   - Vector getProducts() : retourne la liste des produits contenus dans le caddie.
   - double getTotalPrice() : retourne le prix total des produits contenus dans le caddie.
   - buy(int accountId) : achète les produits contenus dans le caddie.

Dans la version qui vous est fournie de l'application de commerce électronique, seuls les composants Account et ProductStore sont définis.

2.3 Gestion de la persistence des composants

La persistance des composants utilise la base de données relationnelle Instant DB également disponible en OpenSource et intégrée avec Jonas. Dans la version qui vous est fournie, vous aurez à rajouter la gestion de la persistence des composants de type Product.
Pour gérer la persistance d'un type de composant donné, il faut distinguer deux étapes principales. Premièrement, il faut préciser les règles de "mapping" entre le composant et la base relationnelle dans le descripteur de déploiement associé au composant. Deuxièmement, il faut créer ou mettre à jour la base pour que celle-ci contienne les éventuelles copies persistentes des composants considérés.

Les descripteurs de déploiement des composants sont définis au travers de deux types de fichiers XML, nommés XXX.xml et jonas-XXX.xml.  On peut utiliser des fichiers de configuration propres à chaque composant, ou bien des fichiers globaux à l'application. Dans notre cas, nous avons utilisé des fichiers globaux. Le fichier ecom.xml précise pour chaque bean, quel est son type et quels sont les fichiers d'implémentation ou de spécification qui lui sont associés. Si le bean est persistant, ce fichier précise également quels sont les champs qui doivent être sauvegardés dans la base de données. La liste complète des informations contenues est donnée dans la documentation de programmation, p19.

  (fichier ecom.xml)
  ...
  <entity>
  <description>Deployment descriptor for the ProductStore bean </description>
  <ejb-name>ProductStore</ejb-name>
  <home>ecom.ProductStoreHome</home>
  <remote>ecom.ProductStore</remote>
  <ejb-class>ecom.ProductStoreBean</ejb-class>
  <persistence-type>Container</persistence-type>
  <prim-key-class>ecom.ProductStorePK</prim-key-class>
  <reentrant>False</reentrant>
  <cmp-field>
  <field-name>productStoreId</field-name>  //field of ProductStoreBean
  </cmp-field>
  <cmp-field>
  <field-name>name</field-name>
  </cmp-field>
  <cmp-field>
  <field-name>accountId</field-name>
  </cmp-field>
  </entity>


Le fichier jonas-ecom.xml précise les règles de mapping entre les champs du bean et la base de données. Il définit également les clauses WHERE liées aux méthodes finder fournies par le Home du bean considéré. Enfin, les contraintes transactionnelles associées à chaque méthode de chaque bean sont également données.

    (fichier jonas-ecom.xml)
    ...
    <jonas-entity>
        <ejb-name>ProductStore</ejb-name>
        <jndi-name>ProductStoreHome</jndi-name>
        <jdbc-mapping>
          <jndi-name>jdbc_1</jndi-name>
          <jdbc-table-name>productStores</jdbc-table-name> //DB table name
          <cmp-field-jdbc-mapping>
     <field-name>productStoreId</field-name> // field of the bean
     <jdbc-field-name>productStoreId</jdbc-field-name> // attribute in DB
          </cmp-field-jdbc-mapping>
          <cmp-field-jdbc-mapping>
     <field-name>name</field-name>
     <jdbc-field-name>name</jdbc-field-name>
          </cmp-field-jdbc-mapping>
          <cmp-field-jdbc-mapping>
     <field-name>accountId</field-name>
     <jdbc-field-name>accountId</jdbc-field-name>
          </cmp-field-jdbc-mapping>
          <finder-method-jdbc-mapping>
          <jonas-method>
          <method-name>findByNumber</method-name>
          </jonas-method>
          <jdbc-where-clause>where productStoreId = ?</jdbc-where-clause>
          </finder-method-jdbc-mapping>
          <finder-method-jdbc-mapping>
      <jonas-method>
      <method-name>findAllProductStores</method-name> //findAllXXX is defined by Jonas
      </jonas-method>
      <jdbc-where-clause></jdbc-where-clause>
          </finder-method-jdbc-mapping>
        </jdbc-mapping>
  </jonas-entity>

  <assembly-descriptor>
   <container-transaction>
   <method>
   <ejb-name>ProductStore</ejb-name>
   <method-name>*</method-name>
   </method>
   <trans-attribute>Supports</trans-attribute>
   </container-transaction>
  </assembly-descriptor>


La mise à jour de la base est alors réalisée au travers d'un script d'initialisation (fichier ecom.idb) qui contient, entres autres, les directives suivantes.

; delete the old table if necessary
e DROP TABLE productstores;
; create the new table
e CREATE TABLE productstores {
         productStoreId int PRIMARY KEY,
         accountId int,
         name VARCHAR(30));

; put some initial data in the table
p INSERT INTO productstores VALUES(?,?,?);
s 1001,107, 'Galeries Lafayette';
s 1002,108, 'Mark & Spencer';
s 1003, 109, 'Bazar de l'Hotel de Ville';

Pour consulter ou modifier cette base au travers d'un interprête SQL, vous pouvez utiliser la commande java commsql, en précisant ensuite l'URL de la base à laquelle vous souhaitez vous connecter (jdbc:idb:ecom.prp par exemple). Pour plus d'informations sur cette commande et sur IDB, vous pouvez consulter le site Web d'InstantDB.

3. Travail à réaliser

3.1 Installation d'un serveur EJB JONAS et d'une partie de l'application de commerce électronique

Il est conseillé de commencer par créer un répertoire racine (par exemple ECOM) pour le mini-projet ECOM. Vous devez ensuite récupérer et placer dans ce répertoire le fichier d'archive compréssé JONAS2.tar.gz, qui fournit à la fois une installation de Jonas sur PC ainsi qu'une partie de l'application de commerce électronique. Commencez par décomprésser ce fichier. Les répertoires suivants sont crées  :

3.2 Etude d'un cas de base

La distribution  de jonas fournit deux exemples de base d'EJB, sous le répertoire $JONAS_ROOT/examples. Ces exemples permettent de comprendre concrètement quelles sont les règles de programmation des beans Entité (répertoire examples/eb) et Session (répertoire examples/eb) .

Répertoire sb
Ce répertoire contient un bean session appelé Op. Il contient les fichiers suivants :

  Op.java : Op bean Remote interface.
  OpHome.java : Op bean Home remote interface.
  OpBean.java : Op bean implementation.
  ejb-jar.xml : Standard deployment descriptor for bean Op.
  jonas-ejb-jar.xml : JOnAS specific deployment descriptor
  jonas.properties : Property file for the EJB server, local to this directory.
  compile.sh : Script shell to compile the bean
  ClientOp.java : Client for the bean.


Répertoire eb
Ce répertoire contient deux beans persistents partageant la même interface (Account, pour la gestion d'un compte bancaire). L'un de ces beans est géré selon le modèle bean-managed persistence, c'est à dire que la gestion de sa persistence est programmée explicitement par le programmeur. La gestion de la persistence de l'autre bean suit le modèle container-managed persistence, c'est à dire que la gestion de sa persistence est prise en charge de manière implicite par le support d'exécution sous-jacent.
Le répertoire eb contient plus précisément les fichiers suivants :
 

 Account.java : Account bean Remote interface.
 AccountHome.java : Account bean Home interface.
 AccountExplBean.java : Account bean implementation, in bean-managed (explicit) persistence.
 AccountImplBean.java : Account bean implementation, in container-managed (implicit) persistence.
 AccountBeanPK.java : Primary key class of the Account bean.
 ClientAccount.java : Client for this bean
 ejb-jar.xml : standard deployment descriptor, for both beans
 jonas-ejb-jar.xml : JOnAS specific deployment descriptor
 jonas.properties : Property file for the EJB server, local to this directory.
 Account.sql : SQL script for Oracle, used to create the table in the database.
 Account.idb : SQL script for InstantDB, used to create the table in the database.
 Account.prp : Configuration file for InstantDB.
 compile.sh : Script shell to compile the bean
Pour exécuter ces exemples, il faut : Vous pouvez ensuite lancer le serveur de noms, par la commande $JONAS_ROOT/bin/registry 1099 (ou source $JONAS_ROOT/bin/registry 1099 selon votre environnement).
Le serveur EJB est activable depuis le répertoire exemple/src/sb (resp. eb) par la commande EJBServer (ou source EJBServer).
Et enfin le client peut être lancé par la commande java sb.ClientOp (resp. java eb.ClientAccount AccountImplHome).

Note : pour tout probème de connection avec la base, il est conseillé :

L'état courant de la base peut être consulté au travers d'un interprête de commandes SQL lancé par la commande java commsql, en précisant ensuite l'URL de la base (jdbc:idb:Account.prp).

3.3 Travail sur l'application de commerce électronique

Les fichiers qui vous sont fournis gèrent les EJB de type Account et ProductStore. On vous demande dans un premier temps de programmer les EJB de type Product et Cart, ainsi qu'un client très simple. Dans cette phase, on ne tient absolument pas compte des contraintes liées aux transactions. On rappelle que les EJB de type Product sont persistants, et sont donc des beans entités, alors que ceux de type Cart sont volatiles et correspondent à des beans sessions. Après avoir programmé ces beans, vous devez les intégrer avec la partie déjà fournie de l'application. Vous devez ensuite tester votre application.

Pour programmer des beans, vous pouvez utiliser l'utilitaire newbean fourni par Jonas (cf. $JONAS_ROOT/bin/newbean), qui permet de générer des squelettes de tous les fichiers requis pour la définition d'un bean.

Dans un deuxième temps, on s'intéresse à la gestion des aspects transactionnels. Après vous être référré à la documentation fournie par Jonas à cet égard, vous adapterez les descripteurs de déploiement des beans en fonction du comportement transactionnel que vous jugez utile de mettre en place. Vous testerez et validerez le bon fonctionnement des transactions, par exemple en lanceant un achat et en arrêtant brutalement le serveur.

Enfin, si le temps vous le permet, vous pourrez étudier comment réaliser la transformation du bean session Cart en un bean persistant. En particulier, cela pose certains problèmes au niveau de la gestion des références inter-beans. Vous tenterez de mettre en évidence ces problèmes et de proposer une solution.

Rappels
Pour exécuter votre application, il faudra que vous ayez crée la base requise pour faire fonctionner votre exemple, en complétant le fichier de script ecom.idb sous $JONAS_ROOT/ecom/src.

Pour disposer d'une documentation de type javadoc des interfaces et classes standards fournies par la spécification J2EE, consulter l'URL http://java.sun.com/products/ejb/javadoc-2_0-pfd/.

3.4 Expérimentations supplémentaires
Si le temps le permet, il est intéressant de tester l'application de commerce électronique en répartissant les beans qui la composent sur un ensemble de serveurs EJB. La configuration de la répartition de l'application suit des règles précises qui sont décrites dans la documentation de programmation, p 17.

3.5 Une solution
Une fois le mini-projet terminé, vous pourrez consulter une solution au travers de l'archive compréssée ecom.tar.gz. La solution proposée n'utilise pas (volontairement) les handles pour gérer les références inter-beans. De cette manière, les liens qui associent les références distribuées et les clés primaires sont explicités. Autrement dit, la gestion des références qui est proposée dans la solution fournie reflète ce qui est géré de manière transparente dans les handles.