Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |
|||||||
Niveau : | Supérieur |
La versions 3.1 des EJB comme la version précédente permet le développement rapide d'objets métiers pour des applications distribuées, sécurisées, transactionnelles et portables.
Cette version apporte de nouvelles fonctionnalités (Les interfaces Local sont optionnelles pour les EJB Session, EJB Singleton, les invocations asynchrones, EJB Lite, packaging simplifié, ...) et un enrichissement de fonctionnalités existantes (Service Timer, noms JNDI portables, ...) qui permettent aux développeurs et aux architectes de répondre aux besoins de leurs applications.
Leur facilité de développement et les nouvelles fonctionnalités des EJB 3.1 leur permettent de devenir très intéressants même pour des applications de tailles moyennes voire petites.
Les EJB 3.1 sont issus des spécifications de la JSR 318.
Ce chapitre contient plusieurs sections :
Au moins une interface (locale ou distante) est requise pour les EJB Session 3.0
Les interfaces sont un excellent moyen pour limiter le couplage et assurer la testabilité : cependant dans certains cas, elles ne sont pas toujours nécessaires notamment si les deux points précédents ne sont pas une grande préoccupation.
Avec la version 3.1, il n'est plus nécessaire de définir une interface Local pour les EJB session : la classe de l'EJB session peut être directement annotée avec @Stateless ou @Stateful.
Rendre les interfaces pour les EJB session optionnelles permet à un EJB session d'être un simple POJO.
Exemple : |
@Stateless
public class MonEJBBean {
}
Les EJB Session n'ont plus l'obligation de définir explicitement une interface Local : le conteneur peut simplement utiliser le bean qui par défaut expose toutes les méthodes publiques de la classe et de ses classes mères. Un client peut obtenir une référence sur ce bean en utilisant l'injection de dépendance ou une recherche dans l'annuaire JNDI comme pour les interfaces Local ou Remote.
Contrairement aux interfaces Local et Remote avec lesquelles la référence obtenue est du type de son interface, c'est le type du bean qui est directement obtenu en tant que référence.
L'exemple ci-dessous définit un EJB session.
Exemple : |
package fr.jmdoudoux.dej.ejb31.domaine;
import javax.ejb.Stateless;
@Stateless
public class MonBean {
public String saluer() {
return "Bonjour";
}
}
Ce bean ne définit aucune interface particulière. Pour l'utiliser, par exemple dans une servlet, il suffit d'utiliser l'injection de dépendance avec l'annotation @EJB sur un objet du type de la classe d'implémentation de l'EJB.
Exemple : |
package fr.jmdoudoux.dej.ejb31.servlets;
import fr.jmdoudoux.dej.ejb31.domaine.MonBean;
import java.io.IOException;
import java.io.PrintWriter;
import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name="MaServlet", urlPatterns={"/MaServlet"})
public class MaServlet extends HttpServlet {
@EJB
private MonBean monBean;
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 MaServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>" + monBean.saluer() + "</h1>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
public String getServletInfo() {
return "Ma servlet de test";
}
}
Le fait d'utiliser le type de l'implémentation du bean comme référence impose quelques contraintes :
Toutes les méthodes publiques du bean et de ses classes mères sont exposées dans la vue no-interface. Ceci expose donc les méthodes de gestion du cycle de vie, ce qui peut ne pas être souhaité.
Seules les interfaces Local sont optionnelles : les interfaces Remote sont toujours obligatoires.
Il ne faut cependant pas abuser de cette fonctionnalité et la réserver à des cas d'applications simples : avec les IDE, le coût de création et de maintenance d'une interface sont négligeables et cela renforce le découplage.
Plusieurs fournisseurs de serveurs d'applications permettaient de n'avoir qu'une seule instance d'un EJB en offrant de préciser le nombre maximum d'instances à créer dans leur descripteur de déploiement. Cette solution n'est malheureusement pas portable puisque dépendante de l'implémentation du fournisseur.
La version 3.1 des EJB propose un nouveau type d'EJB Session nommé Singleton pour résoudre ce problème : il est possible de définir un EJB qui aura les caractéristiques du design pattern singleton : le conteneur garantit qu'une seule instance de cet EJB sera utilisable et partagée dans le conteneur.
C'est un nouveau composant qui ressemble à un EJB Session mais qui ne peut avoir qu'une seule instance dans un conteneur pour une application.
Un EJB singleton est utilisé principalement pour partager ou mettre en cache des données dans l'application. L'avantage des EJB Singleton c'est qu'ils offrent tous les services d'un EJB : sécurité, transaction, injection de dépendances, gestion du cycle de vie et intercepteurs, ...
Un EJB singleton se définit avec l'annotation @Singleton. Par défaut, toutes les méthodes d'un EJB Singleton sont thread-safe et transactionnelles.
Les EJB de type Singleton permettent d'ajouter de nouvelles fonctionnalités aux EJB :
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.LocalBean;
@Singleton
@LocalBean
public class MonCache {
private Map<String, Object> cache;
@PostConstruct
public void initialiser(){
this.cache = new HashMap<String, Object>();
}
public Object get(String cle){
return this.cache.get(cle);
}
public void put(String cle, Object valeur){
this.cache.put(cle, valeur);
}
public void clear(){
this.cache.clear();
}
}
Le conteneur garantit qu'une seule instance sera accessible à l'application : les accès à cette instance pourront être effectués par plusieurs threads.
Il est possible d'annoter certaines méthodes pour gérer le cycle de vie notamment en utilisant les annotations @PostConstruct et @PreDestroy. Ceci peut permettre de réaliser des opérations liées au cycle de vie de l'application : ces traitements étaient uniquement réalisables avant avec l'API Servlet grâce à un ServletContextListener.
L'annotation @Startup demande l'initialisation du Singleton au lancement de l'application. Cette annotation ne permet cependant pas de préciser un ordre de lancement.
Il est toutefois possible de définir un ordre de démarrage des EJB Singleton en utilisant l'annotation @DependsOn. Le conteneur garantira alors que les EJB dépendants sont démarrés avant l'EJB annoté.
Le cycle de vie d'un EJB Singleton est géré par le conteneur. Par défaut, c'est le conteneur qui décide de l'instanciation et de l'initialisation d'un EJB Singleton. L'annotation @Startup permet de demander au conteneur d'initialiser l'EJB à l'initialisation de l'application.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.DependsOn;
import javax.ejb.Singleton;
import javax.ejb.Startup;
@Singleton
@Startup
@DependsOn({"MonSecondBean"})
public class MonBean {
@PostConstruct
public void initialiser() {
System.out.println("Initialisation MonBean");
}
@PreDestroy
public void Detruire() {
System.out.println("Destruction MonBean");
}
}
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;
import javax.ejb.LocalBean;
@Singleton
@LocalBean
public class MonSecondBean {
@PostConstruct
public void initialiser() {
System.out.println("Initialisation MonSecondBean");
}
@PreDestroy
public void Detruire() {
System.out.println("Destruction MonSecondBean");
}
}
Durant l'arrêt de l'application, le conteneur va supprimer l'EJB après avoir éventuellement exécuté les méthodes marquées avec l'annotation @PreDestroy.
L'état de l'EJB est maintenu par le conteneur durant toute la durée de vie de l'application : cet état n'est pas persistant à l'arrêt de l'application ou de la JVM.
La gestion des accès concurrents peut utiliser deux stratégies :
La stratégie est précisée par l'annotation @ConcurrencyManagement qui peut prendre deux valeurs : ConcurrencyManagementType.CONTAINER ou ConcurrencyManagementType.BEAN.
La stratégie CMC répond à la plupart des besoins : elle utilise des métadonnées pour gérer les verrous. Chaque méthode possède un verrou de type read ou write précisé par une annotation.
Un verrou de type read indique que la méthode peut être accédée par plusieurs threads en simultané. Un verrou de type write indique que la méthode ne peut être accédée que par un seul thread : les invocations des autres threads sont mises en attente jusqu'à la fin de l'exécution de la méthode et réactivées une par une.
L'annotation @Lock permet de préciser le type de verrou à utiliser : elle attend en paramètre une valeur qui peut être LockType.READ ou LockType.WRITE.
Cette annotation peut être utilisée sur une classe, une interface ou une méthode. Appliquée sur une classe, cette annotation agit comme valeur par défaut pour toutes les méthodes de la classe sauf pour les méthodes qui sont annotées avec @Lock. Le type de verrou par défaut est write.
La stratégie BMC laisse au développeur le soin de gérer par programmation la gestion des accès concurrents en utilisant notamment les opérateurs synchronized et volatile ou en utilisant l'API contenue dans le package java.util.concurrent.
Par défaut, le temps d'attente d'un thread pour invoquer une méthode de l'EJB Singleton est infini. Il est possible de définir un timeout avec l'annotation @AccessTimeout qui permet de préciser un délai maximum d'attente en millisecondes. Si ce délai est atteint sans que l'invocation ne soit réalisée alors une exception de type ConcurrentAccessTimeoutException est levée.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import javax.ejb.AccessTimeout;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.DependsOn;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import javax.ejb.Startup;
@Singleton
@Startup
@DependsOn({"MonSecondBean"})
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@Lock(LockType.READ)
@AccessTimeout(15000)
public class MonBean {
...
}
La spécification ne prend pas en compte le clustering : elle n'apporte donc aucune précision sur le support des singletons dans un cluster et sauf implémentation particulière du serveur d'applications, il y aura une instance du singleton dans chaque JVM ou l'application est déployée.
Le conteneur doit maintenir actif un EJB Singleton durant la durée de vie de l'application même s'il lève une exception dans une de ses méthodes.
Le but d'EJB Lite est de proposer une version légère d'un conteneur d'EJB utilisable par exemple dans une application Java SE ou un conteneur web comme Tomcat.
L'utilisation des EJB est souvent associée avec des serveurs d'applications Java EE mais il existe des conteneurs d'EJB open source qui peuvent être embarqués comme OpenEJB, EasyBeans ou Embedded JBoss. Le concept est maintenant proposé en standard avec les EJB 3.1.
Le but d'EJB Lite est de permettre de standardiser un conteneur d'EJB embarquable et utilisable avec Java SE. Ceci doit permettre, par exemple, de réaliser des tests unitaires ou d'utiliser des EJB dans des applications desktop ou dans un conteneur web.
Une application web typique n'a pas forcément besoin des EJB de type MDB, des services Timer ou de l'appel distant d'EJB. La plupart des applications utilisent des EJB Session locaux, la persistance, l'injection et les transactions. EJB Lite tente d'offrir une solution en proposant une implémentation allégée.
Les EJB Lite sont un sous-ensemble de l'API EJB qui permet une utilisation des EJB locaux en dehors d'un conteneur EJB comme dans le Web Profile ou une application standalone. EJB Lite propose les fonctionnalités suivantes :
Les fonctionnalités non prises en charge dans les EJB Lite sont :
Le conteneur d'EJB emarqué propose donc un ensemble réduit de fonctionnalités qui permet à un client d'utiliser des EJB de type session sans avoir besoin d'un serveur d'applications Java EE.
Un conteneur embarqué doit au minimum supporter les API définies dans EJB Lite mais les fournisseurs peuvent ajouter à ce support tout ou partie des fonctionnalités des EJB 3.1.
Une API est proposée pour :
La classe EJBContainer permet une utilisation d'un conteneur d'EJB embarqué. Elle possède plusieurs méthodes :
Méthode |
Rôle |
static EJBContainer createEJBContainer() |
créer une nouvelle instance du conteneur et l'initialiser |
Context getContext() |
renvoyer un objet de type javax.naming.Context qui permet un accès à l'annuaire pour rechercher des ressources de type EJB Session |
void close() |
demander l'arrêt du conteneur |
Généralement, il suffit d'ajouter un ou plusieurs jar dans le classpath et d'utiliser l'API pour permettre la mise en oeuvre du conteneur EJB Lite.
Les usages possibles sont nombreux notamment intégrer le conteneur dans une application standalone ou web, faciliter l'exécution de tests, ...
L'exemple suivant utilise GlassFish V3 pour mettre en oeuvre un conteneur d'EJB embarqué dans une application standalone avec des tests unitaires de l'EJB.
L'exemple contient un EJB de type Session Stateless.
Exemple : |
package fr.jmdoudoux.dej.ejb.embedded.domaine;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Stateless;
@Stateless
public class MonBean {
private static Logger logger = Logger.getLogger(MonBean.class.getName());
public long ajouter(int a, int b) {
return a + b;
}
@PostConstruct
public void initialiser() {
logger.log(Level.INFO, "Initialisation instance de MonBean");
}
@PreDestroy
public void detruire() {
logger.log(Level.INFO, "Destruction instance de MonBean");
}
}
Pour compiler la classe, il faut ajouter deux bibliothèques au classpath du projet :
L'application crée une instance du conteneur d'EJB embarqué qui va rechercher et déployer les EJB contenus dans le classpath. Une instance du bean est obtenue à partir de son nom JNDI et utilisée pour invoquer la méthode ajouter().
Exemple : |
package fr.jmdoudoux.dej.ejb.embedded;
import fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.naming.NamingException;
public class Main {
public static void main(String[] args) {
EJBContainer container = EJBContainer.createEJBContainer();
Context context = container.getContext();
MonBean monBean;
try {
monBean = (MonBean) context.lookup("java:global/bin/MonBean");
System.out.println("3+2=" + monBean.ajouter(3, 2));
} catch (NamingException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
container.close();
}
}
Résultat : |
27 déc. 2009 22:57:58 com.sun.enterprise.v3.server.AppServerStartup run
INFO: GlassFish v3 (74.2) startup time : Embedded(2508ms) startup services(316ms) total(2824ms)
27 déc. 2009 22:57:58 org.glassfish.admin.mbeanserver.JMXStartupService$JMXConnectorsStarter
Thread run
INFO: JMXStartupService: JMXConnector system is disabled, skipping.
27 déc. 2009 22:57:58 com.sun.enterprise.transaction.JavaEETransactionManagerSimplified init
Delegates
INFO: Using com.sun.enterprise.transaction.jts.JavaEETransactionManagerJTSDelegate as the
delegate
27 déc. 2009 22:57:59 AppServerStartup run
INFO: [Thread[GlassFish Kernel Main Thread,5,main]] started
27 déc. 2009 22:57:59 com.sun.enterprise.security.SecurityLifecycle <init>
INFO: security.secmgroff
27 déc. 2009 22:57:59 com.sun.enterprise.security.SecurityLifecycle onInitialization
INFO: Security startup service called
27 déc. 2009 22:57:59 com.sun.enterprise.security.PolicyLoader loadPolicy
INFO: policy.loading
27 déc. 2009 22:57:59 com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm admin-realm of classtype com.sun.enterprise.security.auth.realm.
file.FileRealm successfully created.
27 déc. 2009 22:57:59 com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm file of classtype com.sun.enterprise.security.auth.realm.file.FileRealm
successfully created.
27 déc. 2009 22:57:59 com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm certificate of classtype com.sun.enterprise.security.auth.realm.
certificate.CertificateRealm successfully created.
27 déc. 2009 22:57:59 com.sun.enterprise.security.SecurityLifecycle onInitialization
INFO: Security service(s) started successfully....
27 déc. 2009 22:58:00 com.sun.ejb.containers.BaseContainer initializeHome
INFO: Portable JNDI names for EJB MonBean : [java:global/bin/MonBean,
java:global/bin/MonBean!fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean]
27 déc. 2009 22:58:00 fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean initialiser
INFO: Initialisation instance de MonBean
3+2=5
27 déc. 2009 22:58:00 fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean detruire
INFO: Destruction instance de MonBean
27 déc. 2009 22:58:00 org.glassfish.admin.mbeanserver.JMXStartupService shutdown
INFO: JMXStartupService and JMXConnectors have been shut down.
27 déc. 2009 22:58:00 com.sun.enterprise.v3.server.AppServerStartup stop
INFO: Shutdown procedure finished
27 déc. 2009 22:58:00 AppServerStartup run
INFO: [Thread[GlassFish Kernel Main Thread,5,main]] exiting
Une application Java SE peut utiliser un conteneur d'EJB embarqué qui s'exécute dans la même JVM et utilise le même classloader que celui de l'application.
Le test unitaire de l'EJB utilise également le conteneur d'EJB embarqué pour obtenir une instance de l'EJB et invoquer sa méthode ajouter() pour vérifier sa bonne exécution.
Exemple : |
package fr.jmdoudoux.dej.ejb.embedded.domaine;
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.naming.NamingException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class MonBeanTest {
private EJBContainer container;
private Context context;
private MonBean monBean;
@Before
public void setUp() throws NamingException {
container = EJBContainer.createEJBContainer();
context = container.getContext();
monBean = (MonBean) context.lookup("java:global/bin/MonBean");
}
@After
public void tearDown() {
container.close();
}
@Test
public void testAjouter() throws Exception {
int a = 3;
int b = 2;
long attendu = 5L;
long resultat = monBean.ajouter(a, b);
assertEquals("", attendu, resultat);
}
}
Résultat : |
27 déc. 2009 22:55:03 com.sun.enterprise.v3.server.AppServerStartup run
************************************************************************************
INFO: GlassFish v3 (74.2) startup time : Embedded(2711ms) startup services(331ms) total(3042ms)
27 déc. 2009 22:55:03 org.glassfish.admin.mbeanserver.JMXStartupService$JMXConnectorsStarter
Thread run
INFO: JMXStartupService: JMXConnector system is disabled, skipping.
27 déc. 2009 22:55:03 com.sun.enterprise.transaction.JavaEETransactionManagerSimplified init
Delegates
INFO: Using com.sun.enterprise.transaction.jts.JavaEETransactionManagerJTSDelegate as the
delegate
27 déc. 2009 22:55:03 AppServerStartup run
INFO: [Thread[GlassFish Kernel Main Thread,5,main]] started
27 déc. 2009 22:55:04 com.sun.enterprise.security.SecurityLifecycle <init>
INFO: security.secmgroff
27 déc. 2009 22:55:04 com.sun.enterprise.security.SecurityLifecycle onInitialization
INFO: Security startup service called
27 déc. 2009 22:55:04 com.sun.enterprise.security.PolicyLoader loadPolicy
INFO: policy.loading
27 déc. 2009 22:55:04 com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm admin-realm of classtype com.sun.enterprise.security.auth.
realm.file.FileRealm successfully created.
27 déc. 2009 22:55:04 com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm file of classtype com.sun.enterprise.security.auth.realm.file.FileRealm
successfully created.
27 déc. 2009 22:55:04 com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm certificate of classtype com.sun.enterprise.security.auth.
realm.certificate.CertificateRealm successfully created.
27 déc. 2009 22:55:04 com.sun.enterprise.security.SecurityLifecycle onInitialization
INFO: Security service(s) started successfully....
27 déc. 2009 22:55:04 com.sun.ejb.containers.BaseContainer initializeHome
INFO: Portable JNDI names for EJB MonBean : [java:global/bin/MonBean,
java:global/bin/MonBean!fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean]
27 déc. 2009 22:55:05 fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean initialiser
INFO: Initialisation instance de MonBean
27 déc. 2009 22:55:05 fr.jmdoudoux.dej.ejb.embedded.domaine.MonBean detruire
INFO: Destruction instance de MonBean
27 déc. 2009 22:55:05 org.glassfish.admin.mbeanserver.JMXStartupService shutdown
INFO: JMXStartupService and JMXConnectors have been shut down.
27 déc. 2009 22:55:05 com.sun.enterprise.v3.server.AppServerStartup stop
INFO: Shutdown procedure finished
27 déc. 2009 22:55:05 AppServerStartup run
INFO: [Thread[GlassFish Kernel Main Thread,5,main]] exiting
Le conteneur embarqué recherche les EJB à déployer dans le classpath :
L'environnement dans lequel un EJB s'exécute est transparent pour lui : le code de l'EJB est le même dans un conteneur embarqué et dans un serveur d'applications Java EE.
Avant leur version 3.1, les EJB devaient être packagés dans une archive de type jar dédiée. Comme une application d'entreprise est généralement composée d'une partie IHM sous la forme d'une webapp packagée dans une archive de type war, il fallait regrouper les archives war et jar dans une archive de type ear.
Le packaging des EJB avait été simplifié dans la version 3.0 en rendant le descripteur de déploiement optionnel. Cependant, le packaging devait toujours être fait de façon modulaire : un pour la partie web dans une archive de type war et un pour la partie EJB dans une archive de type jar, le tout regroupé dans une archive de type ear.
Ce packaging est intéressant pour rendre modulaire une grosse application mais il est complexe pour une simple application web qui utilise directement des services métiers et dont les composants n'ont pas besoin d'être partagés par plusieurs clients ou d'autres modules Java EE.
La version 3.1 propose de pouvoir intégrer les EJB directement dans la webapp sans avoir à créer un module dédié aux EJB. Les EJB qui sont des POJO annotés peuvent être mis directement dans le sous-répertoire WEB-INF/classes de la webapp et donc packagés directement dans l'archive de type war.
Si le descripteur de déploiement ejb-jar.xml doit être utilisé, il doit être placé dans le sous-répertoire WEB-INF avec le fichier web.xml.
Il est aussi possible d'ajouter un jar contenant les EJB dans le sous-répertoire WEB-INF/lib.
Une archive war ne peut contenir qu'un seul fichier ejb-jar.xml soit directement dans le sous-répertoire WEB-INF de la webapp soit dans le sous-répertoire META-INF d'une des archives jar contenues dans le sous-répertoire WEB-INF/lib
Comme les composants web et EJB sont packagés dans le même module, les ressources définies dans le war peuvent être partagées au travers de l'espace de nommage java:comp/env.
Ainsi, il est possible de définir une source de données dans le descripteur de déploiement web.xml et d'obtenir une référence par le contexte JNDI dans un EJB packagé dans le war.
Exemple : |
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
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/web-app_3_0.xsd">
<resource-ref>
<description>Ma source de données</description>
<res-ref-name>jdbc/bdd</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
</web-app>
Il suffit alors de définir la source de données dans le conteneur ou le serveur d'applications dans lequel le war est déployé et d'utiliser JNDI pour obtenir une référence sur cette source de données.
Exemple : |
package fr.jmdoudoux.dej.ejb31.domaine;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
@Stateless
public class MonBean {
public String saluer() {
Context ctx = null;
DataSource ds = null;
try {
ctx = new InitialContext();
ds = (DataSource) ctx.lookup("java:comp/env/jdbc/bdd");
Logger.getLogger(MonBean.class.getName()).log(Level.INFO,
"*****"+ds.getClass().getName());
// utilisation de la source de données
// ...
} catch (NamingException ex) {
Logger.getLogger(MonBean.class.getName()).log(Level.SEVERE, null, ex);
}
return "Bonjour";
}
}
Cette possibilité est intéressante pour intégrer des EJB dans des applications existantes.
Le nouveau modèle de déploiement pourra pleinement être utilisé avec la fonctionnalité EJB Lite qui peut être mise en oeuvre dans un simple conteneur web comme Tomcat ou Jetty. C'est d'ailleurs ce qui est proposé par le Web Profile.
Cette facilité de packaging favorise l'utilisation des EJB dans les petites et moyennes applications web.
Il est cependant recommandé de réserver ce type de packaging pour des applications simples et de conserver l'usage des archives de type ear pour des applications complexes.
Il est fréquent dans une application d'entreprise d'avoir besoin de fonctionnalités pilotées par des contraintes temporelles permettant leurs déclenchements de façons régulières ou périodiques.
La version 2.1 des EJB propose le service Timer qui permet l'invocation de callbacks dans un contexte transactionnel selon des contraintes temporelles spécifiées. Ce service présente cependant quelques limitations :
La version 3.1 des EJB enrichit le service Timer avec :
Le service EJB Timer du conteneur permet de planifier l'exécution de callbacks en spécifiant un temps, une période ou un intervalle.
Le service EJB Timer propose un support de la configuration de la planification d'un timer de deux façons :
L'annotation @Schedule met en oeuvre une expression de façon similaire à l'utilitaire cron sous Unix pour déclarer un timer qui va exécuter les traitements de la méthode qu'elle annote à chaque fois que le timer expire.
Un timer peut être défini soit automatiquement par le conteneur en utilisant des annotations ou le descripteur de déploiement soit par programmation. Les timers définis par déclaration sont automatiquement créés par le conteneur au déploiement de l'EJB.
L'annotation @Schedule s'utilise sur une méthode qui sera le callback invoqué à chaque fois que la contrainte temporelle est activée.
La méthode de callback invoquée lorsque le timeout d'un Timer est atteint peut être de deux types :
Pour définir le callback d'un timer par programmation, il y a deux solutions :
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.Timer;
import javax.annotation.Resource;
import javax.ejb.LocalBean;
import javax.ejb.ScheduleExpression;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Timeout;
import javax.ejb.TimerService;
@Singleton
@Startup
@LocalBean
public class TraitementsPeriodiques3 {
@Resource
TimerService timerService;
private DateFormat mediumDateFormat =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM);
@PostConstruct
public void creerTimer() {
Logger.getLogger(TraitementsPeriodiques3.class.getName()).log(Level.INFO,
"Creation du Timer");
ScheduleExpression scheduleExp =
new ScheduleExpression().second("*/10").minute("*").hour("*");
Timer timer = timerService.createCalendarTimer(scheduleExp);
}
@Timeout
public void executerTraitement(Timer timer) {
Logger.getLogger(TraitementsPeriodiques3.class.getName()).log(Level.INFO,
"Execution du traitement toutes les 10 secondes "+mediumDateFormat.format(new Date()));
}
}
Les méthodes annotées avec @Timeout ne peuvent pas lever d'exception.
L'interface TimedObject ne définit qu'une seule méthode ejbTimeout() qui attend en paramètre un objet de type Timer encapsulant l'invocation de la méthode de callback.
Dans ce cas, une seule méthode de callback peut être définie, celle de l'interface.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.Timer;
import javax.annotation.Resource;
import javax.ejb.LocalBean;
import javax.ejb.ScheduleExpression;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TimedObject;
import javax.ejb.Timeout;
import javax.ejb.TimerService;
@Singleton
@Startup
@LocalBean
public class TraitementsPeriodiques4 implements TimedObject {
@Resource
TimerService timerService;
private DateFormat mediumDateFormat =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM);
@PostConstruct
public void creerTimer() {
Logger.getLogger(TraitementsPeriodiques4.class.getName()).log(Level.INFO,
"Creation du Timer");
ScheduleExpression scheduleExp =
new ScheduleExpression().second("*/5").minute("*").hour("*");
Timer timer = timerService.createCalendarTimer(scheduleExp);
}
public void ejbTimeout(Timer timer) {
Logger.getLogger(TraitementsPeriodiques4.class.getName()).log(Level.INFO,
"Execution du traitement toutes les 5 secondes "+mediumDateFormat.format(new Date()));
}
}
Les méthodes de callbacks des Timers créés automatiquement sont soit annotées avec @Schedule ou @Schedules ou définies dans l'élément timeout-method du descripteur de déploiement.
Ces méthodes annotées de callbacks peuvent avoir deux signatures (où xxx est le nom de la méthode) :
Elles peuvent avoir n'importe quel modificateur d'accès mais ne peuvent pas être déclarées ni final ni static. Elles ne peuvent pas lever d'exception.
Comme le callback est interne à l'EJB, il ne possède aucun contexte de sécurité.
Le conteneur doit créer une nouvelle transaction si l'attribut de transaction est REQUIRED ou REQUIRED_NEW. Si la transaction échoue ou si elle est abandonnée, le conteneur doit retenter au moins une fois l'exécution du callback.
L'annotation @Schedule permet de créer un timer dont les caractéristiques sont fournies sous la forme d'attributs de l'annotation.
La syntaxe de déclaration se fait sous la forme d'une expression dont la syntaxe est inspirée de l'outil Unix cron. Cette expression peut utiliser huit attributs :
Attribut |
Valeurs possibles |
Exemple |
Hour |
0 à 23 (heure) |
hour = "23" |
Minute |
0 à 59 (minute) |
minute = "59" |
Second |
0 à 59 (seconde) |
second = "59" |
dayOfMonth |
1 à 31 : jour du mois last : dernier jour du mois -1 à -7 : nombre de jours avant la fin du mois {"1st", "2nd", "3rd", "4th", "5th", "Last"} {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} : identifier un jour précis dans le mois |
dayOfMonth = "1 dayOfMonth = "last" dayOfMonth = "-1" dayOfMonth = "1st Mon" |
dayOfWeek |
0 à 7 : jour de la semaine (0 et 7 représentent le dimanche) {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} |
dayOfWeek = "1" |
Month |
1 à 12 : le mois de l'année {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} : le mois selon 3 première lettres |
month = "1" month = "Jan" |
Year |
Une année sur 4 chiffres |
year = "2010" |
timezone |
La valeur fournie à chaque attribut peut prendre différentes formes :
Forme |
Description |
Exemple |
Une valeur simple |
Une valeur unique correspondant à une des valeurs possibles de l'attribut |
hour = "20" |
Une étoile |
Représente toutes les valeurs possibles de l'attribut |
dayOfMonth = "*" |
Une Liste |
Représente un ensemble de valeurs possibles pour l'attribut séparées par des virgules |
dayOfWeek = "Mon, Wed, Thu" |
Une plage |
Représente une plage de valeurs consécutives possibles pour l'attribut dont les deux bornes incluses sont séparées par un tiret |
year = "2010-2019" |
Une incrémentation |
Définie une expression de la forme x/y où la valeur est incrémentée de y dans la plage de valeurs possibles en commençant à la valeur x. Elle ne peut être appliquée que sur heure, minute et seconde. Une fois la valeur maximale atteinte, l'incrémentation s'arrête |
minute= "*/10" (toutes les 10 minutes) |
Les expressions possèdent des règles et des contraintes :
Voici quelques exemples :
Expression |
Description |
@Schedule(hour="6", dayOfMonth="1") |
Le premier de chaque mois à 6 heure du matin |
@Schedule(dayOfWeek="Mon-Fri", hour="22") |
Du lundi au vendredi à 10 heure du soir |
@Schedule(hour = "22", minute = "30", dayOfWeek = "Fri") |
Tous les vendredis à 22 heure 30 |
@Schedule(hour = "10, 14, 18", dayOfWeek = "Mon-Fri") |
Du lundi au vendredi à 10, 14 et 18 heure |
@Schedule(hour = "*", dayOfWeek = "1") |
Toutes les heures de chaque lundi |
@Schedule(hour = "23", dayOfMonth = "Last Fri", month="*") |
Le dernier vendredi de chaque mois à 23 heure |
@Schedule(hour = "22", dayOfMonth = "-3") |
Trois jours avant la fin de chaque mois à 22 heure |
@Schedule(minute = "*/15", hour = "12/1") |
Tous les quart d'heure à partir de midi |
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.LocalBean;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
@Stateless
@LocalBean
public class TraitementsPeriodiques2 {
DateFormat mediumDateFormat =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM);
@Schedule(dayOfWeek="Mon")
public void traiterHebdomadaires() {
Logger.getLogger(TraitementsPeriodiques2.class.getName()).log(Level.INFO,
"Execution du traitement hebdomadaire");
}
@Schedule(minute="*/1", hour="*")
public void traiterMinutes() {
Logger.getLogger(TraitementsPeriodiques2.class.getName()).log(Level.INFO,
"Execution du traitement chaque minute "+mediumDateFormat.format( new Date()));
}
@Schedule(second="*/30", minute="*", hour="*")
public void traiterTrenteSecondes() {
Logger.getLogger(TraitementsPeriodiques2.class.getName()).log(Level.INFO,
"Execution du traitement toutes les 30 secondes "+mediumDateFormat.format(new Date()));
}
}
Résultat : |
INFO: Execution du traitement chaque minute 31 janv. 2010 16:50:00
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:50:00
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:50:30
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:51:00
INFO: Execution du traitement chaque minute 31 janv. 2010 16:51:00
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:51:30
L'annotation @Schedule possède un attribut info qui permet de fournir une description du timer. Ces informations peuvent être retrouvées grâce à la méthode getInfo() de l'instance de type Timer.
Par déclaration, il est possible d'associer plusieurs timers à une même méthode de callback en utilisant l'annotation @Schedules qui agit comme un conteneur d'annotations @Schedule
Exemple : |
@Schedules(
{ @Schedule(hour="20", dayOfWeek="Mon-Thu"),
@Schedule(hour="18", dayOfWeek="Fri")
})
public void envoyerRapport() {
...
}
Un timer peut être persistant ou non : les timers non persistants ne survivent pas à un arrêt du conteneur.
La durée de vie d'un timer non persistant est liée à la durée de vie de la JVM qui l'a créé et dans laquelle il s'exécute : il est considéré comme supprimé en cas d'arrêt de l'application ou d'arrêt volontaire ou non de la JVM.
Les timers définis par déclaration sont par défaut persistants : le conteneur les réactive automatiquement en cas d'arrêt puis de relance.
Exemple : log de démarrage d'un serveur Glassfish v3 |
INFO: [TimerBeanContainer] Created TimerBeanContainer: TimerBean
INFO: Portable JNDI names for EJB TimerBean : [java:global/ejb-timer-service-app/TimerBean,
java:global/ejb-timer-service-app/TimerBean!com.sun.ejb.containers.TimerLocal]
INFO: EJB5109:EJB Timer Service started successfully for datasource [jdbc/__TimerPool]
INFO: ==> Restoring Timers ...
INFO: <== ... Timers Restored.
INFO: Loading application ejb-timer-service-app at /ejb-timer-service-app
Un timer non persistant peut être créé de deux façons :
L'interface javax.ejb.Timer propose des méthodes pour annuler un timer ou obtenir des informations sur lui.
Méthode |
Rôle |
void cancel() |
Demande la suppression du timer et de toutes ses notifications au conteneur |
long getTimeRemaining() |
Obtenir le nombre de millisecondes avant la prochaine notification d'expiration du Timer |
Date getNextTimeout() |
Obtenir la date/heure programmée de la prochaine notification d'expiration du Timer |
ScheduleExpression getSchedule() |
Obtenir l'objet qui définit l'expression de planification |
TimerHandle getHandle() |
Obtenir une version sérialisable du Timer |
Serializable getInfo() |
Obtenir les informations complémentaires fournies lors de la création du Timer |
boolean isPersistent() |
Déterminer si le Timer est persistant ou non |
boolean isCalendar() |
Déterminer si le Timer est basé sur un calendrier |
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.Timer;
import javax.annotation.Resource;
import javax.ejb.LocalBean;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TimedObject;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
@Singleton
@Startup
@LocalBean
public class TraitementsPeriodiques7 implements TimedObject {
@Resource
TimerService timerService;
private DateFormat mediumDateFormat =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
@PostConstruct
public void creerTimer() {
Logger.getLogger(TraitementsPeriodiques7.class.getName()).log(Level.INFO,
"Creation du Timer" + mediumDateFormat.format(new Date()));
TimerConfig config = new TimerConfig();
config.setInfo("donnees complementaires");
Timer timer = timerService.createSingleActionTimer(60000, config);
}
public void ejbTimeout(Timer timer) {
Logger.getLogger(TraitementsPeriodiques7.class.getName()).log(Level.INFO,
"Execution du traitement après 60s d'attente ("+timer.getInfo()+") "
+ mediumDateFormat.format(new Date()));
}
}
L'interface TimerService définit les méthodes pour permettre un accès au service Timer du conteneur. Elle a été enrichie pour permettre de définir des timers par programmation.
Pour obtenir une instance de type TimerService, il faut utiliser la méthode getTimerService() de l'interface EJBContext ou demander l'injection d'une ressource de type TimerService.
Grâce aux différentes surcharges de la méthode createTimer(), l'interface TimerService propose plusieurs méthodes pour créer des instances de type Timer qui sont déclenchées :
La méthode createSingleActionTimer() crée un Timer qui sera supprimé dès que son callback sera invoqué. Une version surchargée permet de préciser un délai d'attente.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.Timer;
import javax.annotation.Resource;
import javax.ejb.LocalBean;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TimedObject;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
@Singleton
@Startup
@LocalBean
public class TraitementsPeriodiques5 implements TimedObject {
@Resource
TimerService timerService;
private DateFormat mediumDateFormat =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM);
@PostConstruct
public void creerTimer() {
Logger.getLogger(TraitementsPeriodiques5.class.getName()).log(Level.INFO,
"Creation du Timer"+mediumDateFormat.format(new Date()));
Timer timer = timerService.createSingleActionTimer(60000, new TimerConfig());
}
public void ejbTimeout(Timer timer) {
Logger.getLogger(TraitementsPeriodiques5.class.getName()).log(Level.INFO,
"Execution du traitement après 60s d'attente "+mediumDateFormat.format(new Date()));
}
}
Une autre version surchargée permet de préciser une date/heure.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.Timer;
import javax.annotation.Resource;
import javax.ejb.LocalBean;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TimedObject;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
@Singleton
@Startup
@LocalBean
public class TraitementsPeriodiques6 implements TimedObject {
@Resource
TimerService timerService;
private DateFormat mediumDateFormat =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
@PostConstruct
public void creerTimer() {
Logger.getLogger(TraitementsPeriodiques6.class.getName()).log(Level.INFO,
"Creation du Timer" + mediumDateFormat.format(new Date()));
GregorianCalendar calend =
new GregorianCalendar(2010, GregorianCalendar.FEBRUARY, 7, 16, 45, 0);
Timer timer = timerService.createSingleActionTimer(calend.getTime(), new TimerConfig());
}
public void ejbTimeout(Timer timer) {
Logger.getLogger(TraitementsPeriodiques6.class.getName()).log(Level.INFO,
"Execution du traitement" + mediumDateFormat.format(new Date()));
}
}
La classe createCalendarTimer() permet de créer un Timer dont les conditions d'exécution son précisées par une instance de la classe ScheduleExpression fournie en paramètre.
La classe ScheduleExpression encapsule l'expression qui définit le déclenchement des traitements des Timers programmés par un calendrier.
La méthode getTimers() retourne une collection des Timers associés avec l'EJB. Il est ainsi possible d'accéder à chaque Timer pour obtenir des informations ou pour les effacer.
Tous les EJB de type Session sont enregistrés dans un annuaire avec un nom unique accessible par un client dans un contexte JNDI que ce soit en utilisant directement le contexte (hors du conteneur) ou en utilisant l'injection de dépendance (dans le conteneur).
Ce nom JNDI est automatiquement défini par le conteneur à l'enregistrement de chaque EJB, chaque fournisseur utilisant sa propre nomenclature puisque les spécifications leur en laisse la latitude.
Cela pose des problèmes de portabilité entre différents conteneurs. C'est encore plus gênant avec l'injection de dépendances puisque le conteneur doit être capable de déterminer le nom JNDI à partir des métadonnées de l'annotation @EJB.
Cette liberté laissée au fournisseur d'implémentations sur le nom JNDI sous lequel l'EJB est désigné limite la portabilité de l'application sur différents serveurs d'applications.
Ainsi, les EJB Session d'une même application déployée dans différents conteneurs se voient déployés avec un nom JNDI différents, ce qui va nécessairement être un problème lors des invocations par le ou les clients. Ceci va à l'encontre de la philosophie de Java EE.
La spécification standardise le nom global JNDI et deux autres espaces de nommages relatifs aux différentes portées d'une application Java EE.
La standardisation du nom JNDI par la spécification permet de définir clairement comment le nom global JNDI (global JNDI name) doit être défini, résolvant ainsi les problèmes de portabilité pour retrouver des références vers des composants ou des ressources.
Ce nom JNDI est composé de façon à le rendre unique dans une instance d'un conteneur en utilisant le préfixe java:global, le nom de l'application, le nom du module, le nom du bean et le nom de l'interface sous la forme.
java:global[/<application-name>]/<module-name>/<bean-name>!<interface-name>
Partie du nom |
Description |
Obligatoire |
application-name |
Nom de l'application dans lequel l'EJB est packagé. Par défaut c'est le nom de l'archive de type ear sans son extension sauf si le nom de l'application est précisée dans le descripteur de déploiement application.xml. |
Non |
module-name |
Nom du module dans lequel l'EJB est packagé. Par défaut, c'est le nom de l'archive de type jar ou war sans son extension sauf si le nom du module est précisé dans le fichier ejb-jar.xml par un élément module-name |
Oui |
bean-name |
Nom du bean Par défaut, c'est le nom de la classe d'implémentation de l'EJB sauf si le nom est précisé par l'attribut name de l'annotation @Stateless, @ Stateful et @Singleton ou par l'élément bean-name du descripteur de déploiement |
Oui |
interface-name |
Nom pleinement qualifié de l'interface sous laquelle l'EJB est exposé. Si l'EJB ne possède aucune interface (no interface view) alors c'est le nom pleinement qualifié de la classe d'implémentation qui est utilisé. |
Oui |
Le nom de l'application est optionnel car il n'est connu que si l'application est packagée dans une archive de type ear.
Le nom du module est déterminé à partir de l'archive jar ou war selon le format de l'archive dans laquelle l'EJB est packagé.
Le nom de l'interface n'est utile que si l'EJB implémente plusieurs interfaces (Locale et Remote) : il est inutile si l'EJB n'implémente qu'une seule interface ou aucune interface. Dans ce cas, le conteneur doit aussi associer l'EJB avec un nom JNDI court sous la forme :
java:global[/<application-name>]/<module-name>/<bean-name>
Le conteneur a aussi l'obligation d'enregistrer l'EJB dans deux autres espaces de nommages du contexte : java:app et java:module.
L'espace de nommage java:app concerne l'application. La syntaxe est la suivante :
java:app/<module-name>/<bean-name>[!<interface-name>]
L'espace de nommage java:module concerne le module. La syntaxe est la suivante :
java:module/<bean-name>[!<interface-name>]
Ceci devrait améliorer la portabilité des applications Java EE entre différents conteneurs.
L'invocation de traitements asynchrones est relativement fréquente dans les applications d'entreprises mais jusqu'à la version 3.0 incluse des EJB aucune solution standard n'était proposée pour ce besoin.
Comme les threads ne peuvent pas être utilisés dans les EJB, une façon couramment employée pour permettre une invocation asynchrone d'un EJB est de passer par un message JMS traité par un EJB de type MDB. Cependant, le rôle principal de JMS est l'échange de messages et pas l'invocation de fonctionnalités de façon asynchrone.
De plus, cette solution n'est pas idyllique car elle ne permet pas facilement d'avoir un retour à la fin des traitements réalisés.
La version 3.1 des EJB propose un support pour l'invocation asynchrone des EJB de type Session en utilisant l'annotation @Asynchronous sur la méthode de l'EJB qui contient les traitements.
Cette méthode peut retourner :
L'invocation asynchrone d'EJB de type Session peut être utilisée sur tous les types d'EJB Session et avec toutes les interfaces de ces EJB.
L'annotation @Asynchronous peut être utilisée sur une méthode, une classe ou une interface.
Si l'annotation @Asynchronous est utilisée sur des méthodes alors seules ces méthodes sont invocables de façon asynchrone.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
@Stateless
public class MailEJB {
@Asynchronous
public Future<Boolean> envoyerAsync() {
return new AsyncResult<Boolean>(true);
}
public Boolean envoyer() {
return true;
}
}
Si l'annotation @Asynchronous est utilisée sur la classe, toutes les méthodes exposées sont invocables de façon asynchrone.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
@Stateless
@Asynchronous
public class MailEJB {
public Future<Boolean> envoyer() {
return new AsyncResult<Boolean>(true);
}
public Future<Boolean> envoyerAvecCopie() {
return new AsyncResult<Boolean>(true);
}
}
Il est aussi possible d'utiliser l'annotation @Asynchronous sur une interface. Dans ce cas, les méthodes invocables de façon asynchrone seront celles précisées par l'interface puisque l'EJB sera invoqué au travers de son interface.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import javax.ejb.Local;
@Local
public interface MailEJBLocal {
Boolean envoyer();
}
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.concurrent.Future;
import javax.ejb.Asynchronous;
import javax.ejb.Remote;
@Remote
public interface MailEJBRemote {
@Asynchronous
Future<Boolean> envoyerAsync();
}
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Stateless;
@Stateless
public class MailEJB implements MailEJBRemote, MailEJBLocal {
public Future<Boolean> envoyerAsync() {
return new AsyncResult<Boolean>(true);
}
public Boolean envoyer() {
return true;
}
}
L'annotation @Asynchronous peut aussi être utilisée sur un EJB de type Singleton.
Lors de l'invocation de la méthode annotée avec @Asynchronous, le client poursuit l'exécution de ses traitements sans attendre la fin de l'exécution de l'invocation.
C'est le conteneur qui garantit que les traitements seront exécutés de façon asynchrone.
L'invocation asynchrone est faite par le client (EJB, application standalone, ...) de façon transparente pour lui.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.jws.WebMethod;
import javax.jws.WebService;
@Stateless
@WebService
public class CommandeEJB implements CommandeEJBLocal, CommandeEJBRemote {
@EJB
MailEJBLocal mailEJB;
@WebMethod
public void valider(int id) {
// traitement de validation de la commande
Logger.getLogger(CommandeEJB.class.getName()).log(Level.INFO,
"validation de la commande numero "+id);
// envoie d'un mail de prise en compte
mailEJB.envoyerAsync(id);
Logger.getLogger(CommandeEJB.class.getName()).log(Level.INFO,
"fin de la validation de la commande");
}
}
La classe java.util.concurrent.Future<T>, disponible depuis la version 5 de Java SE, permet d'avoir un contrôle sur l'invocation asynchrone d'un traitement. Elle est typée avec le type de la valeur de retour à l'issue de l'exécution des traitements.
L'interface Future<V> définit plusieurs méthodes :
Méthode |
Rôle |
boolean cancel(boolean) |
Demander une tentative d'annulation de l'exécution des traitements. Le conteneur va tenter d'annuler l'invocation si celle-ci n'a pas encore commencé. La méthode renvoie true si l'invocation a pu être annulée. Le paramètre permet de demander au conteneur d'informer le bean de la demande d'annulation si celui-ci est déjà en cours d'exécution |
V get() V get(long, TimeUnit) |
Renvoyer la valeur de retour des traitements Cette méthode possède deux surcharges :
|
boolean isCancelled() |
Préciser si l'exécution des traitements a été annulée |
boolean isDone() |
Préciser si l'exécution des traitements est terminée |
La classe javax.ejb.AsyncResult<V> est une implémentation fournie en standard de l'interface Future<V> qui propose notamment un constructeur attendant la valeur de retour de type V en paramètre.
La méthode wasCancelCalled() de l'interface SessionContext renvoie true si le client a invoqué la méthode Future.cancel() avec la valeur true en paramètre.
Exemple : |
package fr.jmdoudoux.dej.ejb31;
import java.util.Date;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
@Stateless
public class MailEJB implements MailEJBRemote, MailEJBLocal {
@Resource
SessionContext ctx;
@Asynchronous
public Future<Boolean> envoyerAsync(int valeur) {
boolean resultat = ( (valeur % 2) == 0);
Logger.getLogger(MailEJB.class.getName()).log(Level.INFO,
"debut de l'envoi du mail "+new Date());
long i = 0;
while ( i < 300000000 && !ctx.wasCancelCalled() ) {
// code des traitements a executer
i++;
}
if (ctx.wasCancelCalled()) {
resultat = false;
}
Logger.getLogger(MailEJB.class.getName()).log(Level.INFO,
"fin de l'envoi du mail "+new Date());
return new AsyncResult<Boolean>(resultat);
}
public Boolean envoyer() {
return true;
}
}
L'exemple ci-dessus effectue un traitement qui peut être interrompu par le client. La méthode envoyerAsync() renvoie un booléen qui indique le succès des traitements : elle renvoie false si l'id fournie est impaire ou si les traitements ont été interrompus par le client.
La méthode get() de l'interface Future peut lever une exception de type ExecutionException qui va encapsuler une éventuelle exception levée par la méthode exécutée de façon asynchrone. L'exception originale est chaînée et donc accessible en utilisant la méthode getCause().
Exemple : |
...
@Action
public void invocationOk() {
executerTraitement(2, false);
}
@Action
public void InvocationKo() {
executerTraitement(1, false);
}
@Action
public void invocationCancel() {
executerTraitement(2, true);
}
public void executerTraitement(int valeur, boolean arret) {
try {
Future<Boolean> future = bean.envoyerAsync(valeur);
if (arret) {
Thread.sleep(1000);
future.cancel(true);
}
Boolean resultat = future.get();
jTextArea1.setText("resultat="+resultat+
" isCancelled="+future.isCancelled());
Logger.getLogger(Main.class.getName()).log(Level.SEVERE,
"résultat="+resultat+ " isCancelled="+future.isCancelled());
} catch (InterruptedException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch(ExecutionException ee) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ee);
} catch (Exception e) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, e);
}
}
...
L'exemple ci-dessus est un extrait de code d'une application cliente Swing qui permet d'invoquer l'EJB de façon asynchrone et de tester les différents cas d'utilisation.
Le contexte de sécurité est utilisé comme lors de l'appel synchrone de la méthode.
Par contre, le contexte transactionnel n'est pas propagé à l'invocation asynchrone d'une méthode. Si la méthode invoquée est marquée avec l'attribut REQUIRES alors une nouvelle transaction est créée (comme si l'attribut REQUIRES_NEW avait été utilisé). Si la méthode invoquée est marquée avec l'attribut SUPPORT alors aucune transaction n'est utilisée. Si la méthode invoquée est marquée avec l'attribut MANDATORY alors une exception de type TransactionRequiredException est toujours levée.
Dans l'exemple ci-dessous, une application standalone va invoquer un EJB déployé dans un serveur d'applications GlassFish v3.
Exemple : |
package fr.jmdoudoux.dej.ebj31.client;
import fr.jmdoudoux.dej.ejb31.CommandeEJBRemote;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Main {
public static void main(String[] args) {
Context ctx = null;
CommandeEJBRemote bean = null;
try {
ctx = new InitialContext();
bean = (CommandeEJBRemote) ctx.lookup("fr.jmdoudoux.dej.ejb31.CommandeEJBRemote");
bean.valider(1234);
} catch (NamingException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
System.exit(1);
}
Logger.getLogger(Main.class.getName()).log(Level.INFO, "Fin de l'application");
}
}
Le nom JNDI de l'EJB est indiqué dans les logs au moment du déploiement de l'EJB dans le conteneur : il est impératif de prendre son interface Remote puisque le client ne s'exécute pas dans le contexte du serveur d'applications.
Il faut ajouter au classpath la bibliothèque qui contient l'interface de l'EJB et ajouter le fichier gf-client.jar contenu dans le sous-répertoire modules du répertoire d'installation de GlassFish v3.
La bibliothèque gf-client.jar contient les valeurs des paramètres par défaut pour permettre un accès à l'annuaire en utilisant JNDI.
Si une exception de type java.net.ConnectException est levée à l'exécution, il faut préciser le port sur lequel l'application peut contacter l'annuaire grâce à JNDI : le plus simple est de définir la propriété org.omg.CORBA.ORBInitialPort de la JVM
Résultat : |
-Dorg.omg.CORBA.ORBInitialPort=42382
La valeur à utiliser est contenue dans les logs de démarrage du serveur.
Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |