Niveau : | Supérieur |
Les EJB (Entreprise Java Bean) sont un des éléments très importants de la plate-forme Java EE pour le développement d'applications distribuées.
La plate-forme Java EE propose de mettre en oeuvre les couches métiers et persistance avec les EJB. Particulièrement intéressants dans des environnements fortement distribués, jusqu'à la version 3, leur mise en oeuvre est assez lourde sans l'utilisation d'outils tels que certains IDE ou XDoclet.
La version 3 des EJB vise donc à simplifier le développement et la mise en oeuvre des EJB qui sont fréquemment jugés trop complexes et trop lourds à mettre en oeuvre.
Cette nouvelle version majeure des EJB propose une simplification de leur développement tout en conservant une compatibilité avec sa précédente version. Elle apporte de très nombreuses fonctionnalités dans le but de simplifier la mise en oeuvre des EJB.
Cette simplification est rendue possible notamment par :
Tous ces éléments délèguent une partie du travail du développeur au conteneur d'EJB.
Ce chapitre contient plusieurs sections :
EJB 1.1 publié en décembre 1999, intégré dans J2EE 1.2 :
EJB 2.0 publié en septembre 2001, intégré à J2EE 1.3 :
EJB 2.1 publié en novembre 2003, intégré à J2EE 1.4 :
EJB 3.0, intégré à Java EE 5 :
EJB 3.1, intégré à Java EE 6
Dans les versions antérieures à la version 3.0 des EJB, le développeur était contraint de créer de nombreuses entités pour respecter l'API EJB (par exemple, l'implémentation d'interfaces engendrant la création de plusieurs méthodes ou le descripteur de déploiement), ce qui rendait l'écriture relativement lourde même avec l'assistance de certains IDE ou outils (XDoclet notamment). Dans la version 3.0, ceci est remplacé par l'utilisation d'annotations.
La mise en oeuvre de l'interface EJBHome n'est plus requise : un EJB de type session est maintenant une simple classe, qui peut implémenter une interface métier.
La seule annotation obligatoire dans un EJB est celle qui précise le type d'EJB (@javax.ejb.Stateless, @javax.ejb.Stateful ou @javax.ejb.MessageDriven).
Les annotations possèdent des valeurs par défaut qui répondent à une majorité de cas typiques d'utilisations. L'utilisation de ces annotations n'est alors requise que si les valeurs par défaut ne répondent pas au besoin. Ceci permet de réduire la quantité de code à écrire.
L'utilisation des annotations et de valeurs par défaut pour la plupart de ces dernières rend optionnelle la nécessité de créer un descripteur de déploiement sauf pour des besoins très particuliers.
Le conteneur obtient des informations sur la façon de mettre en oeuvre un EJB par trois moyens :
L'ordre d'utilisation par le conteneur est : le descripteur de déploiement, les annotations, les valeurs par défaut.
L'utilisation des annotations est plus simple à mettre en oeuvre mais le descripteur de déploiement permet de centraliser les informations.
La nouvelle API Java Persistence remplace la persistance assurée par le conteneur : cette API assure la persistance des données grâce à un mapping O/R reposant sur des POJO.
Le conteneur a la possibilité d'injecter des dépendances d'objets dont il assure la gestion.
Les intercepteurs permettent d'offrir des fonctionnalités proches de celles proposées par l'AOP : ceci permet de définir des traitements lors de l'invocation de méthodes des EJB ou d'invoquer des méthodes particulières liées au cycle de vie de l'EJB.
Les classes et les interfaces des EJB 3.0 sont de simples POJO ou POJI : ceci simplifie le développement des EJB. Par exemple, l'interface Home n'est plus à déclarer.
Il est toujours possible d'implémenter les interfaces SessionBean, EntityBean et MessageDrivenBean mais le plus simple est d'utiliser les annotations définies : @Stateless, @Stateful, @Entity ou @MessageDriven
Exemple : |
@Stateless
public class HelloWorldBean {
public String saluer(String nom) {
return "Bonjour "+nom;
}
}
Il est possible de définir une interface métier pour l'EJB ou de laisser générer cette interface lors du déploiement.
Dans le premier cas, il n'est plus nécessaire qu'elle implémente l'interface EJBObject ou EJBLocalObject mais il faut simplement utiliser les annotations définies : @Remote ou @Local.
Exemple : |
@Remote
@Stateless
public class HelloWorldBean {
public String saluer(String nom)
{
return "Bonjour "+nom;
}
}
Dans le second cas, ces annotations doivent être utilisées dans la classe d'implémentation pour permettre de déterminer l'interface générée.
Il est possible de définir une interface locale et/ou distante pour un même EJB.
Il n'est pas recommandé de laisser les interfaces être générées par le conteneur pour plusieurs raisons :
La spécification 3.0 des EJB fait un usage intensif des annotations. Celles-ci sont issues de la JSR 175 et intégrées dans Java SE 5.0 qui constitue la base de Java EE 5.
Les annotations sont des attributs ou métadonnées à l'image de celles proposées par XDoclet.
Avec les EJB 3.0, les annotations sont utilisées pour générer des entités et remplacer tout ou partie du descripteur de déploiement.
De nombreuses annotations permettent de simplifier le développement des EJB.
La nature de l'EJB est précisée par une des annotations @Stateless, @Stateful, @Entity et @MessageDriven selon le type d'EJB à définir.
Le type d'accès est précisé par deux annotations
Par défaut, l'interface d'appel est locale si aucune annotation n'est indiquée.
Dans le cas d'un accès distant, il est inutile que chaque méthode précise qu'elle peut lever une exception de type RemoteException mais elles peuvent déclarer la levée d'exceptions métiers.
Jusqu'à la version 2.1 des EJB, il était obligatoire d'implémenter plusieurs méthodes relatives à la gestion du cycle de vie de l'EJB notamment ejbActivate, ejbLoad, ejbPassivate, ejbRemove, ... pour chaque EJB même si ces méthodes ne contenaient aucun traitement.
Avec les EJB 3.0, l'implémentation de ces méthodes est remplacée par l'utilisation facultative d'annotations sur les méthodes concernées. La signature de ces méthodes doit être de la forme public void nomMethode()
Par exemple, pour que le conteneur exécute automatiquement une méthode avant de retirer l'instance du bean, il faut annoter la méthode avec l'annotation @Remove.
Plusieurs annotations permettent ainsi de définir des méthodes qui interviendront dans le cycle de vie de l'EJB.
Annotation |
Rôle |
@PostConstruct |
la méthode est invoquée après que l'instance est créée et que les dépendances sont injectées |
@PostActivate |
la méthode est invoquée après que l'instance de l'EJB est désérialisée du disque. C'est l'équivalent de la méthode ejbActivate() des EJB 2.x |
@Remove |
la méthode est invoquée avant que l'EJB ne soit retiré du conteneur |
@PreDestroy |
la méthode est invoquée avant que l'instance de l'EJB ne soit supprimée |
@PrePassivate |
la méthode est invoquée avant que de l'instance de l'EJB ne soit sérialisée sur disque. C'est l'équivalent de la méthode ejbPassivate() des EJB 2.x |
L'utilisation facultative de ces annotations remplace la définition obligatoire des méthodes de gestion du cycle de vie utilisées jusqu'à la version 2.1 des EJB.
Le descripteur de déploiement n'est plus obligatoire puisqu'il peut être remplacé par l'utilisation d'annotations dédiées directement dans les classes des EJB.
Chaque attribut de déploiement possède une valeur par défaut qu'il ne faut définir que si cette valeur ne répond pas au besoin.
Plusieurs annotations sont définies par les spécifications des EJB pour permettre de déclarer le type de bean, le type de l'interface, des références vers des ressources qui seront injectées, la gestion des transactions, la gestion de la sécurité, ...
Chaque vendeur peut définir en plus ses propres annotations dans l'implémentation de son serveur d'applications. Leur utilisation n'est cependant pas recommandée car elle rend l'application dépendante du serveur d'applications utilisé.
L'utilisation des annotations va simplifier le développement des EJB mais la gestion de la configuration pourra devenir plus complexe puisqu'elle n'est plus centralisée.
L'EJB déclare les ressources dont il a besoin à l'aide d'annotations. Le conteneur va injecter ces ressources lorsqu'il va instancier l'EJB donc avant l'appel aux méthodes liées au cycle de vie du bean ou aux méthodes métiers. Ceci impose que l'injection de ressources se fasse sur des objets gérés par le conteneur.
Ces ressources peuvent être de diverses natures : référence vers un autre EJB, contexte de sécurité, contexte de persistance, contexte de transaction, ...
Plusieurs annotations sont définies pour mettre en oeuvre l'injection de dépendances :
L'utilisation de l'injection de dépendances remplace l'utilisation implicite de JNDI.
L'injection peut aussi être définie dans le descripteur de déploiement.
|
La suite de cette section sera développée dans une version future de ce document
|
Les intercepteurs sont une fonctionnalité avancée similaire à celle proposée par l'AOP : ils permettent d'intercepter l'invocation de méthodes pour exécuter des traitements.
Ils sont définis grâce à des annotations dédiées notamment @Interceptors et @AroundInvoke ou dans le descripteur de déploiement.
Leur utilisation peut être variée : traces, logs, gestion de la sécurité, ...
Le développement d'EJB n'a jamais été facile et est même devenu plus complexe au fur et à mesure des nouvelles spécifications.
Avant la version 3.0 des EJB, les EJB étaient relativement complexes et lourds à mettre en oeuvre :
La version 3.0 des spécifications des EJB apporte une solution de simplification à tous les points précédemment cités.
Les principales différences entre les EJB 2.x et EJB 3.0 sont donc :
Il n'existe pas de règles imposées mais il est important de définir des conventions de nommage pour les différentes entités qui sont utilisées lors de la mise en oeuvre des EJB.
Exemple :
Nom du bean : CalculEJB
Nom de la classe métier : CalculBean
Interface locale : CalculLocal
Interface distante : CalculRemote
Les EJB Session sont généralement utilisés comme façade pour proposer des fonctionnalités qui peuvent faire appel à d'autres composants ou entités tels que des EJB session, des EJB Entity, des POJO, ...
La version 3.0 des EJB rend inutile l'implémentation d'une interface spécifique à l'API EJB. Mais même si cela n'est pas obligatoire, il est fortement recommandé (dans la mesure du possible) de définir une interface dédiée à l'EJB qui va notamment préciser son mode d'accès et les méthodes utilisables.
Cette interface est alors une simple POJI.
Un EJB peut être invoqué :
L'interface distante définit les méthodes qui peuvent être appelées par un client en dehors de la JVM du conteneur. L'interface ou le bean doit être marqué avec l'annotation @Remote implémentée dans la classe javax.ejb.Remote
L'interface locale définit les méthodes qui peuvent être appelées par un autre EJB s'exécutant dans la même JVM que le conteneur. Les performances sont ainsi accrues car les mécanismes de protocoles d'appels distants ne sont pas utilisés (sérialisation/désérialisation, RMI, ...).
L'utilisation de l'interface Local pour des appels à l'EJB dans un même JVM est fortement recommandée car cela améliore les performances de façon importante. L'interface Remote met en oeuvre des mécanismes de communication utilisant la sérialisation, ce qui dégrade les performances notamment de façon inutile si l'appel à l'EJB se fait dans une même JVM.
Un client ne dialogue jamais en direct avec une instance de l'EJB : le client utilise toujours l'interface pour accéder au bean grâce à un proxy généré par le conteneur. Même un client local utilise un proxy particulier dépourvu des accès réseau. Ce proxy permet au conteneur d'assurer certaines fonctionnalités comme la sécurité et les transactions.
Les beans de type stateless sont les plus simples et les plus véloces car le conteneur gère un pool d'instances qui sont utilisées au besoin, ce qui évite des opérations d'instanciation et de destruction à chaque utilisation. Ceci permet une meilleure montée en charge de l'application.
L'annotation @javax.ejb.Stateless permet de préciser qu'un EJB session est de type stateless. Elle s'utilise sur une classe qui encapsule un EJB et possède plusieurs attributs :
Attribut |
Rôle |
String name |
Nom de l'EJB (optionnel) |
String mappedName |
Nom sous lequel l'EJB sera mappé. La valeur par défaut est le nom non qualifié de la classe (optionnel) |
String description |
Description de l'EJB (optionnel) |
Il faut définir l'interface de l'EJB avec l'annotation précisant le mode d'accès.
L'annotation @javax.ejb.Remote permet de préciser que l'EJB pourra être accédé par des clients distants. Elle s'utilise sur une classe qui encapsule un EJB ou l'interface qui décrit les fonctionnalités de l'EJB utilisables à distance. Cette annotation ne peut être utilisée que pour des EJB sessions.
Elle possède un seul attribut :
Attribut |
Rôle |
Class[] value |
Préciser la liste des interfaces distantes de l'EJB. Son utilisation est obligatoire si la classe de l'EJB implémente plusieurs interfaces différentes java.io.Serializable, java.io.Externalizable ou une des interfaces du package javax.ejb (optionnel) |
Exemple : |
import javax.ejb.Remote;
@Remote
public interface CalculRemote {
public long additionner(int valeur1, int valeur2);
}
Cette interface est marquée avec l'annotation @Remote, elle permet un appel distant et définit la méthode additionner.
Remarque : l'utilisation de l'annotation rend inutile l'utilisation de la clause throws RemoteException des versions antérieures des EJB.
L'annotation @javax.ejb.Local permet de préciser que l'EJB pourra être accédé par des clients locaux de la JVM. Elle s'utilise sur une classe qui encapsule un EJB ou l'interface qui décrit les fonctionnalités de l'EJB utilisables en local dans la JVM. Cette annotation ne peut être utilisée que pour des EJB sessions.
Elle possède un attribut :
Attribut |
Rôle |
Class[] value |
Préciser la liste des interfaces distantes de l'EJB. Son utilisation est obligatoire si la classe de l'EJB implémente plusieurs interfaces différentes java.io.Serializable, java.io.Externalizable ou une des interfaces du package javax.ejb (optionnel) |
Exemple : |
import javax.ejb.Local;
@Local
public interface CalculLocal {
public long additionner(int valeur1, int valeur2);
}
Cette interface est marquée avec l'annotation @Local pour permettre un appel local et définir la méthode additionner.
Il faut ensuite définir la classe de l'EJB qui va contenir les traitements métiers.
Exemple : |
import javax.ejb.*;
@Stateless
public class CalculBean implements CalculRemote, CalculLocal {
public long additionner(int valeur1, int valeur2) {
return valeur1 + valeur2;
}
}
Cette classe est marquée avec l'annotation @Stateless et implémente les interfaces distante et locale précédemment définies.
Il est préférable lorsque cela est possible d'utiliser l'interface Local car elle est beaucoup plus performante. L'interface Remote est à utiliser lorsque le client n'est pas dans la même JVM.
Les annotations @Local et @Remote peuvent être utilisées directement sur l'EJB mais il est préférable de définir une interface par mode d'accès et d'utiliser l'annotation adéquate sur chacune des interfaces.
La classe de l'EJB ne doit plus implémenter l'interface javax.ejb.SessionBean qui était obligatoire avec les EJB 2.x. Maintenant, les EJB Session de type stateless peuvent utiliser les callbacks d'évènements marqués avec les annotations suivantes :
Les beans de type Stateful sont capables de conserver leur état durant toute leur utilisation par le client. Cet état n'est cependant pas persistant : les données sont perdues à la fin de son utilisation ou à l'arrêt du serveur. Un exemple type d'utilisation de ce type de bean est l'implémentation d'un caddie pour un site de vente en ligne.
L'annotation @javax.ejb.Stateful permet de préciser qu'un EJB Session est de type Stateful. Elle s'utilise sur une classe qui encapsule un EJB.
Elle possède plusieurs attributs :
Attribut |
Rôle |
String name |
Nom de l'EJB (optionnel) |
String mappedName |
Nom sous lequel l'EJB sera mappé. La valeur par défaut est le nom non qualifié de la classe (optionnel) |
String description |
Description de l'EJB (optionnel) |
Le conteneur EJB a la possibilité de sérialiser/désérialiser des EJB de type Stateful notamment dans le cas où la JVM du conteneur commence à manquer de mémoire. Dans ce cas, le conteneur peut sérialiser des EJB (passivate) qui ne sont pas en cours d'utilisation sur le disque. Dès qu'un de ces EJB sera sollicité, le conteneur va le déserialiser (activate) à partir du disque pour le remettre en mémoire et pouvoir l'utiliser.
Les EJB 3 n'ont plus à implémenter l'interface javax.ejb.SessionBean comme c'était le cas dans les versions antérieures. Maintenant, les EJB session de type stateful peuvent utiliser les callbacks d'évènements marqués avec les annotations suivantes :
Le support des services web dans les EJB 3.0 repose essentiellement sur JAX-WS 2.0 et SAAJ qu'il faut privilégier au détriment de JAX-RPC qui est toujours supporté.
|
La suite de cette section sera développée dans une version future de ce document
|
Les exceptions personnalisées qui sont utilisées dans les interfaces métiers des EJB doivent être annotées avec @javax.ejb.ApplicationException qui s'utilise donc sur une classe encapsulant une exception métier.
Une exception annotée avec @ApplicationException sera directement envoyée au client par le conteneur.
Elle possède un seul attribut x:
Attribut |
Rôle |
boolean rollback |
Préciser si le conteneur doit effectuer un rollback si cette exception est levée. La valeur par défaut est false (optionnel) |
L'attribut rollback de l'annotation @ApplicationException de type booléen permet de préciser si la levée de l'exception va déclencher ou non un rollback de la transaction en cours. La valeur par défaut est false, signifiant qu'il n'y aura pas de rollback.
Exemple : |
package com.jmd.test.domaine.ejb;
import javax.ejb.ApplicationException;
@ApplicationException
public class ErreurMetierException extends Exception {
public ErreurMetierException() {
}
public ErreurMetierException(String msg) {
super(msg);
}
}
L'annotation @ApplicationException peut être utilisée avec des exceptions de type checked et unchecked.
Dans les versions antérieures des EJB, les EJB de type Entity avaient la charge de la persistance des données. Les EJB de type Entity CMP (Container Managed Persistence) nécessitent simplement un fichier de description.
Les EJB 3.0 proposent d'utiliser l'API Java Persistence pour assurer la persistance des données dans les EJB : ils utilisent un modèle de persistance léger standard en remplacement des entity beans de type CMP.
JPA repose sur des beans entity qui sont de simples POJO enrichis d'annotations permettant de mettre en oeuvre les concepts de POO tels que l'héritage ou le polymorphisme.
Jusqu'à la version 3.0 des EJB, les Entity beans sont des composants qui dépendent pleinement du conteneur d'EJB du serveur d'applications dans lequel ils s'exécutent. L'utilisation de POJO avec l'API Java Persistence permet de rendre les beans entity indépendants du conteneur. Ceci présente plusieurs avantages dont celui de pouvoir facilement tester les beans puisqu'ils ne requièrent plus de conteneur pour leur exécution.
Avec la version 3.0 des EJB, les beans entity sont donc des POJO qui n'ont pas besoin d'implémenter une interface spécifique aux EJB, doivent posséder un constructeur sans argument et implémenter l'interface Serializable.
Les attributs persistants sont déclarés par des annotations soit au niveau de l'attribut soit au niveau de son getter/setter. De ce fait, ils peuvent être utilisés directement comme objets du domaine ; il n'y a plus l'obligation de définir un DTO.
Les beans de type Entity sont dans la version 3.0 des spécifications de simple POJO utilisant les annotations de l'API Java Persistence (JPA) pour définir le mapping.
Les informations de mapping entre une table et un objet peuvent être définies grâce aux annotations mais aussi par un fichier de mapping qui permet d'externaliser les informations du POJO. Il est possible de mixer les deux (annotations et fichiers de mapping) mais les données incluses dans le fichier sont prioritaires sur les annotations.
Le bean entity doit être annoté avec l'annotation @Entity implémentée dans la classe javax.persistence.Entity.
L'annotation @Table implémentée dans la classe javax.persistence.Table permet de préciser le nom de la table vers laquelle le bean sera mappé. L'utilisation de cette annotation est facultative si le nom de la table correspond au nom de la classe.
Pour mapper un champ de la table avec une propriété du bean, il faut utiliser l'annotation @Column implémentée dans la classe javax.persistence.Column sur le getter de la propriété. L'utilisation de cette annotation est facultative si le nom du champ correspond au nom de la propriété.
Le champ correspondant à la clé primaire de la table doit être annoté avec l'annotation @Id implémentée dans la classe javax.persistence.Id. L'utilisation de cette annotation est obligatoire car un identifiant unique est obligatoire pour chaque occurrence et l'API n'a aucun moyen de déterminer le champ qui encapsule cette information.
Il peut être pratique pour un bean de type entity d'implémenter l'interface Serializable : le bean pourra être utilisé dans les paramètres et la valeur de retour des méthodes métiers d'un EJB. Le bean peut ainsi être utilisé pour la persistance et le transfert de données.
Exemple : |
package com.jmd.test.domaine.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
@Entity
@Table(name = "PERSONNE")
@NamedQueries({@NamedQuery(name = "Personne.findById",
query = "SELECT p FROM Personne p WHERE p.id = :id"),
@NamedQuery(name = "Personne.findByNom",
query = "SELECT p FROM Personne p WHERE p.nom = :nom"),
@NamedQuery(name = "Personne.findByPrenom",
query = "SELECT p FROM Personne p WHERE p.prenom = :prenom")})
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "ID", nullable = false)
private Integer id;
@Column(name = "NOM")
private String nom;
@Column(name = "PRENOM")
private String prenom;
public Personne() {
}
public Personne(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
@Override
public String toString() {
return "com.jmd.test.domaine.entity.Personne[id=" + id + "]";
}
}
Remarque : il est préférable de définir tous les beans de type entity dans un package dédié.
La mise en oeuvre précise de l'API JPA est proposée dans le chapitre qui lui est consacré.
La version 3.0 propose une refonte complète des EJB entités afin de simplifier leur développement. Cette simplification est assurée en grande partie par la mise en oeuvre de JPA qui permet :
La persistance d'objets avec JPA repose sur plusieurs fonctionnalités :
La classe EntityManager est responsable de la gestion des opérations sur une entité notamment grâce à plusieurs méthodes :
|
La suite de cette section sera développée dans une version future de ce document
|
Le bean entity n'est utilisé que pour le mapping. Pour réaliser des opérations avec le bean entity, il faut développer un EJB Session qui va encapsuler la logique des traitements à réaliser avec le bean entity.
Il faut définir l'interface Local et/ou Remote des méthodes métiers de l'EJB.
Il faut ensuite définir l'EJB qui va utiliser l'API Java Persistence et le bean entity.
L'injection de dépendances est utilisée pour obtenir une instance de l'EntityManager par le conteneur.
Exemple : |
@PersistenceContext
private EntityManager em;
L'annotation @PersistenceContext demande au conteneur d'injecter une instance de la classe EntityManager.
Le conteneur retrouve l'EntityManager grâce au nom de l'unité de persistance fourni comme valeur à la propriété unitName de l'annotation si plusieurs unités de persistance sont définies.
L'instance de type EntityManager peut être utilisée dans les méthodes métiers pour réaliser des traitements sur le bean entity.
Exemple : |
...
public void create(Personne personne) {
em.persist(personne);
}
public void edit(Personne personne) {
em.merge(personne);
}
public void remove(Personne personne) {
em.remove(em.merge(personne));
}
public Personne find(Object id) {
return em.find(com.jmd.test.domaine.entity.Personne.class, id);
}
...
L'exemple de cette section va développer un EJB métier effectuant des opérations de type CRUD sur une table nommée personne et permettant l'appel de cet EJB par un service web.
La table personne contient trois champs :
Cette table est stockée dans une base de données de type JavaDB.
Une connexion vers la base de données est définie dans l'annuaire sous le nom MaTestDb
Remarque : les sources de cet exemple sont générées par l'IDE Netbeans.
La classe Personne encapsule une entité sur la table personne.
Exemple : |
package com.jmd.test.domaine.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
@Entity
@Table(name = "PERSONNE")
@NamedQueries({@NamedQuery(name = "Personne.findById",
query = "SELECT p FROM Personne p WHERE p.id = :id"),
@NamedQuery(name = "Personne.findByNom",
query = "SELECT p FROM Personne p WHERE p.nom = :nom"),
@NamedQuery(name = "Personne.findByPrenom",
query = "SELECT p FROM Personne p WHERE p.prenom = :prenom")})
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "ID", nullable = false)
private Integer id;
@Column(name = "NOM")
private String nom;
@Column(name = "PRENOM")
private String prenom;
public Personne() {
}
public Personne(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
@Override
public String toString() {
return "com.jmd.test.domaine.entity.Personne[id=" + id + "]";
}
}
L'interface métier locale est définie dans l'interface PersonneFacadeLocal
Exemple : |
package com.jmd.test.domaine.ejb;
import com.jmd.test.domaine.entity.Personne;
import java.util.List;
import javax.ejb.Local;
@Local
public interface PersonneFacadeLocal {
void create(Personne personne);
void edit(Personne personne);
void remove(Personne personne);
Personne find(Object id);
List<Personne> findAll();
}
L'interface métier distante est définie dans l'interface PersonneFacadeRemote
Exemple : |
package com.jmd.test.domaine.ejb;
import com.jmd.test.domaine.entity.Personne;
import java.util.List;
import javax.ejb.Remote;
@Remote
public interface PersonneFacadeRemote {
void create(Personne personne);
void edit(Personne personne);
void remove(Personne personne);
Personne find(Object id);
List<Personne> findAll();
}
La façade est implémentée sous la forme d'un EJB de type stateless.
Exemple : |
package com.jmd.test.domaine.ejb;
import com.jmd.test.domaine.entity.Personne;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless
public class PersonneFacade implements PersonneFacadeLocal, PersonneFacadeRemote {
@PersistenceContext
private EntityManager em;
public void create(Personne personne) {
em.persist(personne);
}
public void edit(Personne personne) {
em.merge(personne);
}
public void remove(Personne personne) {
em.remove(em.merge(personne));
}
public Personne find(Object id) {
return em.find(com.jmd.test.domaine.entity.Personne.class, id);
}
public List<Personne> findAll() {
return em.createQuery("select object(o) from Personne as o").getResultList();
}
}
Les fonctionnalités offertes par l'EJB sont de type CRUD.
Le fichier persistence.xml demande simplement l'utilisation de la connexion définie dans l'annuaire.
Exemple : |
<?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="EnterpriseApplication3-ejbPU" transaction-type="JTA">
<jta-data-source>MaTestDb</jta-data-source>
<properties/>
</persistence-unit>
</persistence>
Pour permettre une meilleure séparation des rôles de chaque classe, le service web est développé dans une classe dédiée.
Exemple : |
package com.jmd.test.services;
import com.jmd.test.domaine.ejb.PersonneFacadeLocal;
import com.jmd.test.domaine.entity.Personne;
import java.util.List;
import javax.ejb.EJB;
import javax.jws.Oneway;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.ejb.Stateless;
@WebService()
@Stateless()
public class PersonneWS {
@EJB
private PersonneFacadeLocal ejbRef;
@WebMethod(operationName = "create")
@Oneway
public void create(Personne personne) {
ejbRef.create(personne);
}
@WebMethod(operationName = "edit")
@Oneway
public void edit(Personne personne) {
ejbRef.edit(personne);
}
@WebMethod(operationName = "remove")
@Oneway
public void remove(Personne personne) {
ejbRef.remove(personne);
}
@WebMethod(operationName = "find")
public Personne find(Object id) {
return ejbRef.find(id);
}
@WebMethod(operationName = "findAll")
public List<Personne> findAll() {
return ejbRef.findAll();
}
}
L'injection de dépendances est utilisée pour laisser le conteneur fournir au service web une référence sur l'instance de l'EJB.
|
La suite de cette section sera développée dans une version future de ce document
|
Un client distant est en mesure d'utiliser des EJB possédant une interface de type Remote : dans ce cas, plusieurs opérations sont à réaliser
Les informations nécessaires à la connexion à l'annuaire JNDI du serveur d'applications sont spécifiques à chaque implémentation du serveur.
Le nom de stockage de l'interface dans JNDI est aussi spécifique au serveur utilisé.
|
La suite de cette section sera développée dans une version future de ce document
|
|
La suite de cette section sera développée dans une version future de ce document
|
Le conteneur peut être utilisé pour assurer l'injection de dépendances de certaines ressources requises par exemple par un contexte de persistance ou un autre EJB.
L'injection de dépendances est réalisée au moment de l'instanciation du bean par le conteneur.
L'injection de dépendances permet de simplifier le travail des développeurs : il n'est plus nécessaire d'invoquer l'annuaire du serveur par JNDI et de caster le résultat pour obtenir une instance de la dépendance. C'est le conteneur lui-même qui va s'en charger grâce à des annotations déchargeant le développeur de l'écriture du code utilisant JNDI ou un objet de type EJBContext.
Plusieurs annotations sont définies pour mettre en oeuvre cette injection de dépendances :
Les annotations relatives à l'injection de dépendances peuvent être utilisées sur des variables d'instance ou sur des méthodes de type setter.
L'annotation @EJB permet de demander au conteneur d'injecter une référence sur un EJB sans avoir à faire appel explicitement à l'annuaire du serveur avec JNDI.
Exemple : |
@EJB
private PersonneFacadeLocal ejbReference;
Le conteneur utilise par défaut le type de la variable pour déterminer le type de l'instance de l'EJB qui sera injectée.
Elle s'utilise sur une classe, une méthode ou une propriété :
Elle possède plusieurs attributs :
Attribut |
Rôle |
Class beanInterface |
Nom de l'interface de l'EJB |
String beanName |
Nom de l'EJB (correspond à l'attribut name des annotations @Stateless et @Stateful). Par défaut, c'est le nom de la classe de l'EJB |
String description |
Description de l'EJB |
String mappedName |
Nom JNDI de l'EJB. Cet attribut n'est pas portable |
String name |
Nom avec lequel l'EJB sera recherché |
L'injection de dépendances est réalisée entre l'assignation de l'EJBContext et le premier appel d'une méthode de l'EJB.
S'il y a une ambiguïté pour déterminer le type de l'EJB à injecter, il est possible d'utiliser les attributs beanName et mappedName de l'annotation @EJB pour désigner l'EJB concerné.
L'annotation @javax.annotation.Resource permet d'injecter des instances de ressources gérées par le conteneur telles qu'une datasource JDBC ou une destination JMS (queue ou topic) par exemple.
Cette annotation est utilisable sur une classe, une méthode ou un champ. Si l'annotation est utilisée sur une méthode ou un champ, le conteneur injecte les références au moment de l'initialisation du bean.
Elle possède plusieurs attributs :
Attribut |
Rôle |
String name |
Nom de la ressource |
Class type |
Type de la ressource |
AuthenticationType authenticationType |
Type d'authentification à utiliser pour accéder à la ressource. Les valeurs possibles sont AuthenticationType.CONTAINER et AuthenticationType.APPLICATION |
boolean shareable |
Indiquer si la ressource peut être partagée entre cet EJB et d'autres EJB. Ne s'applique que sur certains types de ressources |
String mappedName |
Nom JNDI de la ressource |
String description |
Description de la ressource |
|
La suite de cette section sera développée dans une version future de ce document
|
L'annotation @WebServiceRef possède plusieurs attributs :
Attribut |
Rôle |
String name |
Nom JNDI de la ressource |
String wsdlLocation |
URL pointant sur le WSDL du service web |
Class type |
Type Java |
Class value |
La classe du service qui doit obligatoirement hériter de javax.xml.ws.Service |
String mappedName |
Pour définir les mêmes fonctionnalités dans le descripteur de déploiement, il faut utiliser le tag <service-ref>.
Un intercepteur est une méthode qui sera exécutée selon deux types d'événements :
Un intercepteur permet de définir des traitements, généralement transverses, qui seront exécutés lorsque ces événements surviendront. Leur rôle est similaire à certaines fonctionnalités de base de l'AOP (programmation orientée aspect)
Les intercepteurs sont utilisables avec des EJB Session et MessageDriven.
Les annotations dédiées, utilisées pour la mise en oeuvre des intercepteurs, sont regroupées dans le package javax.interceptor :
Un intercepteur permet de définir des traitements sous la forme de méthodes qui seront exécutées soit à l'invocation d'une méthode métier soit lors d'événements liés au cycle de vie de l'EJB. Son rôle est similaire à certaines fonctionnalités de base proposées par l'AOP.
Un intercepteur peut être défini soit :
La signature des méthodes de callback diffère selon la nature de l'intercepteur :
L'interface javax.interceptor.InvocationContext définit les fonctionnalités pour permettre d'utiliser un contexte lors de l'invocation d'un ou plusieurs intercepteurs.
Cette interface définit plusieurs méthodes :
Méthode |
Rôle |
Object getTarget() |
Renvoyer l'instance de l'EJB |
Method getMethod() |
Renvoyer la méthode métier de l'EJB qui a provoqué l'invocation de l'intercepteur. Si l'invocation est liée au cycle de vie de l'EJB alors cette méthode renvoie null |
Object[] getParameters() |
Renvoyer un tableau des paramètres de la méthode du bean pour laquelle l'intercepteur a été invoqué |
void setParameters(Object[]) |
Modifier les paramètres qui seront utilisés pour l'invocation de la méthode |
Map<String,Object> getContextData() |
Obtenir une collection des données associées à l'invocation du callback |
Object proceed() |
Invoquer le prochain intercepteur de la chaîne ou de la méthode métier de l'EJB si tous les intercepteurs ont été invoqués |
Une instance de type InvocationContext est passée en paramètre des intercepteurs.
Il est ainsi possible d'échanger des données entre les invocations des intercepteurs définis pour une même méthode.
Attention : une instance d'InvocationContext n'est pas partageable entre un intercepteur pour des méthodes métiers et un intercepteur pour des événements liés au cycle de vie des EJB.
L'annotation @AroundInvoke permet de marquer une méthode qui sera exécutée lors de l'invocation des méthodes métiers d'un EJB. Cette annotation ne peut être utilisée qu'une seule fois dans une même classe d'un intercepteur ou d'un EJB. Il n'est pas possible d'annoter une méthode métier avec l'annotation @AroundInvoke.
La signature d'une méthode annotée avec @AroundInvoke doit être de la forme :
Object nomMethode(InvocationContext) throws Exception
Une méthode annotée avec @AroundInvoke doit toujours invoquer la méthode proceed() de l'instance de type InvocationContext fournie en paramètre pour permettre l'invocation d'éventuels autres intercepteurs assossiés à la méthode.
Exemple : |
package com.jmd.test.domaine.ejb;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
/**
* Intercepteur qui calcule le temps d'exécution d'une méthode métier
* @author jmd
*/
public class MesurePerfIntercepteur {
@AroundInvoke
public Object mesurerPerformance(InvocationContext ic) throws Exception {
long debutExec = System.currentTimeMillis();
try {
return ic.proceed();
} finally {
long tempsExec = System.currentTimeMillis() - debutExec;
System.out.println("[PERF] Temps d'execution de la methode " + ic.getClass()
+ "." + ic.getMethod() + " : " + tempsExec + " ms");
}
}
}
L'exemple ci-dessus permet de définir un intercepteur qui logguera les temps d'exécutions des méthodes métiers des EJB.
Un intercepteur peut être exécuté lorsque certains événements liés au cycle de vie de l'EJB tels que la création, la destruction, la passivation ou la réactivation surviennent.
Les EJB 2.x imposaient l'implémentation de méthodes d'une interface telles que ejbCreate(), ejbPassivate(), ... Avec les EJB 3.x, ces méthodes peuvent avoir un nom quelconque du moment qu'elles sont annotées avec une annotation liée à un événement du cycle de vie de l'EJB. Les annotations pour définir des callbacks sur des invocations de méthodes liées au cycle de vie de l'EJB sont :
Il est possible dans une même classe d'utiliser plusieurs de ces annotations mais il n'est pas possible d'utiliser plusieurs fois la même dans une même classe.
Une méthode annotée avec une annotation liée au cycle de vie dans une classe d'un intercepteur doit invoquer la méthode proceed() de l'instance de type InvocationContext fournie en paramètre pour permettre l'invocation des traitements liés à l'état courant du cycle de vie de l'EJB.
Une classe d'intercepteurs est un simple POJO qui doit obligatoirement avoir un constructeur sans paramètre et dont certaines méthodes sont annotées avec l'annotation @AroundInvoke ou avec une annotation liée au cycle de vie de l'EJB.
Exemple : |
package com.jmd.test.domaine.ejb;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
public class MonIntercepteur {
@AroundInvoke
public Object audit(InvocationContext ic)
throws Exception {
System.out.println("MonIntercepteur Invocation de la methode : " + ic.getMethod());
return ic.proceed();
}
@PreDestroy
public void preDestroy(InvocationContext ic) {
System.out.println("MonIntercepteur suppression du bean : " + ic.getTarget());
}
@PostConstruct
public void postConstruct(InvocationContext ic) {
System.out.println("MonIntercepteur Creation du bean : " + ic.getTarget());
}
}
Les intercepteurs peuvent avoir accès par injection de dépendances aux ressources gérées par le conteneur (EJB, EntityManager, destination JMS, ...).
Un intercepteur métier peut lever une exception applicative puisque les méthodes métiers peuvent lever une exception dans leur clause throws.
Les intercepteurs définis dans la classe de l'EJB sont exécutés après les intercepteurs précisés par l'annotation @Interceptors.
Il est possible de définir des intercepteurs par défaut qui seront appliqués à tous les EJB d'un même jar.
La définition d'un intercepteur par défaut ne peut se faire que dans le descripteur de déploiement ejb-jar.xml. Ils ne peuvent pas être définis par des annotations.
Pour déclarer un intercepteur par défaut, il faut modifier le descripteur de déploiement ejb-jar.xml en utilisant un tag <interceptor-binding> fils du tag <assembly-descriptor>.
Le tag <interceptor-binding> peut avoir deux tags fils :
L'intercepteur doit être déclaré dans un tag <interceptor> fils du tag <interceptors>. Le tag fils <interceptor-class> permet de préciser le nom pleinement qualifié de la classe de l'intercepteur.
Exemple : |
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee"
version = "3.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
<interceptors>
<interceptor>
<interceptor-class>com.jmd.test.domaine.ejb.MesurePerfIntercepteur</interceptor-class>
</interceptor>
</interceptors>
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>com.jmd.test.domaine.ejb.MesurePerfIntercepteur</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
Les intercepteurs par défaut sont toujours invoqués avant les autres intercepteurs.
Pour empêcher l'invocation d'un intercepteur par défaut pour un EJB, il faut l'annoter avec l'annotation @javax.interceptor.excludeDefaultInterceptors.
Ces intercepteurs offrent donc deux avantages :
Plusieurs annotations peuvent être utilisées lors de la mise en oeuvre des intercepteurs. Celles-ci sont soit des annotations dédiées contenues dans le package javax.interceptor soit des annotations standards de l'API Java contenues dans le package javax.annotation.
L'annotation @javax.annotation.Postconstruct permet de définir un intercepteur qui est lié à l'événement de création du cycle de vie de l'EJB.
La méthode annotée avec cette annotation sera invoquée par le conteneur après l'initialisation de l'EJB et l'injection des dépendances mais avant l'appel de la première méthode.
Elle peut être utilisée dans la classe d'un intercepteur ou dans la classe d'un EJB mais dans les deux cas, une seule méthode peut être annotée avec cette annotation.
Elle s'utilise sur une méthode dont la signature doit respecter quelques contraintes :
L'annotation @javax.annotation.PreDestroy permet de définir un intercepteur qui est lié à l'événement de suppression du cycle de vie de l'EJB.
La méthode annotée avec cette annotation sera invoquée par le conteneur avant que l'EJB ne soit détruit du conteneur. Celle-ci pourra par exemple procéder à la libération de ressources.
Elle peut être utilisée dans la classe d'un intercepteur ou dans la classe d'un EJB mais dans les deux cas, une seule méthode peut être annotée avec cette annotation.
Elle s'utilise sur une méthode dont la signature doit respecter quelques contraintes :
L'annotation @javax.interceptor.AroundInvoke permet de définir un intercepteur qui est lié à l'exécution de méthodes métiers. Cette annotation peut être utilisée dans la classe d'un intercepteur ou dans la classe d'un EJB mais dans les deux cas, une seule méthode peut être annotée avec cette annotation.
Elle s'utilise sur une méthode. Elle ne possède aucun attribut.
L'annotation @javax.interceptor.ExcludeClassInterceptors permet de demander d'inhiber l'invocation des intercepteurs pour une méthode. L'inhibition ne concerne pas les intercepteurs par défaut.
Elle s'utilise sur une méthode. Elle ne possède aucun attribut
L'annotation @javax.interceptor.ExcludeDefaultInterceptors permet d'inhiber l'invocation des intercepteurs par défaut. Utilisée sur la classe d'un bean, cette annotation inhibe l'invocation des intercepteurs par défaut pour toutes les méthodes métiers du bean. Utilisée sur une méthode d'un bean, l'inhibition se limite à cette méthode.
Cette annotation s'utilise sur une classe ou une méthode.
L'annotation @javax.interceptor.Interceptors permet de définir les classes d'intercepteurs qui seront invoquées par le conteneur. Si plusieurs classes d'intercepteurs sont définies alors elles seront invoquées dans l'ordre de leur définition dans l'annotation.
Si l'annotation est utilisée sur la classe du bean alors les intercepteurs seront invoqués pour chaque méthode du bean. Si l'annotation est utilisée sur une méthode alors les intercepteurs seront invoqués uniquement pour la méthode.
Les intercepteurs sont invoqués dans un ordre précis :
Elle s'utilise sur une classe ou une méthode. Elle possède un seul attribut :
Attribut |
Rôle |
Class[] value |
Préciser un tableau de classes d'intercepteurs. L'ordre des intercepteurs dans le tableau définit leur ordre d'invocation (obligatoire) |
Chaque EJB qui souhaite utiliser un intercepteur devra l'ajouter grâce à l'annotation @javax.interceptor.Interceptors.
Exemple : |
package com.jmd.test.domaine.ejb;
import com.jmd.test.domaine.entity.Personne;
import java.util.List;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless
@Interceptors({ com.jmd.test.domaine.ejb.MonIntercepteur.class})
public class PersonneFacade implements PersonneFacadeLocal, PersonneFacadeRemote {
@PersistenceContext
private EntityManager em;
public void create(Personne personne) {
em.persist(personne);
}
...
public List<Personne> findAll() {
return em.createQuery("select object(o) from Personne as o").getResultList();
}
}
Lors du lancement du serveur d'applications et de l'appel de cet EJB, les traces suivantes sont affichées dans la console du serveur d'applications :
Résultat : |
...
Creation du bean
: com.jmd.test.domaine.ejb.PersonneFacade@1697e2a
...
Invocation de la
methode : public java.util.List
com.jmd.test.domaine.ejb.PersonneFacade.findAll()
...
Plusieurs intercepteurs peuvent être indiqués par cette annotation : leur ordre d'exécution sera celui dans lequel ils sont précisés dans l'annotation.
Un intercepteur est toujours exécuté dans la même transaction et le même contexte de sécurité que la méthode qui est à l'origine de son invocation.
Par défaut, si l'intercepteur est défini au niveau de la classe de l'EJB, toutes les méthodes concernées de l'EJB provoqueront l'invocation de l'intercepteur par le conteneur.
Si l'intercepteur est défini au niveau d'une méthode, l'intercepteur ne sera exécuté qu'à l'invocation de la méthode annotée.
L'annotation @javax.interceptor.ExcludeClassInterceptors sur une méthode permet de demander que l'exécution des intercepteurs de type @AroundInvoke précisés dans l'annotation @Interceptors soit ignorée pour la méthode.
L'annotation @javax.interceptorExcludeDefaultInterceptors sur une classe ou une méthode permet de demander que l'exécution des intercepteurs par défaut soit ignorée.
Il est possible d'associer un intercepteur à un EJB dans le descripteur de déploiement, donc sans utiliser l'annotation @Interceptors.
Exemple : |
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee"
version = "3.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
<interceptors>
<interceptor>
<interceptor-class>com.jmd.test.domaine.ejb.MesurePerfIntercepteur</interceptor-class>
</interceptor>
</interceptors>
<assembly-descriptor>
<interceptor-binding>
<ejb-name>PersonneFacade</ejb-name>
<interceptor-class>com.jmd.test.domaine.ejb.MesurePerfIntercepteur</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
Les EJB de type MessageDriven permettent de réaliser des traitements asynchrones exécutés à la réception d'un message dans une queue JMS.
Ils ne proposent pas d'interface locale ou distante et ne peuvent pas être utilisés comme un service web. Pour connecter le bean à une queue JMS, il faut que le bean implémente l'interface javax.jms.MessageListener.
Cette interface définit la méthode onMessage(Message).
L'annotation @ javax.ejb.MessageDriven permet de préciser qu'un EJB est de type MessageDriven. Elle s'utilise sur une classe qui encapsule un EJB.
L'annotation @MessageDriven possède plusieurs attributs optionnels :
Attribut |
Rôle |
ActivationConfigProperty[] activationConfig |
Préciser les informations de configuration (type de endpoint, destination (queue ou topic), mode d'aquittement des messages, ...) sous la forme d'un tableau d'annotations de type @javac.ejb.ActivationConfigProperty (optionnel) |
String description |
Description de l'EJB (optionnel) |
String mappedName |
Nom sous lequel l'EJB sera mappé. Peut aussi être utilisé pour désigner le nom JNDI de la destination utilisée (optionnel) |
Class messageListenerInterface |
Préciser l'interface de type message Listener. Il faut utiliser cet attribut si l'EJB n'implémente pas d'interface ou implémente plusieurs interfaces différentes de java.io.Serializable, java.io.Externalizable ou une ou plusieurs interfaces du package javax.ejb. La valeur par défaut est Object.class (optionnel) |
String name |
Nom de l'EJB. La valeur par défaut est le nom non qualifié de la classe (optionnel) |
Les paramètres nécessaires à la configuration de l'EJB notamment le type et la destination sur laquelle le bean doit écouter doivent être précisés grâce à l'attribut activationConfig. Cet attribut est un tableau d'objets de type ActivationConfigProperty.
L'annotation @javax.ejb.ActivationConfigProperty permet de préciser le nom et la valeur d'une propriété de la configuration des EJB de type MessageDriven. Elle s'utilise dans la propriété activationConfig d'une annotation de type javax.ejb.MessageDriven.
Elle possède plusieurs attributs :
Attribut |
Rôle |
String propertyName |
Préciser le nom de la propriété (obligatoire) |
String propertyValue |
Préciser la valeur de la propriété (obligatoire) |
Exemple : |
@MessageDriven(mappedName = "jms/MonEJBQueue", activationConfig = {
@ActivationConfigProperty(propertyName="acknowledgeMode", propertyValue="Auto-acknowledge"),
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue")
})
L'exemple ci-dessous est un EJB de type MessageDriven qui écoute sur une queue nommée jms/MonEJBQueue et qui affiche sur la console le contenu des messages de type texte reçus dans la queue.
Exemple : |
package com.jmd.test.domaine.ejb;
import javax.annotation.Resource;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.ejb.MessageDrivenContext;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
@MessageDriven(mappedName = "jms/MonEJBQueue", activationConfig = {
@ActivationConfigProperty(propertyName="acknowledgeMode", propertyValue="Auto-acknowledge"),
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue")
})
public class MonEJBMessageBean implements MessageListener {
@Resource
private MessageDrivenContext mdc;
public MonEJBMessageBean() {
}
public void onMessage(Message message) {
TextMessage msg = null;
try {
if (message instanceof TextMessage) {
msg = (TextMessage) message;
System.out.println("Message recu = " + msg.getText());
}
} catch (JMSException e) {
e.printStackTrace();
mdc.setRollbackOnly();
} catch (Throwable te) {
te.printStackTrace();
}
}
}
Il est possible d'écrire un client de test par exemple sous la forme d'une servlet. Cette servlet peut utiliser l'injection de dépendances si elle s'exécute dans le même serveur d'applications.
Exemple : |
package com.jmdoudoux.test.servlet;
import java.io.*;
import java.net.*;
import javax.annotation.Resource;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.servlet.*;
import javax.servlet.http.*;
public class EnvoyerMessage extends HttpServlet {
@Resource(mappedName = "jms/MonEJBQueue")
Queue queue = null;
@Resource(mappedName = "jms/MonEJBQueueFactory")
QueueConnectionFactory factory = null;
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet EnvoyerMessage</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Servlet EnvoyerMessage</h1>");
out.println("<form>");
out.println("Message : <input type='text' name='msg'><br/>");
out.println("<input type='submit'><br/>");
out.println("</form>");
out.println("</body>");
out.println("</html>");
String msg = request.getParameter("msg");
if (msg != null) {
QueueConnection connection = null;
QueueSession session = null;
MessageProducer messageProducer = null;
try {
connection = factory.createQueueConnection();
session = connection.createQueueSession(false,
QueueSession.AUTO_ACKNOWLEDGE);
messageProducer = session.createProducer(queue);
TextMessage message = session.createTextMessage();
message.setText(msg);
messageProducer.send(message);
messageProducer.close();
connection.close();
} catch (JMSException ex) {
ex.printStackTrace();
}
}
} finally {
out.close();
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
public String getServletInfo() {
return "Envoi d'un message pour test EJB de type MDB";
}
}
Si le client ne s'exécute pas dans le même serveur d'applications, il faut utiliser JNDI pour obtenir la queue et la fabrique de connexions.
Exemple : |
package com.jmdoudoux.test.servlet;
import java.io.*;
import java.net.*;
import javax.annotation.Resource;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.servlet.*;
import javax.servlet.http.*;
public class EnvoyerMessage extends HttpServlet {
Queue queue = null;
QueueConnectionFactory factory = null;
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet EnvoyerMessage</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Servlet EnvoyerMessage</h1>");
out.println("<form>");
out.println("Message : <input type='text' name='msg'><br/>");
out.println("<input type='submit'><br/>");
out.println("</form>");
out.println("</body>");
out.println("</html>");
String msg = request.getParameter("msg");
if (msg != null) {
QueueConnection connection = null;
QueueSession session = null;
MessageProducer messageProducer = null;
try {
InitialContext ctx = new InitialContext();
queue = (Queue) ctx.lookup("jms/MonEJBQueue");
factory = (QueueConnectionFactory) ctx.lookup("jms/MonEJBQueueFactory");
connection = factory.createQueueConnection();
session = connection.createQueueSession(false,
QueueSession.AUTO_ACKNOWLEDGE);
messageProducer = session.createProducer(queue);
TextMessage message = session.createTextMessage();
message.setText(msg);
messageProducer.send(message);
messageProducer.close();
connection.close();
} catch (JMSException ex) {
ex.printStackTrace();
} catch (NamingException ex) {
ex.printStackTrace();
}
}
} finally {
out.close();
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
public String getServletInfo() {
return "Envoi d'un message pour test EJB de type MDB";
}
}
Il est important que la queue et la fabrique de connexions soient définies dans l'annuaire du serveur d'applications.
Exemple avec GlassFish : extrait du fichier sun-resources.xml
Exemple : |
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//Sun Microsystems, Inc.
//DTD Application Server 9.0 Resource Definitions
//EN" "http://www.sun.com/software/appserver/dtds/sun-resources_1_3.dtd">
<resources>
...
<admin-object-resource enabled="true" jndi-name="jms/MonEJBQueue"
object-type="user" res-adapter="jmsra" res-type="javax.jms.Queue">
<description/>
<property name="Name" value="PhysicalQueue"/>
</admin-object-resource>
<connector-resource enabled="true" jndi-name="jms/MonEJBQueueFactory"
object-type="user" pool-name="jms/MonEJBQueueFactoryPool">
<description/>
</connector-resource>
...
</resources>
Les EJB de type MessageDriven peuvent exploiter toutes les fonctionnalités de JMS : utilisation d'une queue ou d'un topic comme destination, utilisation des différents types de messages (TextMessage, ObjectMessage, ...)
Les EJB doivent être packagés dans une archive de type jar qui contiendra tous les éléments nécessaires à leur exécution.
Le fichier jar peut lui-même être incorporé dans une archive de type EAR (Enterprise Archive) qui regroupe plusieurs modules dans une même entité (EJB, application web, application client lourde, ...).
|
La suite de cette section sera développée dans une version future de ce document
|
Une transaction exécute une succession d'unités de traitements qui utilisent une ou plusieurs ressources, le plus souvent une base de données. Ces unités de traitements forment un ensemble d'activités qui interagit pour former un tout fonctionnel : leurs exécutions doivent toutes réussir ou aucune ne doit être exécutée.
Le but d'une transaction est de s'assurer que toutes les unités de traitements qu'elle inclut seront correctement exécutées ou qu'aucune ne le sera si un problème survient.
Une transaction permet d'assurer l'intégrité des données car soit elle s'exécute correctement dans son intégralité soit elle ne fait aucune modification.
Une transaction possède quatre caractéristiques connues sous l'acronyme ACID :
L'abandon d'une transaction ne doit donc pas simplement se limiter à son arrêt, il est aussi obligatoire d'annuler toutes les mises à jour déjà réalisées par la transaction pour permettre de laisser le système dans son état initial au lancement de la transaction.
Le conteneur d'EJB propose un support des transactions par déclaration ce qui évite d'avoir à mettre en oeuvre explicitement une API de gestion des transactions dans le code.
Dans les EJB, une transaction concerne une méthode d'un EJB : cette transaction inclut tous les traitements contenus dans la méthode. Ceci inclut donc aussi les appels aux méthodes d'autres EJB sous réserve de leur déclaration de participation à une transaction.
La transaction est aussi propagée au contexte de persistance assuré par les EntityManager. Si la transaction est validée, alors le contexte de persistance va rendre persistante les modifications effectuées durant la transaction.
Tous les traitements inclus dans la transaction définissent la portée de la transaction.
Lorsque la transaction est gérée par le conteneur, la décision de valider ou d'abandonner la transaction est prise par le conteneur. Une transaction est abandonnée si une exception est levée dans les traitements de la méthode ou par une des méthodes de l'EJB appelées dans ses traitements.
L'annotation @TransactionAttribute implémentée dans la classe javax.ejb.TransactionAttribute ou le descripteur des EJB permet de mettre en oeuvre les transactions par déclaration. Ceci transforme les caractéristiques de la transaction sans avoir à modifier le code des traitements dans la méthode de l'EJB.
L'annotation javax.ejb.TransactionManagement permet de préciser le mode de gestion des transactions dans un EJB de type session ou message driven. Ce mode peut prendre deux valeurs :
Elle s'utilise sur une classe d'un EJB session ou message driven.
Elle possède un attribut :
Attribut |
Rôle |
TransactionManagementType value |
Préciser le mode de gestion des transactions dans l'EJB. Cet attribut peut prendre deux valeurs :
|
Dans le cas où le mode précisé est BEAN, il est nécessaire de coder la gestion des transations dans les méthodes qui en ont besoin en utilisant l'API JTA.
L'annotation @javax.ejb.TransactionAttribute permet de préciser dans quel contexte transactionnel une méthode d'un EJB sera invoquée. Cette annotation est incompatible avec la valeur BEAN de l'annotation TransactionManagement.
Elle s'utilise sur une classe d'un EJB ou sur une méthode d'un EJB session. Utilisée sur une classe, l'annotation s'applique à toutes les méthodes de l'EJB.
Elle possède un attribut :
Attribut |
Rôle |
TransactionAttributeType value |
Préciser le contexte transactionnel d'invocation d'une méthode de l'EJB. Cet attribut peut prendre plusieurs valeurs :
|
L'annotation @TransactionAttribute peut prendre différentes valeurs :
Cette annotation peut être utilisée au niveau de l'EJB (dans ce cas, toutes les méthodes de l'EJB utilisent la même déclaration des attributs de transaction) ou au niveau de chaque méthode.
Les déclarations des attributs relatives aux transactions peuvent aussi être faites dans le descripteur de déploiement. Le tag <container-transaction> est utilisé pour préciser les attributs de transaction d'une ou plusieurs méthodes d'un EJB.
Pour un EJB, le tag fils <method> permet de préciser la ou les méthodes concernées. Le tag fils <ejb-name> indique l'EJB. Le tag <method-name> permet de préciser la méthode concernée ou toutes les méthodes de l'EJB en mettant * comme valeur du tag.
Le tag fils <trans-attribute> permet de préciser l'attribut de transaction à utiliser.
Il est fortement recommandé d'utiliser un contexte de persistance (EntityManager) dans la portée d'une transaction afin de s'assurer que tous les accès à la base de données se font dans un contexte transactionnel. Ceci implique d'utiliser les attributs de transaction Required, Requires_New ou Mandatory.
Un EJB de type MessageDriven ne peut utiliser que les attributs de transaction NotSupported et Required. L'attribut NotSupported précise que les messages ne seront pas traités dans une transaction. L'attribut Required précise que les messages seront traités dans une transaction créée par le conteneur.
Il n'est pas possible d'utiliser l'attribut Mandatory avec un EJB qui est proposé sous la forme d'un service web.
La gestion des attributs d'une transaction est importante car l'utilisation d'un EJB dans un contexte transactionnel est coûteuse en ressources. Il faut bien tenir compte du fait que la valeur par défaut des attributs de transaction est utilisée si aucun attribut n'est précisé et que cet attribut par défaut est REQUIRED, ce qui place automatiquement l'EJB dans un contexte transactionnel.
Il est donc fortement recommandé d'utiliser un attribut de transaction NotSupported lorsqu'aucune transaction n'est requise.
Les autorisations reposent en Java EE sur la notion de rôle. Un ou plusieurs rôles sont affectés à un utilisateur. L'attribution des autorisations se fait donc au niveau rôle et non au niveau utilisateur.
Même s'il est possible d'utiliser une API dédiée, généralement la mise en oeuvre de la sécurité dans les EJB se fait de manière déclarative.
Seuls les EJB de type Session peuvent être sécurisés.
Pour définir des restrictions, il faut utiliser le descripteur de déploiement ou les annotations dédiées. Ces restrictions reposent sur la notion de rôle.
Lorsqu'une méthode est invoquée et que le conteneur détecte une violation des restrictions d'accès alors ce dernier lève une exception de type javax.ejb.EJBAccessException qui devra être traitée par le client appelant.
Lorsqu'un client utilise des fonctionnalités du conteneur d'EJB, il possède un identifiant de sécurité durant sa connexion. L'authentification de l'utilisateur est à la charge de l'application cliente.
La façon dont l'utilisateur est fourni au conteneur est dépendante de l'implémentation des EJB utilisés. Généralement cela se fait en passant des propriétés lors de la recherche du contexte JNDI.
Certains serveurs d'applications utilisent des mécanismes plus complexes et plus riches fonctionnellement en mettant en oeuvre l'API JAAS par exemple.
Lors de l'invocation des méthodes des EJB, cet identifiant de sécurité est passé implicitement à chaque appel pour permettre au conteneur de vérifier les autorisations d'utilisation par l'utilisateur.
La définition des restrictions d'accès permet la mise en oeuvre des mécanismes d'autorisation.
Lorsqu'un utilisateur invoque un EJB, le conteneur contrôle les autorisations d'exécution de la méthode invoquée en comparant le ou les rôles de l'utilisateur avec le ou les rôles autorisés à exécuter cette méthode.
Le mécanisme d'autorisations est précisement défini dans les spécifications des EJB. La définition des autorisations peut être déclarée de deux façons différentes :
Par défaut, toutes les méthodes publiques d'un EJB peuvent être invoquées sans restriction de sécurité.
La définition de restrictions d'accès à un EJB se fait principalement grâce à l'annotation @javax.annotation.security.RolesAllowed qui permet de préciser les rôles qui seront autorisés à invoquer la méthode de l'EJB.
L'annotation @RolesAllowed peut s'utiliser :
L'annotation @PermitAll permet l'invocation par tout le monde : c'est l'annotation par défaut si aucune restriction n'est définie.
L'annotation @DenyAll permet d'empêcher l'invocation d'une méthode quel que soit le rôle de l'utilisateur qui l'invoque.
L'annotation @RunAs permet de forcer le rôle sous lequel l'EJB est exécuté dans le conteneur. Cette annotation ne fait aucun contrôle d'accessibilité.
La définition de la configuration de sécurité incluant les rôles et les restrictions d'accès peut être réalisée en tout ou partie dans le descripteur de déploiement.
Les restrictions d'accès sont définies dans un tag <method-permission>
Le tag <method-permission> peut avoir plusieurs tags fils :
Le tag <method> possède plusieurs tags fils :
Pour empêcher l'accès à certaines méthodes, il faut utiliser le tag <exclude-list> fils du tag <assembly-descriptor>. Le tag <exclude-list> a un rôle équivalent à l'annotation @DenyAll. Chaque méthode concernée est décrite avec un tag fils <method>.
Les spécifications des EJB 3.0 définissent plusieurs annotations pour gérer et mettre en oeuvre la sécurité dans les accès réalisés sur les EJB.
L'annotation @DeclareRoles permet de définir la liste des rôles qui sont utilisés par un EJB pour sécuriser l'invocation de ses méthodes.
Cette annotation est utile pour préciser les rôles au conteneur dans le cas où les restrictions d'accès sont définies par programmation. Elle peut aussi être utilisée pour fournir explicitement au conteneur la liste des rôles implicitement définis dans les annotations @RolesAllowed.
L'annotation @DeclareRoles s'applique uniquement sur une classe. Elle ne possède qu'un seul attribut :
Attribut |
Rôle |
String[] value |
Préciser le ou les rôles utilisés lors du contrôle d'accès à l'EJB (obligatoire) |
Aucun client ne peut invoquer la méthode de l'EJB qui est marquée avec cette annotation.
L'annotation @DenyAll s'applique uniquement sur une méthode. Elle ne possède pas d'attribut.
L'annotation @PermitAll permet de préciser que la ou les méthodes de l'EJB n'ont aucune restriction d'accès.
L'annotation @PermitAll s'applique sur une classe ou une méthode. Elle ne possède pas d'attribut.
Cette annotation est l'annotation par défaut pour un EJB si aucune restriction d'accès n'est explicitement définie.
L'annotation @RolesAllowed permet de préciser les rôles qui seront autorisés à invoquer une ou plusieurs méthodes d'un EJB.
L'annotation @RolesAllowed s'applique sur une classe ou une méthode.
Appliquée à une classe, cette annotation définit les restrictions d'accès par défaut de toutes les méthodes de l'EJB
Appliquée à une méthode, cette annotation définit les restrictions d'accès pour la méthode en remplaçant les éventuelles restrictions par défaut.
Elle ne possède qu'un seul attribut :
Attribut |
Rôle |
String[] value |
Préciser le ou les rôles qui peuvent invoquer la ou les méthodes (obligatoire) |
L'annotation @RunAs permet de préciser le rôle sous lequel un EJB va être executé dans le conteneur indépendemment du rôle de l'utilisateur qui invoque l'EJB.
L'annotation @RunAs s'utilise sur la classe d'un EJB. Elle possède un seul attribut :
Attribut |
Rôle |
String value |
Préciser le rôle sous lequel l'EJB s'exécute (obligatoire) |
Cette annotation peut être utilisée sur un EJB de type Session ou Message Driven.
L'interface EJBContext propose des fonctionnalités relatives à la mise en oeuvre de la sécurité.
La méthode javax.security.Principal getCallerPrincipal() permet de connaître l'utilisateur qui invoque l'EJB.
L'interface javax.security.Principal encapsule l'utilisateur qui invoque un EJB. Sa méthode getName() permet de connaître le nom de l'utilisateur.
La méthode boolean isCallerInRole() renvoie un booléen qui vaut true si l'utilisateur qui invoque l'EJB possède le rôle founi en paramètre.
Lorsque cette méthode est utilisée, il faut utiliser l'annotation @DeclareRoles en lui précisant en paramètre les rôles qui sont utilisés avec la méthode isCallerInRole(). Autrement, il faut effectuer la déclaration équivalente dans le descripteur de déploiement. Ceci permet au conteneur de savoir que ces rôles sont utilisés par l'EJB.
L'utilisation de ces méthodes permet de mettre en oeuvre des fonctionnalités d'autorisations plus pointues que la simple vérification vis-à-vis d'un rôle.