Mise en place d'un site de commerce électronique avec J2EE

Configuration requise

Nous utiliserons ici l'implémentation de référence des EJB fournie par Sunsoft sous le nom de Java 2 Entreprise Edition Developpement Kit. Celle-ci présente les avantages d'être gratuite, complète, de posséder un outil graphique de déploiement facile à utiliser, d'intégrer une base de données gratuite (Cloudscape d'Informix) et surtout de n'avoir AUCUNE EXTENSION PROPRIETAIRE. Ainsi, si vous développez des EJB qui fonctionnent avec cette implémentation, ils ont les meilleurs chances de fonctionner avec un système plus performant.

L'exemple montré ici a été testé sous la configuration suivante :

Objectif

Réaliser un petit site web montrant la liste de produits disponibles sur un site online et permettant à un consommateur de les acheter. A ce sujet nous réaliserons 3 EJB :

Entity Beans

Produit
Représente un produit vendu sur le site. Il sera décrit par les attributs suivants :
Client
Représente un client du site, ses attributs sont :

Session Bean

Nous utiliserons un seul Session Bean associé au passage du client sur le site. Il mettra en relation un Client avec un ou plusieurs Produits. Ce Session Bean peut être vu comme le Caddie que remplit le client au cours de son passage.

Programmation des EntityBeans

En règle générale, on commence toujours par définir la partie métier d'un EJB. Dans le cas des Entity Beans, comme dans celui des Session Beans, l'interface métier (dite Interface Remote par analogie avec JRMP) doit dériver de javax.ejb.EJBObject qui elle même dérive de java.rmi.Remote, l'on en déduit immédiatement que toutes les méthodes de l'interface métier doivent propager java.rmi.RemoteException.

Convention : On donne à l'interface métier le nom simple de l'objet que l'on souhaite modéliser, car c'est à travers cette interface qu'il sera accédé. Donc, si l'on souhaite traiter un Client, l'interface métier s'appellera Client.

Le fragment de code suivant montre l'interface Client :

import javax.ejb.EJBObject;
import java.rmi.RemoteException;


public interface Client extends EJBObject 
{
  public String  getRef() throws RemoteException;
  public String  getNom() throws RemoteException;

  public void    setNom(String nom) throws RemoteException;
  public String  getAdresse() throws RemoteException;
  public void    setAdresse(String adresse) throws RemoteException;
  public String  getEmail() throws RemoteException;
  public void    setEmail(String email) throws RemoteException;
  public double  getSolde() throws RemoteException;
  public void    setSolde(double solde) throws RemoteException;
}

Je sais ce que vous allez dire. Il n'est pas nécessaire (ni interdit !) de déclarer public les méthodes d'une interface : elles le sont par défaut. Cela tient à ma manière de programmer. Lorsque je désire implémenter une interface, je commence par effectuer un copier / coller de ses méthodes dans la classe qui réalise l'implémentation de manière à ne rien oublier. Si je ne mettais pas public dans l'interface, j'aurais tendance à l'oublier dans la classe. Ce sera encore plus vrai lorsque nous allons écrire l'EJB lui même car, comme il n'implémente pas directement l'interface métier mais est relayé par l'EJB Object, le compilateur ne hurlera s'il manque une méthode ou si celle-ci n'est pas déclarée public. Au mieux, c'est l'outil de déploiement qui se plaindra, au pire, vous aurez une exception lors de l'exécution.

Attaquons nous désormais à l'interface home dont nous donnons immédiatement le code, sans oublier de noter au passage qu'elle dérive obligatoire de javax.ejb.EJBHome qui elle même dérive de java.rmi.Remote, toutes les méthodes devront donc propager java.rmi.RemoteException.

import java.util.Collection;
import java.rmi.RemoteException;
import javax.ejb.EJBHome;
import javax.ejb.CreateException;
import javax.ejb.FinderException;


public interface ClientHome extends EJBHome 
{


    public Client create(String    referenceClient,
                         String    nom,
                         String    adresse,
                         String    email,
                         double    soldeInitial) 
      throws RemoteException, CreateException;
    
    public Client findByPrimaryKey(String referenceClient) 
        throws FinderException, RemoteException;
    
    public Collection findByName(String name)

        throws FinderException, RemoteException;


    public Collection findByEmail(String email)
        throws FinderException, RemoteException;    


    public Collection findByAdresse(String adresse)
        throws FinderException, RemoteException;
}

