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 :
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 :
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.
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.
Pour cela, on utilise le deploytool de J2EE. Préalablement à son utilisation, il faut lancer le serveur

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".

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 :

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 :

Les options proposées sont les suivantes :


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 :

Cette étape est absolument cruciale car elle vous permet :
Une fois l'opération terminée, la fenêtre devra ressembler à :

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

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 :

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 :

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.

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 :

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.

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...

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

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

... 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".

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 !

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