Une introduction à la technologie EJB (2/3) 1 Les singletons des EJB 3.1 1.1 Synchronisation gérée manuellement Depuis la version 3.1, des EJB Statless à instance unique sont maintenant disponibles : ce sont des singletons. En voila un exemple : package services; import java.util.hashmap; import java.util.map; import javax.annotation.postconstruct; import javax.ejb.concurrencymanagement; import javax.ejb.concurrencymanagementtype; import javax.ejb.singleton; @Singleton(name = "storage") @Startup() @ConcurrencyManagement(ConcurrencyManagementType.BEAN) public class Storage { final Map<String, String> storage; public Storage() { storage = new HashMap<String, String>(); @PostConstruct public void init() { System.out.println("Storage running..."); /* Récupérer une valeur */ public String get(string key) { return storage.get(key); /* Déposer une valeur */ public void set(string key, String value) { storage.put(key, value); Ces nouveaux composants posent un problème de synchronisation car tous les clients vont utiliser la même instance. Dans notre exemple, l instance de HaspMap ne doit pas être utilisée de manière simultanée par plusieurs Threads. Utilisez OpenEJB (disponible ici 1 ) pour implanter cet EJB. Commencez par utiliser la clause synchronized pour régler ce problème. Tester cette implantation en réalisant une classe de test qui appelle votre EJB dans plusieurs threads. Vous pouvez vous inspirer de l exemple ci-dessous : 1. ress-ejb 1
@Test(timeout = 7000) public void testreadaccess() throws Exception { Runnable execution = new Runnable() { public void run() { monbean.mamethode(); ; Thread t1 = new Thread(execution); Thread t2 = new Thread(execution); Thread t3 = new Thread(execution); Thread t4 = new Thread(execution); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); Prévoyez de ralentir les méthodes get et set en utilisant la méthode sleep ci-dessous : private void sleep(int ms) { try { Thread.sleep(ms); catch (InterruptedException e) { 1.2 Synchronisation gérée par le conteneur Dans cette dernière version la synchronisation est trop forte car nous aurions pu traiter plusieurs appels simultanés à get. Pour ce faire nous allons enlever les clauses synchronized, changer l annotation ConcurrencyManagement pour indiquer que la synchronisatyion sera géree par le conteneur : @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) et ajouter des annotations de synchronisation offertes par les EJB 3.1 (javax.ejb.lock 2 ). Nos méthodes deviennent : /* Récupérer une valeur (utilisations simultanées) */ @Lock(LockType.READ) public String get(string key) {... /* Déposer une valeur (utilisation exclusive) */ @Lock(LockType.WRITE) public void set(string key, String value) {... 2. http ://docs.oracle.com/javaee/6/api/?javax/ejb/lock.html 2
Testez cette version en lançant plusieurs threads qui utilisent la méthode get. Utilisez l attribut timeout de l annotation @Test pour tester l exécution en parallèle des threads. Continuez en utilisant cette fois la méthode set. Terminez en créant un test qui mélange threads de lecture et threads d écriture. Comment s opère la synchronisation? 1.3 Lancement et dépendances Vous pouvez ajouter à votre classe l annotation javax.ejb.startup 3 : @Startup Pour forcer la création de ce singleton (sans appel d un éventuel client). Nous pouvons aussi organiser la création des singletons en utilisant l annotation javax.ejb.dependson 4 : @DependsOn("unAutreSingleton") qui indique qu un autre singleton doit être créé avant celui qui contient l annotation. Exercice : créer un singleton qui offre un service très simple et utilisez l annotation précédente pour forcer sa création avant celle de la classe Storage. 2 Les appels asynchrones Afin d éviter que des clients ne soient bloqués en attendant la fin d un appel à un EJB, nous pouvons maintenant, grace à l annotation javax.ejb.asynchronous 5, indiquer qu une méthode ou toutes les méthodes d un EJB doivent être de manière asynchrone : 3. http ://docs.oracle.com/javaee/6/api/?javax/ejb/startup.html 4. http ://docs.oracle.com/javaee/6/api/?javax/ejb/dependson.html 5. http ://docs.oracle.com/javaee/6/api/?javax/ejb/asynchronous.html 3
package services; import java.util.concurrent.future; import javax.annotation.postconstruct; import javax.annotation.resource; import javax.ejb.asyncresult; import javax.ejb.asynchronous; import javax.ejb.sessioncontext; import javax.ejb.stateless; @Stateless public class LongWorks { @PostConstruct public void prepare() { System.out.println("Preparation de " + this); @Asynchronous public void alongwork() { for (int i = 0; (i < 10); i++) { sleep(1000); private void sleep(int ms) { try { Thread.sleep(ms); catch (InterruptedException e) { Exercice : Créez un test unitaire afin d appeler la méthode alongwork. Vérifiez avec timeout que cet appel ne bloque pas le client. 2.1 Appels asynchrones avec résultats L exemple précédent est très simple car la méthode ne renvoie aucun résultat. Dans le cas contraire, nous allons utiliser l interface java.util.concurrent.future 6 pour transmettre le résultat et permettre au client de questionner l état d avancement de l appel asynchrone : @Asynchronous public Future<String> alongworkwithresult(string value) { sleep(5000); return new AsyncResult<String>(value); Exercice 1 : Utilisez la méthode get de l interface Future pour récupérer, chez le client, le résultat de plusieurs appels à la méthode alongworkwithresult. Exercice 2 : Testez, par attente active, la fin de l appel (méthode isdone de l interface Future ), puis récupérez le résultat (méthode get ). 6. http ://docs.oracle.com/javase/6/docs/api/?java/util/concurrent/future.html 4
2.2 Contrôler un appel asynchrone Un client est également capable de demander l arrêt d un traitement asynchrone (méthode cancel de l interface java.util.concurrent.future 7 ). La méthode métier va devoir, périodiquement, tester cette demande comme le montre l exemple ci-dessous : @Resource private SessionContext context; @Asynchronous public Future<String> alongbreakableworkwithresult(string value) { for (int i = 0; (i < 10); i++) { sleep(500); if (context.wascancelcalled()) { return null; return new AsyncResult<String>(value); Exercice 1 : Prévoyez un test unitaire qui va lancer un traitement asynchrone, attendre (un peu) son déroulement et finalement demander son interruption. Exercice 2 : Explorez les capacités de la classe javax.ejb.sessioncontext 8. 3 Sécuriser les accès aux EJB Il est possible de limiter l accès aux méthodes d un EJB à des utilisateurs qui auraient certains profils. En voici un exemple (librement inspiré des exemples présents sur le site d OpenEJB 9 ) : 7. http ://docs.oracle.com/javase/6/docs/api/?java/util/concurrent/future.html 8. http ://docs.oracle.com/javaee/6/api/?javax/ejb/sessioncontext.html 9. http://openejb.apache.org/examples-trunk/index.html 5
package services; import java.util.linkedlist; import java.util.list; import java.util.set; import javax.annotation.security.permitall; import javax.annotation.security.rolesallowed; import javax.ejb.stateful; @Stateful public class Names { Set<String> names = new java.util.hashset<>(); @RolesAllowed({ "Employee", "Manager" ) public void addname(string name) throws Exception { names.add(name); @RolesAllowed({ "Manager" ) public void deletename(string name) { names.remove(name); @PermitAll public List<String> getnames() { return new LinkedList<>(names); Exercice 1 : Vérifiez avec un test unitaire que la méthode getnames est accessible. Exercice 2 : Vérifiez que la méthode addname n est pas accessible (utilisez l attribut expected de l annotation org.junit.test 10 ). 3.1 Authentification par un proxy EJB Pour accéder aux méthodes protégées nous devons nous authentifier. La première solution consiste à passer par un EJB public qui va servir de porte d entrée vers les services authentifiés. En voila un exemple : package services; import javax.annotation.security.runas; import javax.ejb.stateless; @Stateless @RunAs("Manager") public class AsManager { public void deletename(names names, String name) { names.deletename(name); Exercice 1 : Vérifiez avec un test unitaire que la méthode deletename de l EJB AsManager est accessible à un client non authentifié. 10. http ://junit.sourceforge.net/javadoc/?org/junit/test.html 6
3.2 Authentification par identifiant et mot de passe La deuxième solution consiste à donner un identifiant et un mot de passe. Commencez par créer les deux fichiers users.properties accessible à partir du CLASSPATH : pierre=mot-de-passe-de-pierre paul=mot-de-passe-de-paul et groups.properties : Manager=pierre Employee=pierre,paul Le code du client va maintenant intégrer la fourniture du couple identifiant/mot de passe : Properties p = new Properties(); p.put(context.initial_context_factory, "org.apache.openejb.core.localinitialcontextfactory"); p.put(context.security_principal, "paul"); p.put(context.security_credentials, "mot-de-passe-de-paul"); Exercice 1 : Testez l accès authentifié aux méthodes. Faites un test pour le profil Employee et un autre pour le profil Manager. Exercice 2 : Dans votre EJB, interrogez l instance de l interface javax.ejb.sessioncontext 11 pour connaitre l utilisateur authentifié et testez son profil (méthode iscallerinrole ) ; 3.3 Authentification par programmation Testez le troisième exemple 12 d openejb pour mettre en place une authentification définie par programmation (API JAAS 13 ). 11. http ://docs.oracle.com/javaee/6/api/?javax/ejb/sessioncontext.html 12. http://openejb.apache.org/examples-trunk/testing-security-3/ 13. http://www.oracle.com/technetwork/java/javase/jaas/index.html 7