Abstract
Pour profiter pleinement de la lecture de cet article vous devez posséder :
Une JVM (JDK) 5.0 minimum (http://java.sun.com)
Ant 1.6.x (http://ant.apache.org)
Table of Contents
De nos jours les entreprises cherchent à centraliser leurs systèmes d'informations mais aussi à les rendre intéropérables. Les serveurs d'application répondent à ce besoin. La technologie J2EE est la plus répandue dans ce secteur. Interopérable, standardisée, supportée par la majorité des éditeurs, du marché, elle est particulièrement adaptée aux applications transactionnelles robustes et distribuées.
Le principal atout des serveurs J2EE (Java 2 Enterprise Edition) est leur architecture multiplate-forme liée à l'utilisation du langage Java. La plate-forme J2EE offre aussi la portabilité des applications d'un serveur à l'autre. De plus, l'ensemble du cycle de vie d'une application est couvert : développement, assemblage, déploiement et administration.
Les EJB font partie de la spécification J2EE, qui se nomme désormais Java EE. La version 5.0 devrait être finalisée au mois de mai prochain à l'occasion de la conférence JavaOne 2006. L'inconvénient majeur de la version actuelle, version 1.4, concerne la partie persistance et métier avec les EJB 2.1. Les développeurs trouvent cela complexe. Un exemple est la création d'un EJB. Pour créer un objet métier (EJB) rapidement, il faut écrire des fichiers XML (un fichier standard et un fichier spécifique pour chaque serveur d'application) ainsi que plusieurs classes et interfaces Java. Des outils annexes sont apparus afin de simplifier la vie des développeurs J2EE tels que XDoclet pour écrire plus rapidement des EJBs ou Hibernate pour simplifier la partie persistance. EJB 3.0 est donc une évolution majeure ayant pour but de simplifier le développement. Cela est possible avec l'adoption de métadonnées : Le code est directement annoté sans passer par un fichier externe (XML). Par exemple, pour définir un EJB, la déclaration de la classe sera préfixée avec l'annotation @Stateless si c'est un EJB sans état. Les descripteurs de déploiement (fichiers XML) deviennent obsolètes même s'ils sont toujours supportés. Une autre nouveauté des EJB3 concerne la partie persistance avec un modèle proche de celui utilisé par Hibernate. Celui-ci consiste à utiliser des objets standards (POJO Plain Old Java Object).
Dans cet article, nous allons décrire les étapes permettant la construction d'une application métier écrite avec les EJB3. Pour ce faire, le conteneur léger EasyBeans sera utilisé. Ce projet est hébergé par le consortium ObjectWeb et servira de conteneur EJB3 au serveur d'application JOnAS, serveur d'application certifié J2EE lui même hébergé par ObjectWeb.
Dans le cadre de l'exemple, la version standalone/source du produit EasyBeans sera utilisée.
Les sources d'EasyBeans sont à télécharger sur le site web du projet : http://www.easybeans.org/download.html
Il suffit de le décompresser le fichier téléchargé. Par exemple, si le fichier source se nomme ow_easybeans_src.tgz, il faut xecuter la commande suivante sous Linux :
jar xf ow_easybeans_src.tgz
Il existe de nombreux outils permettant de décompresser ce type d'archives.
Les exemples se trouvent dans le répertoire examples.
Les sources d'EasyBeans se trouvent dans le répertoire src. Les bibliothèques utilisées sont dans le répertoire lib.
Afin de vérifier que l'installation et la configuration d'EasyBeans est correcte, les exemples livrés peuvent être testés. Il existe une tache ant afin de compiler les exemples puis de les lancer un par un, chaque exemple ayant son propre fichier build.xml, fichier utilisé par Ant.
Unde documentation complémentaire se trouve sur le site d'EasyBeans : http://www.easybeans.org/doc/userguide/en/integrated/userguide.html
L'application qui va être réalisée à titre d'exemple est une bibliothèque. On accède à une interface permettant de lister les livres écrits par des différents auteurs. Les deux élements que sont le livre et l'auteur sont représentés par 2 EJB entités. Ces deux beans ont une relation simpliste : un livre est associé à un seul auteur et pour un auteur donné, il peut y avoir plusieurs livres.
Une bean entité est une simple classe consédérée comme POJO. Cela signifie qu'il faut que la classe comporte un constructeur par défaut. De plus, pour chaque attribut de la classe, un getter et un setter sont positionnés.
Un Bean entité aura comme annotation @Entity.
La clef primaire est définie par une annotation @Id. Dans le cas de l'exemple, cette clef doit être auto-générée. Au final la méthode getId sera la suivante :
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long getId() {
return this.id;
}Le bean Auteur est lié au bean Livre. Un auteur peut avoir plusieurs livres (relation OneToMany)
La relation est définie également via une annotation :
@OneToMany(mappedBy = "auteur", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public Collection<Livre> getLivres() {
return livres;
}Cette relation est de type One-To-Many. Le lien sera fait avec le bean entité Livre sur l'attribut auteur (propriétaire de la relation)
Le type de fetch est EAGER. Un autre choix peut être Lazy. Dans le cas de l'exemple, les beans entités seront accédés par un client dans une autre JVM (mode détaché). Il ne faut donc pas utiliser le modèle Lazy. Il faut aussi que le bean implémente la classe Serializable car l'accès est fait depuis une autre JVM.
Enfin, le mode cascade est positionné à ALL. Lorsque le bean Auteur sera rendu persistant, les livres attachés à l'auteur seront également persistés.
package com.programmez.ejb3;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
/**
* Definition d'une classe representant l'auteur d'un livre.
* @author Florent Benoit
*/
@Entity
@NamedQuery(name = "tousLesAuteurs", query = "select o FROM Auteur o")
public class Auteur implements Serializable {
/**
* Attribut pour la serialisation Java.
*/
private static final long serialVersionUID = 8279536554568120695L;
/**
* Clef primaire (generee automatiquement).
*/
private long id;
/**
* Nom de l'auteur.
*/
private String nom = null;
/**
* Liste de livres ecrits par l'auteur.
*/
private Collection<Livre> livres;
/**
* Constructeur par defaut.
*/
public Auteur() {
livres = new ArrayList<Livre>();
}
/**
* Constructeur utilise pour initialiser cet entity bean.
* @param nom - le nom de l'auteur.
*/
public Auteur(final String nom) {
this();
setNom(nom);
}
/**
* Non utilisation du mode Lazy.
* @return livres ecrits par cet auteur.
*/
@OneToMany(mappedBy = "auteur", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public Collection<Livre> getLivres() {
return livres;
}
/**
* Ajout d'un livre avec son titre.
* @param titre - le titre du livre.
*/
public void ajoutLivre(final String titre) {
Livre livre = new Livre();
livre.setTitre(titre);
livre.setAuteur(this);
livres.add(livre);
}
/**
* Definition des livres ecrits par cet auteur.
* @param livres - les livres ecrits.
*/
public void setLivres(final Collection<Livre> livres) {
this.livres = livres;
}
/**
* @return nom de l'auteur.
*/
public String getNom() {
return nom;
}
/**
* Definition du nom de l'auteur.
* @param nom - le nom de l'auteur.
*/
public void setNom(final String nom) {
this.nom = nom;
}
/**
* @return un identifiant (auto-incremental)
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long getId() {
return this.id;
}
/**
* Definition du nouvel identifiant.
* @param id - nouvel identifiant
*/
public void setId(final long id) {
this.id = id;
}
}Une requête EJB-QL est définie sur la classe du bean. Celle-ci sera utilisée pour obtenir l'ensemble des auteurs.
Ce bean est également un bean entité. Une relation existe par rapport au bean Auteur, un livre étant lié à un seul auteur : Relation Many-To-One. Cela est défini par l'annotation ManyToOne.
L'annotation JoinColumn spécifie ici la colonne de la clef primaire à utiliser lors de l'association entre deux beans entités.
/**
* @return auteur du livre.
*/
@ManyToOne
@JoinColumn(name = "Auteur_id")
public Auteur getAuteur() {
return auteur;
}Voici la classe complète :
package com.programmez.ejb3;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQuery;
/**
* Definition d'une classe representant un Livre. Un livre comporte un auteur et
* un titre.
* @author Florent Benoit
*/
@Entity
@NamedQuery(name = "tousLesLivres", query = "select o FROM Livre o")
public class Livre implements Serializable {
/**
* Attribut pour la serialisation Java.
*/
private static final long serialVersionUID = 7870634228111717036L;
/**
* Clef primaire (generee automatiquement).
*/
private long id;
/**
* Auteur du livre.
*/
private Auteur auteur;
/**
* Titre du livre.
*/
private String titre;
/**
* Constructeur par defaut.
*/
public Livre() {
}
/**
* Constructeur utilise pour initialiser cet entity bean.
* @param auteur - l'auteur du livre.
* @param titre - le titre du livre.
*/
public Livre(final String titre, final Auteur auteur) {
setTitre(titre);
setAuteur(auteur);
}
/**
* @return auteur du livre.
*/
@ManyToOne
@JoinColumn(name = "Auteur_id")
public Auteur getAuteur() {
return auteur;
}
/**
* Definition de l'auteur du livre.
* @param auteur - l'auteur du livre.
*/
public void setAuteur(final Auteur auteur) {
this.auteur = auteur;
}
/**
* @return titre du livre.
*/
public String getTitre() {
return titre;
}
/**
* Definition du titre du livre.
* @param titre - le titre du livre.
*/
public void setTitre(final String titre) {
this.titre = titre;
}
/**
* @return un identifiant (auto-incremental)
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long getId() {
return this.id;
}
/**
* Definition du nouvel identifiant.
* @param id - nouvel identifiant
*/
public void setId(final long id) {
this.id = id;
}
}Il est requis d'avoir un fichier de persistance, META-INF/persistence.xml, permettant de décrire des informations utiles. Par exemple le datasource à utiliser lors de la persistance en base de données.
<persistence 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="entity">
<provider></provider>
<jta-data-source>jdbc_1</jta-data-source>
<properties>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
</properties>
</persistence-unit>
</persistence>L'accès aux beans entités est réalisé à travers un entity manager, gérant les beans entités. Cet entity manager sera accédé par un bean session sans état.
Ce bean sera utilisé par un client lourd. Ce bean possède 3 méthodes principales définies par une interface :
package com.programmez.ejb3;
import java.util.List;
import javax.ejb.Remote;
/**
* Interface remote du bean jouant le role du bean "facade".
* @author Florent Benoit
*/
@Remote
public interface SessionFacadeRemote {
/**
* Creation d'auteurs et de livres.
*/
void init();
/**
* @return la liste des auteurs.
*/
List<Auteur> listeDesAuteurs();
/**
* @return la liste des livres
*/
List<Livre> listeDesLivres();
}Cette interface est utilisée pour accéder au bean depuis un client distant. Elle est donc annotée avec l'annotation @Remote
Une méthode est nécessaire pour initialiser la base de données avec des livres et auteurs.
Enfin, deux méthodes permettent de récuperer une liste d'auteurs ou de livres. Les objets récupérés sont des objets beans entités.
L'implémentation de cette interface est faite par le bean SessionFacade.
Comme évoqué plus haut, le bean stateless va utiliser un objet entity Manager afin de manipuler les entités. Pour cela, il y a utilisation de la nouvelle fonctionnalité des EJB 3.0 : l'injection de resource ou "dependency injection".
/**
* Entity manager utilise par ce bean.
*/
@PersistenceContext
private EntityManager entityManager = null;L'annotation @PersistenceContext va être analysée par le conteneur et la variable entityManager va être initialisée. Ensuite, il suffit d'appeler des méthodes sur cette variable, elle n'a plus la valeur nulle. Ce n'est plus au developpeur de rechercher la variable, il demande au conteneur.
Afin d'ajouter un auteur et ses livres dans une base, il suffit de créer les beans et de les persister au travers de l'entity manager.
Auteur balzac = new Auteur("Honore de Balzac");
Livre pereGloriot = new Livre("Le Pere Goriot", balzac);
balzac.getLivres().add(pereGloriot);
Livre lesChouans = new Livre("Les Chouans", balzac);
balzac.getLivres().add(lesChouans);
// Enregistrement (seulement l'auteur car il y a l'attribut Cascade positionné à ALL).
entityManager.persist(balzac);Un bean entité est initialisé comme une classe simple avec l'instruction new(). Cela change des précédentes versions des EJBs (2.1).
On peut ensuite soit ajouter les livres par une méthode du bean auteur, soit créé un livre de manière independante. Il faut également maintenir la cohérence de la relation, c'est à dire ajouter le libre à l'auteur et inversement.
Ensuite, le bean créé doit être rendu persistant dans la base de données. Cela est réalisé avec l'opération persist sur l'entitymanager en donnant en paramètre le bean entité balzac (dans l'exemple).
Pour obtenir la liste des auteurs, il y a interrogation de l'entitymanager. La requete utilisée dans le cadre de l'exemple est celle définie dans la classe entité avec l'annotation NamedQuery.
Petit rappel, le bean contenait :
@Entity
@NamedQuery(name = "tousLesAuteurs", query = "select o FROM Auteur o")
public class Auteur implements Serializable {Le bean stateless va donc utiliser le nom de la requête comme argument.
/**
* @return la liste des auteurs
*/
@SuppressWarnings("unchecked")
public List<Auteur> listeDesAuteurs() {
Query auteurs = entityManager.createNamedQuery("tousLesAuteurs");
return auteurs.getResultList();
}Cette méthode renvoie la liste des auteurs qui sont stockés dans la base de données.
Code complet de la classe :
package com.programmez.ejb3;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
/**
* Bean sans etat assurant le dialogue cote serveur avec les beans entites.
* @author Florent Benoit
*/
@Stateless
public class SessionFacade implements SessionFacadeRemote {
/**
* Entity manager utilise par ce bean.
*/
@PersistenceContext
private EntityManager entityManager = null;
/**
* Creation d'auteurs et de livres.
*/
public void init() {
// livres de balzac
Auteur balzac = new Auteur("Honore de Balzac");
Livre pereGloriot = new Livre("Le Pere Goriot", balzac);
balzac.getLivres().add(pereGloriot);
Livre lesChouans = new Livre("Les Chouans", balzac);
balzac.getLivres().add(lesChouans);
// Enregistrement (seulement l'auteur car il y a l'attribut Cascade positionn? ? ALL).
entityManager.persist(balzac);
// Livres victor hugo
Auteur hugo = new Auteur("Victor Hugo");
hugo.ajoutLivre("Les Miserables");
hugo.ajoutLivre("Notre-Dame de Paris");
// Enregistrement
entityManager.persist(hugo);
// should be done by the server
entityManager.joinTransaction();
}
/**
* @return la liste des auteurs
*/
@SuppressWarnings("unchecked")
public List<Auteur> listeDesAuteurs() {
Query auteurs = entityManager.createNamedQuery("tousLesAuteurs");
return auteurs.getResultList();
}
/**
* @return la liste des livres
*/
@SuppressWarnings("unchecked")
public List<Livre> listeDesLivres() {
return entityManager.createNamedQuery("tousLesLivres").getResultList();
}
}Une différence entre les EJB2 et 3 est le fait que l'on peut directement récuperer l'interface business du bean. Il n'y a plus d'opération create(); Une fois le bean récupéré, les méthodes business peuvent être appellées directement :
Context initialContext = new InitialContext(); SessionFacadeRemote facadeBean = (SessionFacadeRemote) initialContext.lookup(jndiName); // Initialisation de la base facadeBean.init();
Enfin, voici le code permettant de récuperer la liste des auteurs et de les traiter :
// Recuperation de la liste des auteurs
List<Auteur> auteurs = facadeBean.listeDesAuteurs();
// Liste par auteur des livres ecrits par ceux-ci.
if (auteurs != null) {
for (Auteur auteur : auteurs) {
System.out.println("Liste de livres pour l'auteur dont le nom est '" + auteur.getNom() + "' :");
Collection<Livre> livres = auteur.getLivres();
if (livres == null) {
System.out.println("- Aucun livre.");
} else {
for (Livre livre : livres) {
System.out.println("- Livre ayant pour titre '" + livre.getTitre() + "'.");
}
}
}
} else {
System.out.println("Aucun auteur.");
}Il y a récupération d'une liste de bean entités. Ensuite, afin de récupérer les livres associés, la méthode getLivres() est appelée et retourne dans ce cas une liste de beans entités Livres. Code complet :
package com.programmez.ejb3;
import java.util.Collection;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
/**
* Simple client of the stateless.
* @author Florent Benoit
*/
public final class Client {
/**
* Classe utilitaire, aucun constructeur public.
*/
private Client() {
}
/**
* Point d'entree du programme.
* @param args les arguments (non requis)
* @throws Exception s'il y a une exception.
*/
public static void main(final String[] args) throws Exception {
// Propri?t?s pour le registry
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.objectweb.carol.jndi.spi.MultiOrbInitialContextFactory");
Context initialContext = new InitialContext();
SessionFacadeRemote facadeBean = (SessionFacadeRemote) initialContext.lookup("com.programmez.ejb3.SessionFacade" + "_"
+ SessionFacadeRemote.class.getName() + "@Remote");
// Initialisation de la base
facadeBean.init();
// Recuperation de la liste des auteurs
List<Auteur> auteurs = facadeBean.listeDesAuteurs();
// Liste par auteur des livres ecrits par ceux-ci.
if (auteurs != null) {
for (Auteur auteur : auteurs) {
System.out.println("Liste de livres pour l'auteur dont le nom est '" + auteur.getNom() + "' :");
Collection<Livre> livres = auteur.getLivres();
if (livres == null) {
System.out.println("- Aucun livre.");
} else {
for (Livre livre : livres) {
System.out.println("- Livre ayant pour titre '" + livre.getTitre() + "'.");
}
}
}
} else {
System.out.println("Aucun auteur.");
}
// Recuperation de la liste des livres
System.out.println("");
System.out.println("Liste des livres :");
List<Livre> livres = facadeBean.listeDesLivres();
if (livres != null) {
for (Livre livre : livres) {
System.out.println(" - Livre avec titre '" + livre.getTitre() + "' ayant pour auteur '"
+ livre.getAuteur().getNom() + "'.");
}
}
}
}