Cette interface est typique d'un Entity Bean car elle contient des méthodes dites finder (de type findXXX) en plus de l'indispensable create. Tout EJB doit obligatoirement posséder au moins une méthode create destinée, malgré son nom un peu trompeur, à initialiser une instance d'EJB qui vient d'être créée (tout vierge) par le conteneur lui même. En plus de java.rmi.RemoteException, la (ou les) méthode de création doit propager javax.ejb.CreateException qui sera émise, par exemple, si la clef primaire est déja utilisée dans la base.

De surcroît, les Entity Beans fournissent également des méthodes finder qui permettent d'extraire de retrouver les Entity Beans dans la base en fonction de critères. Il est très fortement recommandé (ce qui veut dire, obligatoire, je vous le rappelle) de fournir la méthode findByPrimaryKey qui renvoie l'instance d'EJB correspondant à sa clef primaire. Notez bien que le résultat renvoyé correspond à l'interface métier (Client) et non à la classe d'implémentation. L'implémentation de cette méthode est entièrement à la charge du conteneur dans le cas d'un EntityBean CMP.

Les autres finders sont facultatifs et dépendent de l'EJB que vous voulez créer. Ils seront associés à une requête sur le système de stockage persistent sous-jacent, c'est à dire, à une requête SQL dans notre cas. Vous remarquerez qu'ils renvoient le type java.util.Collection, interface générique qui caractérise un type collection abstrait. Ceci est rendu nécessaire par le fait qu'une requête peut renvoyer plusieurs éléments. Bien entendu, dans le cas de la recherche par clef primaire un seul élément peut être renvoyé. L'implémentation des méthodes finder est à la charge du conteneur à une étape près : lors du déploiement, vous serez invité à saisir la requête (SQL dans notre cas) associée à votre demande.

Convention : On donne à l'interface home le nom de l'interface remote suivi du mot Home.

Maintenant que nous avons défini les interfaces, il nous reste à mettre en place l'implémentation. Comme nous travaillons sur un Entity Bean, nous devons lui faire implémenter javax.ejb.EntityBean.

Convention : La classe d'implémentation s'appelle d'ordinaire NomInterfaceRemoteEJB.

Les points les plus importants à retenir sont les suivants :

import java.util.*;
import javax.ejb.*;


public class ClientEJB implements EntityBean 
{


  // Implementation de create en provenance
  // de l'interface Home


  public String ejbCreate(String    referenceClient,
                          String    nom,
                          String    adresse,
                          String    email,
                          double    soldeInitial) 
  {
    ref=referenceClient;
    this.nom=nom;
    this.adresse=adresse;
    this.email=email;
    this.solde=soldeInitial;
    return null; // Crucial !
  }


  // Implementation des methodes metier


  public String  getRef()
  {
    return this.ref;
  }


  public String  getNom()
  {
    return this.nom;
  }


  public void    setNom(String nom)
  {
    this.nom=nom;
  }


  public String  getAdresse()
  {
    return this.adresse;
  }


  public void    setAdresse(String adresse)
  {
    this.adresse=adresse;
  }


  public String  getEmail()
  {
    return this.email;
  }


  public void    setEmail(String email)
  {
    this.email=email;
  }


  public double  getSolde()
  {
    return this.solde;
  }


  public void    setSolde(double solde)
  {
    this.solde=solde;
  }


	  // Methodes callback


  public void setEntityContext(EntityContext context) {}
  public void unsetEntityContext() {}
  public void ejbActivate() {}
  public void ejbPassivate() {}
  public void ejbLoad() {}
  public void ejbStore() {}
  public void ejbRemove() {}
  public void ejbPostCreate(String    referenceClient,
                            String    nom,
                            String    adresse,
                            String    email,
                            double    soldeInitial) 
   { }


  public String ref; // Clef primaire
  public String nom;
  public String adresse;
  public String email;
  public double solde;


}

Sur le même modèle, on batit un Entity Bean pour les produits. Les codes sont donnés ci dessous :

Interface Métier :

import javax.ejb.EJBObject;
import java.rmi.RemoteException;


public interface Produit extends EJBObject 
{
  public String  getRef() throws RemoteException;
  public String  getLabel() throws RemoteException;
  public void    setLabel(String label) throws RemoteException;
  public String  getDescription() throws RemoteException;
  public void    setDescription(String description) throws RemoteException;
  public int     getStock() throws RemoteException;
  public void    setStock(int stock) throws RemoteException;
  public double  getPrix() throws RemoteException;
  public void    setPrix(double prix) throws RemoteException;

}

Interface Home :

import java.util.Collection;
import java.rmi.RemoteException;
import javax.ejb.EJBHome;
import javax.ejb.CreateException;
import javax.ejb.FinderException;


public interface ProduitHome extends EJBHome 
{


    public Produit create(String    referenceProduit,
                          String    label,
                          String    description,
                          int       stock,
                          double    prix) 

      throws RemoteException, CreateException;
    
    public Produit findByPrimaryKey(String referenceProduit) 
        throws FinderException, RemoteException;
    
    public Collection findByLabel(String label)
        throws FinderException, RemoteException;


    public Collection findByDescription(String description)
        throws FinderException, RemoteException;    


    public Collection findByStockRange(int inf, int sup)
        throws FinderException, RemoteException;


    public Collection findByPriceRange(double inf, double sup)
        throws FinderException, RemoteException;
}

Classe d'implémentation :

import java.util.*;
import javax.ejb.*;


public class ProduitEJB implements EntityBean 
{


  // Implementation de create en provenance
  // de l'interface Home


  public String ejbCreate(String    referenceProduit,
                          String    label,
                          String    description,
                          int       stock,
                          double    prix)
  {
    ref=referenceProduit;
    this.label=label;
    this.description=description;
    this.stock=stock;
    this.prix=prix;
    return null; // Crucial !
  }


  // Implementation des methodes metier


  public String  getRef()
  {
    return this.ref;
  }


  public String  getLabel()
  {
    return this.label;
  }


  public void    setLabel(String label)
  {
    this.label=label;
  }


  public String  getDescription()
  {
    return this.description;
  }


  public void    setDescription(String description)
  {
    this.description=description;
  }


  public int  getStock()
  {
    return this.stock;
  }


  public void    setStock(int stock)

  {
    this.stock=stock;
  }


  public double  getPrix()
  {
    return this.prix;
  }


  public void    setPrix(double prix)
  {
    this.prix=prix;
  }


  public void setEntityContext(EntityContext context) {}
  public void unsetEntityContext() {}
  public void ejbActivate() {}
  public void ejbPassivate() {}
  public void ejbLoad() {}
  public void ejbStore() {}
  public void ejbRemove() {}
  public void ejbPostCreate(String    referenceProduit,
                            String    label,
                            String    description,
                            String    stock,
                            double    prix) 
   { }


  public String ref; // Clef primaire
  public String label;
  public String description;
  public int    stock;
  public double prix;


}

Assurez vous que tout se compile normalement en incluant le fichier j2ee.jar (situé dans le sous répertoire lib de la distribution de j2ee) dans votre classpath.

Déploiement des Entity Beans

Pour cela, on utilise le deploytool de J2EE. Préalablement à son utilisation, il faut lancer le serveur

Aspect général du deploy tool

Création d'une application

La première chose consiste à créer une application java entreprise (un fichier .ear). Pour cela, il faut utiliser la commande "File -> New Application". On vous demande alors de spécifier le nom du fichier .ear. Attention ! si vous utilisez le bouton "browser" pour choisir le répertoire, il faudra sans doute que vous spécifiez l'extention .ear manuellement dans la fenêtre de sélection du nom du fichier. Dans un second temps, il vous faudra spécifier un nom (Display Name) à votre application. Celui-ci est utilisé en interne par le deploytool et n'a que peu d'incidence sur l'exécution. En revanche, c'est lui qui apparaît dans le volet "Local Applications" de la fenêtre de l'outil de déploiement. Dans notre cas, nous l'avons appellée "commerce".

Création d'une nouvelle application

Une fois l'application créée, vous voyez son nom apparaître à gauche dans le volet "Local Applications" et dans la barre de titre de la fenêtre, tel que montré sur l'image suivante :

Après création de l'application

Le volet d'exploration dont l'onglet "General" est activé par défaut nous indique que l'application s'appelle "commerce" (Application Display Name), que le fichier est "c:/ejbsun/commerce/commerce.ear" (Application File Name) et que, pour l'instant, il ne contient que 3 fichiers : le manifeste (MANIFEST.MF) ainsi que 2 fichiers de déploiement (application.xml et sun-j2ee-ri.xml). Nous allons le peupler en ajoutant nos entreprise beans.

Ajout des entreprise Beans

Nous allons maintenant ajouter les 2 entity beans que nous venons juste de créer. C'est un processus qui demande une certaine habitude, aussi nous allons le détailler. Commencez par activer la commande "File -> New Entreprise Bean". La première fenêtre qui apparaît n'est qu'indicative, passez immédiatement à la seconde en cliquant sur "Next". La boîte de dialogue suivante est alors à l'écran :

Fenêtre de création d'un EJB

Les options proposées sont les suivantes :

Vue du contenu

Supposons que vous ayez défini des classes accessoires, une exception par exemple, vous devriez aussi la rajouter dans le "Contents". Appuyez sur "Next", vous obtenez :

Boîte de spécification des classes

Cette étape est absolument cruciale car elle vous permet :

  1. De spécifier le rôle de chaque classe parmi celles que vous avez ajouté au "contents"
  2. Choisir le type du Bean : Session Stateless, Session Statefull ou Entity
  3. Choisir le nom du Bean tel qu'il sera montré dans l'explorateur de l'outil de déploiement.

Une fois l'opération terminée, la fenêtre devra ressembler à :

Déploiement

La fenêtre suivante permet de gérer la persistence des Entity Beans. Lorsque vous la découvrez, voici à quoi elle ressemble :

déploiement

Dans cette fenêtre vous allez :

Pour commencer, sélectionnez "Container-Managed Persistency" , la liste des attributs apparaît alors dans le panel central. Sélectionnez alors, en cochant la case, ceux que vous désirez sauver persistents, comme le montre la figure suivante :

Déploiement

Dans notre cas, nous avons donc sélectionné tous les attributs. Autre aspect, plus important encore, la gestion de la clef primaire. La clef primaire du Client est sa référence, codée dans l'attribut ref, elle est de type String, c'est ce qui a été indiqué dans la partie base de la fenêtre.

Les prochaines étapes sont pour des utilisations avancées à l'exception de la gestion des transactions vers laquelle je me dirige à présent. Les méthodes sont recensées à gauche, en face de chacune on trouve son comportement vis à vis des transactions. Initialement, toutes les méthodes sont en mode "NotSupported". Si l'on considère les méthodes métier de notre Bean, passons celles de type "get" en "Supports" et celles de type "set" en "Required" comme le montre l'exemple suivant :

Déploiement

L'écran suivant, qui est aussi le dernier, vous montre alors le fichier ejb-jar.xml qui a été produit par l'interface graphique. Certes nous aurions pu le faire facilement à la main, mais l'interface graphique nous simplifie tout de même bien la vie.

Déploiement

Il est toujours possible de revenir en arrière pour modifier ce que l'on a créé, par exemple un clic sur le Bean Client nous donne :

déploiement

où chaque onglet correspond à une page du processus de création. A ce sujet, cliquez donc sur l'onglet Entity. Car à présent, nous allons faire l'opération qui n'est pas du tout normalisée : le mappage sur la base de données. Pour ce faire, activez le bouton "Deployment Settings" de l'onget Entity. Il fait apparaître une nouvelle boîte de dialogue assez complexe.

Déploiement

Dans sa partie haute, on vous demande des renseignements sur votre connection à la base de données. Dans notre cas, il faut spécifier "jdbc/Cloudscape" ; les "User Name" et "Password" devant être laissés blancs comme le montre la figure suivante. Une fois cette partie renseignée, vous pouvez cliquer sur le bouton "Generate SQL now" qui va créer du code SQL pour toute la partie "persistence" de l'application, à savoir le stockage en base, la récupération en base (en particulier le "findByPrimaryKey") etc...

Déploiement

Si tout se passe bien, vous obtenez le message de confirmation suivant :

Déploiement

... d'ordinaire suivi de celui-ci :

Déploiement

... qui indique que, certes, le code des méthodes finder est à la charge du processus de déploiement mais que vous devez tout de même fournir la requête SQL permettant de retrouver les enregistrements. Le format de ces requètes est un peu particulier et doit suivre une symbolique précise :

Par exemple, pour indiquer que la méthode findByStockRange recherche les articles dont la quantité stockée est comprise entre les 2 paramètres, vous utilisez :

SELECT "ref" FROM "ProduitEJBTable" WHERE "stock" BETWEEN ?1 AND ?2

où vous n'avez qu'à créer la partie en italique, le reste a déja été mis pour vous par le déployeur. Je voudrais revenir sur la partie intermédiaire qui gère la création et la destruction des tables. Si vous n'activez pas le "Create Table on Deploy" il vous faudra créer vous même la table et la nommer "ClasseDImplementationTable".

Déploiement

Une fois ce travail réalisé, il ne reste qu'à déployer à l'aide du menu Tools / Deploy Application. N'oubliez pas de cocher "Return client jar" sur la première page. La deuxième permet quand à elle de spécifier des noms JNDI permettant d'accéder aux interface home. Notez bien les noms que vous saisissez (ici "MonProduit" et "MonClient") car sans eux vous perdez accès à vos classes !

Déploiement

Si le processus se passe bien, vous devriez obtenir un écran comme celui la :

Déploiement