This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Réseaux et télécom Programmation Développement web Sécurité Système d’exploitation
customer 27921 at Fri Mar 11 19:07:20 +0100 2011
Propriété de Albiri Sigue
Java EE 6 et GlassFish 3 Antonio Goncalves
Traduit par Éric Jacoboni, avec la contribution technique de Éric Hébert
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Pearson Education France a apporté le plus grand soin à la réalisation de ce livre afin de vous fournir une information complète et fiable. Cependant, Pearson Education France n’assume de responsabilités, ni pour son utilisation, ni pour les contrefaçons de brevets ou atteintes aux droits de tierces personnes qui pourraient résulter de cette utilisation. Les exemples ou les programmes présents dans cet ouvrage sont fournis pour illustrer les descriptions théoriques. Ils ne sont en aucun cas destinés à une utilisation commerciale ou professionnelle. Pearson Education France ne pourra en aucun cas être tenu pour responsable des préjudices ou dommages de quelque nature que ce soit pouvant résulter de l’utilisation de ces exemples ou programmes. Tous les noms de produits ou marques cités dans ce livre sont des marques déposées par leurs propriétaires respectifs. Publié par Pearson Education France 47 bis, rue des Vinaigriers 75010 PARIS Tél. : 01 72 74 90 00 www.pearson.fr
Titre original : Beginning Java™ EE6 Platform with GlassFish™ 3
Traduction : Éric Jacoboni, avec la contribution de Éric Hébert
Aucune représentation ou reproduction, même partielle, autre que celles prévues à l’article L. 122-5 2˚ et 3˚ a) du code de la propriété intellectuelle ne peut être faite sans l’autorisation expresse de Pearson Education France ou, le cas échéant, sans le respect des modalités prévues à l’article L. 122-10 dudit code. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Table des matières Avant-propos ......................................................................................................................
XIII
À propos de l’auteur .........................................................................................................
Tour d’horizon de Java EE 6 ............................................................................................
5
Présentation de Java EE. .......................................................................................................
Un peu d’histoire............................................................................................................ Standards. ......................................................................................................................... Architecture. .................................................................................................................... Composants...................................................................................................................... Conteneurs. ...................................................................................................................... Services............................................................................................................................. Protocoles réseau............................................................................................................ Paquetages........................................................................................................................ Java Standard Edition....................................................................................................
6 6 9 9 10 11 12 14 15 16
Spécifications de Java EE 6 .................................................................................................
16
Nouveautés de Java EE 6......................................................................................................
Plus léger. ......................................................................................................................... Élagage.............................................................................................................................. Profils................................................................................................................................. Plus simple d’utilisation .............................................................................................. Plus riche.......................................................................................................................... Plus portable....................................................................................................................
Persistance en Java ..............................................................................................................
51
Résumé de la spécification JPA. ..........................................................................................
52 53 54 54
Historique de la spécification...................................................................................... Nouveautés de JPA 2.0.................................................................................................. Implémentation de référence ...................................................................................... Comprendre les entités..........................................................................................................
ORM = Object-Relational Mapping.......................................................................... Interrogation des entités................................................................................................ Méthodes de rappel et écouteurs ............................................................................... Récapitulatif............................................................................................................................
3
55 55 57 59
Écriture de l’entité Book............................................................................................... Écriture de la classe Main. ........................................................................................... Unité de persistance pour la classe Main. ................................................................ Compilation avec Maven.............................................................................................. Exécution de la classe Main avec Derby.................................................................. Écriture de la classe BookTest..................................................................................... Unité de persistance pour la classe BookTest ......................................................... Exécution de la classe BookTest avec Derby intégré ............................................
Association d’une entité........................................................................................................
73 76
Configuration par exception. ....................................................................................... Associations élémentaires.....................................................................................................
Tables................................................................................................................................. Clés primaires.................................................................................................................. Attributs............................................................................................................................ Types d’accès................................................................................................................... Collections de types de base........................................................................................ Association des types de base. ....................................................................................
77 78 80 85 92 95 97
Associations avec XML........................................................................................................
99
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Types d’accès d’une classe intégrable....................................................................... Correspondance des relations...............................................................................................
Relations dans les bases de données relationnelles. .............................................. Relations entre entités .................................................................................................. Chargement des relations. ............................................................................................ Tri des relations............................................................................................................... Traduction de l’héritage........................................................................................................
4
V
102 104 106 107 109 121 122
Stratégies d’héritage. ..................................................................................................... Type de classes dans une hiérarchie d’héritage. .....................................................
Gestion des objets persistants............................................................................................
141
Interrogation d’une entité......................................................................................................
142
Le gestionnaire d’entités.......................................................................................................
145 147 149 150 162
Obtenir un gestionnaire d’entités. .............................................................................. Contexte de persistance. ............................................................................................... Manipulation des entités............................................................................................... L’API de cache................................................................................................................ JPQL.........................................................................................................................................
Select.................................................................................................................................. From................................................................................................................................... Where................................................................................................................................. Order By. .......................................................................................................................... Group By et Having....................................................................................................... Suppressions multiples . ............................................................................................... Mises à jour multiples................................................................................................... Requêtes...................................................................................................................................
Introduction aux EJB.............................................................................................................
200 201 202 204 205 206 207
Types d’EJB..................................................................................................................... Anatomie d’un EJB . ..................................................................................................... Conteneur d’EJB. ........................................................................................................... Conteneur intégré........................................................................................................... Injection de dépendances et JNDI.............................................................................. Méthodes de rappel et intercepteurs.......................................................................... Tour d’horizon de la spécification EJB...............................................................................
L’entité Book.................................................................................................................... Le bean de session sans état BookEJB...................................................................... Unité de persistance pour le BookEJB...................................................................... La classe Main. ............................................................................................................... Compilation et assemblage avec Maven................................................................... Déploiement sur GlassFish.......................................................................................... Exécution de la classe Main avec Derby.................................................................. La classe BookEJBTest..................................................................................................
Beans de session et service timer.......................................................................................
225
Beans de session.....................................................................................................................
226 226 229 232 239 252
Beans sans état................................................................................................................ Beans avec état................................................................................................................ Singletons......................................................................................................................... Modèle des beans de session....................................................................................... Appels asynchrones. ......................................................................................................
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Le service timer......................................................................................................................
Expressions calendaires................................................................................................ Création automatique d’un timer................................................................................ Création d’un timer par programme .........................................................................
Méthodes de rappel et intercepteurs................................................................................
265
Cycles de vie des beans de session......................................................................................
266 266 267 269
Beans sans état et singletons........................................................................................ Beans avec état................................................................................................................ Méthodes de rappel........................................................................................................ Intercepteurs............................................................................................................................
9
Intercepteurs autour des appels................................................................................... Intercepteurs de méthode.............................................................................................. Intercepteur du cycle de vie......................................................................................... Chaînage et exclusion d’intercepteurs . ....................................................................
ACID.................................................................................................................................. Transactions locales....................................................................................................... XA et transactions distribuées..................................................................................... Support des transactions avec les EJB................................................................................
Transactions gérées par le conteneur ........................................................................ Transactions gérées par le bean. ................................................................................. Sécurité. ...................................................................................................................................
Principal et rôle............................................................................................................... Authentification et habilitation. .................................................................................. Gestion de la sécurité dans EJB...........................................................................................
10
288 289 296 298 299 300
Sécurité déclarative........................................................................................................ Sécurité par programmation. .......................................................................................
Introduction à JSF..................................................................................................................
310
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
VIII
Java EE 6 et GlassFish 3
FacesServlet et faces-config.xml................................................................................. Pages et composants...................................................................................................... Moteurs de rendu............................................................................................................ Convertisseurs et validateurs....................................................................................... Beans gérés et navigation............................................................................................. Support d’Ajax................................................................................................................
311 311 313 313 314 315
Résumé des spécifications de l’interface web ..................................................................
316 316 317 317 318 318
Bref historique des interfaces web............................................................................. JSP 2.2, EL 2.2 et JSTL 1.2......................................................................................... JSF 2.0............................................................................................................................... Nouveautés de JSF 2.0.................................................................................................. Implémentation de référence....................................................................................... Récapitulatif............................................................................................................................
11
L’entité Book.................................................................................................................... L’EJB BookEJB. ............................................................................................................. Le bean géré BookController. ..................................................................................... La page newBook.xhtml................................................................................................ La page listBooks.xhtml................................................................................................ Configuration avec web.xml......................................................................................... Compilation et assemblage avec Maven................................................................... Déploiement dans GlassFish. ...................................................................................... Exécution de l’application............................................................................................
Traitement et navigation.....................................................................................................
385
Le modèle MVC.....................................................................................................................
385 387 389 390
FacesServlet..................................................................................................................... FacesContext.................................................................................................................... Configuration de Faces. ................................................................................................ Beans gérés..............................................................................................................................
Écriture d’un bean géré................................................................................................. Modèle d’un bean géré.................................................................................................. Navigation........................................................................................................................ Gestion des messages.................................................................................................... Conversion et validation........................................................................................................
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
X
13
Java EE 6 et GlassFish 3
Envoi de messages.................................................................................................................
419
Présentation des messages....................................................................................................
420 420 421
JMS.................................................................................................................................... MDB.................................................................................................................................. Résumé de la spécification des messages . ........................................................................
Bref historique des messages. ..................................................................................... JMS 1.1............................................................................................................................. EJB 3.1.............................................................................................................................. Implémentation de référence.......................................................................................
421 422 422 422 423
Envoi et réception d’un message. ........................................................................................
Point à point..................................................................................................................... Publication-abonnement............................................................................................... API JMS. .......................................................................................................................... Sélecteurs.......................................................................................................................... Mécanismes de fiabilité ............................................................................................... MDB : Message-Driven Beans. ...........................................................................................
Création d’un MDB....................................................................................................... Le modèle des MDB...................................................................................................... MDB comme consommateur....................................................................................... MDB comme producteur.............................................................................................. Transactions..................................................................................................................... Gestion des exceptions.................................................................................................. Récapitulatif............................................................................................................................
14
446 447 447 452 453 455 456
OrderDTO. ....................................................................................................................... OrderSender..................................................................................................................... OrderMDB. ...................................................................................................................... Compilation et assemblage avec Maven................................................................... Création des objets administrés. ................................................................................. Déploiement du MDB dans GlassFish...................................................................... Exécution de l’exemple.................................................................................................
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Table des matières
XI
WSDL................................................................................................................................ SOAP................................................................................................................................. Protocole de transport.................................................................................................... XML. .................................................................................................................................
467 468 468 468
Résumé de la spécification des services web . ..................................................................
Bref historique des services web................................................................................ Spécifications Java EE. ................................................................................................. Implémentation de référence.......................................................................................
468 469 469 471
Appel d’un service web ........................................................................................................
471
JAXB : Java Architecture for XML Binding.....................................................................
473 476 478
Liaison............................................................................................................................... Annotations...................................................................................................................... La partie immergée de l’iceberg..........................................................................................
WSDL................................................................................................................................ SOAP................................................................................................................................. JAX-WS : Java API for XML-Based Web Services.........................................................
Le modèle JAX-WS....................................................................................................... Appel d’un service web ............................................................................................... Récapitulatif............................................................................................................................
481 481 484 485 486 494
La classe CreditCard..................................................................................................... Le service web CardValidator..................................................................................... Compilation et assemblage avec Maven................................................................... Déploiement dans GlassFish. ...................................................................................... Le consommateur du service web ............................................................................. Création des artefacts du consommateur et assemblage avec Maven............... Exécution de la classe Main. .......................................................................................
Spécification des services web REST.................................................................................
516
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
XII
Java EE 6 et GlassFish 3
Historique rapide de REST.......................................................................................... JAX-RS 1.1...................................................................................................................... Nouveautés de JAX-RS 1.1. ........................................................................................ Implémentation de référence.......................................................................................
Du Web aux services web . .......................................................................................... Pratique de la navigation sur le Web. ........................................................................ Interface uniforme.......................................................................................................... Accessibilité..................................................................................................................... Connectivité..................................................................................................................... Sans état............................................................................................................................ JAX-RS : Java API for RESTful Web Services.................................................................
Le modèle JAX-RS........................................................................................................ Écriture d’un service REST......................................................................................... Définition des URI......................................................................................................... Extraction des paramètres. ........................................................................................... Consommation et production des types de contenus . .......................................... Fournisseurs d’entités.................................................................................................... Méthodes ou interface uniforme................................................................................. Informations contextuelles........................................................................................... Gestion des exceptions.................................................................................................. Cycle de vie. .................................................................................................................... Récapitulatif............................................................................................................................
523 523 524 525 526 528 530 532 534 536 537
L’entité Book.................................................................................................................... Le service REST BookResource................................................................................. Configuration avec web.xml......................................................................................... Compilation et assemblage avec Maven................................................................... Déploiement dans GlassFish. ...................................................................................... Exécution de l’exemple.................................................................................................
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Avant-propos
Bien que Java EE 5 soit unanimement considérée comme la version la plus importante de Java EE, Java EE 6 fournit de nombreuses fonctionnalités supplémentaires. La création d’applications d’entreprise est, en effet, encore améliorée grâce à EJB 3.1, JPA 2.0, JAX-RS et JSF 2.0. La plate-forme Java EE est désormais arrivée à un niveau de maturité permettant de faire côtoyer légèreté et puissance. Vous pourriez évidemment utiliser votre navigateur favori pour parcourir les nombreux blogs, wikis et articles consacrés à Java EE 6, mais je vous conseille plutôt de commencer par lire ce livre : il est concis, pragmatique et il distille l’expérience complète de l’auteur sur le sujet. Cet ouvrage utilise GlassFish comme serveur d’applications sous-jacent pour plusieurs raisons : GlassFish V3 est l’implémentation de référence et est donc en phase avec Java EE 6. Par ailleurs, l’expérience acquise en utilisant une implémentation de référence et les technologies les plus récentes est adaptable au monde de l’entreprise. Ce que vous apprendrez sera directement exploitable en production. Antonio Goncalves est un exemple rare de développeur où se mêlent passion pour Java et expérience professionnelle de Java EE. Son travail de consultant, combiné à sa participation au Java User Group de Paris ainsi, bien sûr, que son rôle dans les différents groupes d’experts Java EE 6 en font l’auteur idéal de Java EE 6 et GlassFish 3. Après la lecture de ce livre, vous aurez compris que la plus grande richesse de Java EE n’est pas la somme de ses fonctionnalités mais la communauté qui l'a créé, ainsi que sa nature même de standard qui vous permet de choisir ou de modifier l'implémentation en fonction de vos besoins. La liberté n'est pas simplement représentée par l'open-source, mais également par les standards ouverts. Alexis Moussine-Pouchkine Équipe GlassFish, Sun Microsystems.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
À propos de l’auteur
Antonio Goncalves est un architecte logiciel ; il vit à Paris. S’intéressant au développement en Java depuis la fin des années 1990, il a travaillé dans diverses sociétés de différents pays, pour lesquelles il intervient désormais en tant que consultant en architecture logicielle Java EE. Son expérience lui a permis d’acquérir une grande connaissance des serveurs d’applications comme WebLogic, JBoss et, bien sûr, GlassFish. C’est un ardent défenseur des logiciels open-source – il est membre de l’OSSGTP (Open Source Get Together Paris). Il est également l’un des initiateurs et des animateurs du Paris Java User Group. Antonio a écrit son premier livre sur Java EE 5 en 2007. Depuis, il a rejoint le JCP et est l’un des experts de plusieurs JSR (Java EE 6, JPA 2.0 et EJB 3.1). Au cours de ces dernières années, Antonio est intervenu dans plusieurs conférences internationales consacrées essentiellement à Java EE – notamment JavaOne, The Server Side Symposium, Devoxx et Jazoon. Il a également publié de nombreux articles techniques, aussi bien pour des sites web (DevX) que pour des magazines spécialisés (Programmez, Linux Magazine). Antonio possède le diplôme d’ingénieur en informatique du CNAM (Conservatoire national des arts et métiers) de Paris et une maîtrise en conception orientée objet de l’université de Brighton.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Remerciements
Écrire un livre sur une nouvelle spécification comme Java EE 6 est une tâche énorme qui mobilise les talents de plusieurs personnes. Avant tout, je remercie Steve Anglin, d’Apress, de m’avoir donné l’opportunité de participer à la collection "Beginning Series" d’Apress, que j’apprécie beaucoup en tant que lecteur. Tout au long de mon travail de rédaction, j’ai été en contact avec Candace English et Tom Welsh, qui ont relu l’ouvrage et m’ont rassuré lorsque j’avais des doutes sur le fait de pouvoir le terminer en temps voulu. Je remercie également les relecteurs techniques, Jim Farley et Sumit Pal, qui ont fait un excellent travail en me suggérant de nombreuses améliorations. Enfin, j’ai admiré le travail remarquable d’Ami Knox, qui a produit la dernière version de l’édition. Merci également à Alexis Midon et Sebastien Auvray, les coauteurs du Chapitre 15, consacré aux services web REST : Alex est un informaticien passionné, fan de REST, et Sébastien est un développeur talentueux qui utilise REST de façon pragmatique. Merci à eux pour leur aide précieuse. Je remercie tout spécialement Alexis Moussine-Pouchkine, qui a gentiment accepté d’écrire la préface et la section sur GlassFish. Il m’a également été d’un grand secours pour contacter les bonnes personnes pour des sujets particuliers : je pense à Ryan Lubke pour JSF 2.0, à Paul Sandoz pour JAX-RS 1.1 et à François Orsini pour Derby. Merci à Damien Gouyette pour son aide sur JSF 2.0. Damien a une grande expérience en développement web et en JSF en particulier (au fait, merci à celui qui gère le dépôt SVN). Merci également à Arnaud Héritier pour avoir écrit la section sur Maven et pour m’avoir aidé à résoudre certains problèmes avec Maven, ainsi qu’à Nicolas de Loof pour sa relecture technique sur le sujet. Sébastien Moreno m’a aidé pour JUnit et a relu le manuscrit complet avec David Dewalle et Pascal Graffion – tout cela dans un délai imparti très serré. Je les remercie beaucoup pour leur travail. Je remercie les correcteurs, Denise Green et Stefano Costa, pour avoir tenté de donner une touche shakespearienne à ce livre.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
XVI
Java EE 6 et GlassFish 3
Les diagrammes de cet ouvrage ont été réalisés avec l’extension Visual Paradigm d’IntelliJ IDEA. Je remercie d’ailleurs Visual Paradigm et JetBrains pour m’avoir offert une licence gratuite de leurs excellents produits. Je n’aurais pas pu écrire ce livre sans l’aide de la communauté Java : tous ceux qui ont pris sur leur temps libre pour m’aider dans leurs courriers électroniques, dans les listes de diffusion ou sur les forums de discussion. La liste de diffusion des groupes d’experts JCP est, évidemment, la première qui me vient à l’esprit : merci à Roberto Chinnici, Bill Shannon, Kenneth Saks, Linda DeMichiel, Michael Keith, Reza Rahman, Adam Bien, etc. Un gros bisou à ma fille, Éloïse : les interruptions dues à son espièglerie m’ont aidé alors que je passais mes week-ends à écrire. Un livre est le produit d’un nombre incalculable de personnes que je voudrais remercier pour leur contribution, que ce soit pour un avis technique, une bière dans un bar, un extrait de code... Merci donc à Jean-Louis Dewez, Frédéric Drouet, les geeks du JUG de Paris, T. Express, les membres d’OSSGTP, les Cast Codeurs, FIP, Marion, Les Connards, Vitalizen, La Fontaine, Ago, Laure, La Grille, les Eeckman, Yaya, Rita, os Navalhas, La Commune Libre d’Aligre, etc. Merci à tous !
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Introduction
Aujourd’hui, les applications doivent accéder à des données, appliquer une logique métier, ajouter des couches de présentation et communiquer avec des systèmes externes. Les entreprises tentent de réaliser toutes ces opérations à moindre coût, en se servant de technologies standard et robustes supportant des charges importantes. Si vous êtes dans cette situation, ce livre est fait pour vous. Java Enterprise Edition est apparu à la fin des années 1990 et a doté le langage Java d’une plate-forme logicielle fiable, prête pour les besoins des entreprises. Critiqué à chaque nouvelle version, mal compris ou mal utilisé, en compétition avec les frameworks open-source, J2EE a été considéré comme une technologie lourde. Java EE a bénéficié de toutes ces critiques pour s’améliorer : son but actuel est la simplicité. Si vous faites partie de ceux qui pensent "que les EJB sont nuls et qu’il faut s’en débarrasser", lisez ce livre et vous changerez d’avis. Les EJB (Enterprise Java Beans), comme toutes les technologies de Java EE, sont des composants puissants. Si, au contraire, vous êtes un fan de Java EE, cet ouvrage vous montrera que la plate-forme a trouvé son équilibre grâce à une simplification du développement, à de nouvelles spécifications, à un modèle de composants EJB plus légers, à des profils et à l’élagage. Si vous débutez avec Java EE, ce livre vous guidera dans votre apprentissage des spécifications les plus importantes au moyen d’exemples et de diagrammes très simples à comprendre. Les standards ouverts sont l’une des forces principales de Java EE. Plus que jamais, les applications écrites avec JPA, EJB, JSF, JMS, les services web SOAP ou REST sont portables entre les différents serveurs d’applications. Comme vous pourrez le constater, la plupart des implémentations de référence de Java EE 6 (GlassFish, EclipseLink, Mojarra, OpenMQ, Metro et Jersey) sont distribuées sous les termes d’une licence open-source. Ce livre explore les innovations de cette nouvelle version et examine les différentes spécifications et la façon de les assembler pour développer des applications. Java
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
2
Java EE 6 et GlassFish 3
EE 6 est formé d’environ 30 spécifications et c’est une version importante pour la couche métier (EJB 3.1, JPA 2.0), la couche web (Servlet 3.0, JSF 2.0) et l’interopérabilité (services web SOAP et REST). Ce livre présente une grande partie de toutes ces spécifications, utilise le JDK 1.6 et certains patrons de conception bien connus, ainsi que le serveur d’applications GlassFish, la base de données Derby, JUnit et Maven. Il est abondamment illustré de diagrammes UML, de code Java et de copies d’écran.
Structure du livre Ce livre ne se veut pas une référence exhaustive de Java EE 6. Il s’intéresse essentiellement aux spécifications les plus importantes et aux nouvelles fonctionnalités de cette version. Sa structure respecte le découpage de l’architecture d’une application. Présentation Chapitre 10 : JavaServer Faces Chapitre 11 : Pages et composants Chapitre 12 : Traitements et navigation
Logique métier Chapitre 6 : Enterprise Java Beans Chapitre 7 : Beans de session et service timer Chapitre 8 : Méthodes de rappel et intercepteurs Chapitre 9 : Transactions et sécurité
Interopérabilité Chapitre 13 : Envoi de messages Chapitre 14 : Services web SOAP Chapitre 15 : Services web REST
Persistance Chapitre 2 : Persistance en Java Chapitre 3 : ORM : ObjectRelational Mapping Chapitre 4 : Gestion des objets persistants Chapitre 5 : Méthodes de rappel et écouteurs
Le Chapitre 1 présente brièvement l’essentiel de Java EE 6 et les outils que nous utiliserons dans ce livre (JDK, Maven, JUnit, Derby et GlassFish). La couche de persistance est décrite du Chapitre 2 au Chapitre 5 et s’intéresse principalement à JPA 2.0. Après un survol général et quelques exemples au Chapitre 2, le Chapitre 3 s’intéresse à l’association objet-relationnel (ORM). Le Chapitre 4 montre comment gérer et interroger les entités, tandis que le 5 présente leur cycle de vie, les méthodes de rappel et les écouteurs.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Introduction
3
Pour développer une couche métier transactionnelle avec Java EE 6, on utilise naturellement les EJB, qui seront décrits du Chapitre 6 au Chapitre 9. Le Chapitre 6 passe en revue la spécification, son histoire et donne des exemples de code, tandis que le 7 s’intéresse plus particulièrement aux beans de session et à leur modèle de programmation, ainsi qu’au nouveau service timer. Le Chapitre 8 est consacré au cycle de vie des EJB et aux intercepteurs, tandis que le 9 explique tout ce qui a trait aux transactions et à la sécurité. Du Chapitre 10 au Chapitre 12, vous apprendrez à développer une couche de présentation avec JSF 2.0. Après une présentation de la spécification au Chapitre 10, le Chapitre 11 s’intéresse à la construction d’une page web avec JSF et Facelets. Le 12, quant à lui, explique comment interagir avec un "backend" EJB et comment naviguer entre les pages. Enfin, les derniers chapitres vous présenteront différents moyens d’échanger des informations avec d’autres systèmes. Le Chapitre 13 montre comment échanger des messages asynchrones avec JMS (Java Message Service) et les MDB (Message-Driven Beans) ; le Chapitre 14 s’intéresse aux services web SOAP tandis que le 15 est consacré aux services web REST.
Téléchargement et exécution du code Les exemples de ce livre sont conçus pour être compilés avec le JDK 1.6, déployés sur le serveur d’applications GlassFish V3 et stockés dans la base de données Derby. Le Chapitre 1 explique comment installer tous ces logiciels et chaque chapitre montre comment compiler, déployer, exécuter et tester les composants selon la technologie employée. Tous les codes des exemples ont été testés sur la plate-forme Windows, mais ni sur Linux ni sur OS X. Les codes sources de ces exemples sont disponibles sur le site des éditions Pearson (www.pearson.fr), sur la page consacrée à cet ouvrage.
Contacter l’auteur Pour toute question sur le contenu de ce livre, sur le code ou tout autre sujet, contactez-moi à l’adresse [email protected]. Vous pouvez également visiter mon site web, http://www. antoniogoncalves.org.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
1 Tour d’horizon de Java EE 6 De nos jours, les entreprises évoluent dans une compétition à l’échelle mondiale. Elles ont besoin pour résoudre leurs besoins métiers d’applications qui deviennent de plus en plus complexes. À notre époque de mondialisation, les sociétés sont présentes sur les différents continents, fonctionnent 24 heures sur 24, 7 jours sur 7 via Internet et dans de nombreux pays ; leurs systèmes doivent être internationalisés et savoir traiter plusieurs monnaies et des fuseaux horaires différents – tout ceci en réduisant les coûts, en améliorant le temps de réponse des services, en stockant les données sur des supports fiables et sécurisés et en offrant différentes interfaces graphiques à leurs clients, employés et fournisseurs. La plupart des sociétés doivent combiner ces défis innovants avec leur système d’information existant tout en développant en même temps des applications B2B (business to business) pour communiquer avec leurs partenaires. Il n’est pas rare non plus qu’une société doive coordonner des données stockées à divers endroits, traitées par plusieurs langages de programmation et acheminées via des protocoles différents. Évidemment, ceci ne doit pas faire perdre d’argent, ce qui signifie qu’il faut empêcher les pannes du système et toujours rester disponible, sécurisé et évolutif. Les applications d’entreprise doivent faire face aux modifications et à la complexité tout en étant robustes. C’est précisément pour relever ces défis qu’a été créé Java Enterprise Edition (Java EE). La première version de Java EE (connue sous le nom de J2EE) se concentrait sur les problèmes que devaient résoudre les sociétés en 1999 : les composants distribués. Depuis, les logiciels ont dû s’adapter à de nouvelles solutions techniques comme les services web SOAP ou REST. La plate-forme a donc évolué pour tenir compte de ces besoins en proposant plusieurs mécanismes standard sous forme de spécifications. Au cours des années, Java EE a évolué pour devenir plus riche, plus léger, plus simple d’utilisation et plus portable.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
6
Java EE 6 et GlassFish 3
Ce chapitre fait un tour d’horizon de Java EE. Après une présentation rapide de son architecture interne, il présentera les nouveautés de Java EE 6. La seconde partie du chapitre est consacrée à la mise en place de votre environnement de développement pour que vous puissiez vous-même mettre en œuvre les extraits de code présentés dans ce livre.
Présentation de Java EE Lorsque l’on veut traiter une collection d’objets, on ne commence pas par développer sa propre table de hachage : on utilise l’API des collections. De même, lorsque l’on a besoin d’une application transactionnelle, sécurisée, interopérable et distribuée, on ne développe pas des API de bas niveau : on utilise la version entreprise de Java (Java EE). Tout comme l’édition standard de Java (Java SE, Standard Edition) permet de traiter les collections, Java EE fournit des moyens standard pour traiter les transactions via Java Transaction API (JTA), les messages via Java Message Service (JMS) ou la persistance via Java Persistence API (JPA). Java EE est un ensemble de spécifications pour les applications d’entreprise ; il peut donc être considéré comme une extension de Java SE destinée à faciliter le développement d’applications distribuées, robustes, puissantes et à haute disponibilité. Java EE 6 est une version importante. Non seulement elle marche dans les pas de Java EE 5 pour fournir un modèle de développement simplifié, mais elle ajoute également de nouvelles spécifications et apporte des profils et de l’élagage pour alléger ce modèle. La sortie de Java EE 6 coïncide avec le dixième anniversaire de la plate-forme entreprise : elle combine donc les avantages du langage Java avec l’expérience accumulée au cours de ces dix dernières années. En outre, elle tire profit du dynamisme de la communauté open-source et de la rigueur du JCP. Désormais, Java EE est une plate-forme bien documentée, avec des développeurs expérimentés, une communauté d’utilisateurs importante et de nombreuses applications déployées sur les serveurs d’entreprises. C’est un ensemble d’API permettant de construire des applications multi-tier reposant sur des composants logiciels standard ; ces composants sont déployés dans différents conteneurs offrant un ensemble de services. Un peu d’histoire
Dix ans permettent de se faire une idée de l’évolution de Java EE (voir Figure 1.1), qui s’est d’abord appelé J2EE. La première version, J2EE 1.2, a été initialement développée par Sun et est apparue en 1999 sous la forme d’une spécification contenant dix
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6
7
Java Specification Requests (JSR). À cette époque, on parlait beaucoup de CORBA : c’est la raison pour laquelle J2EE 1.2 a été créé en ayant à l’esprit la création de systèmes distribués. Les Enterprise Java Beans (EJB) introduits alors permettaient de manipuler des objets service distants avec ou sans état et, éventuellement, des objets persistants (beans entités). Ils étaient construits selon un modèle distribué et transactionnel utilisant le protocole sous-jacent RMI-IIOP (Remote Method InvocationInternet Inter-ORB Protocol). La couche web utilisait les servlets et les JavaServer Pages (JSP) et les messages étaient envoyés via JMS. Figure 1.1
Profil Web EoD
Historique de J2EE/Java EE.
Facilité de développement Services web
Application d'entreprise
Robuste, évolutif
Java EE 5
J2EE 1.4
Élagage Conteneur intégrable JAX-RS Validation des beans
J2EE 1.3
J2EE 1.2
Project JPE
Mai 1998
Servlet JSP EJB JMS RMI/IIOP
Dec 1999 10 specs
EJB CMP JCA
Sept 2001 13 specs
Services web Gestion Déploiement
Nov 2003 20 specs
Java EE 6
Annotations Injection JPA WS-* JSF
Mai 2006 23 specs
Profil web
Q3 2009 28 specs
INFO CORBA est apparu vers 1988, précisément parce que les systèmes d’entreprise commençaient à être distribués (Tuxedo, CICS, par exemple). Les EJB puis J2EE lui ont emboîté le pas, mais dix ans plus tard. Lorsque J2EE est apparu pour la première fois, CORBA avait déjà gagné son statut industriel, mais les sociétés commençaient à utiliser des solutions "tout-Java" et l’approche neutre de CORBA vis-à-vis des langages de programmation est donc devenu redondante.
À partir de J2EE 1.3, la spécification a été développée par le Java Community Process (JCP) en réponse à la JSR 58. Le support des beans entité est devenu obligatoire et les EJB ont introduit les descripteurs de déploiement XML pour stocker les métadonnées (qui étaient jusqu’alors sérialisées dans un fichier avec EJB 1.0). Cette version a également réglé le problème du surcoût induit par le passage des paramètres par valeur avec les interfaces distantes en introduisant les interfaces locales
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
8
Java EE 6 et GlassFish 3
et en passant les paramètres par référence. J2EE Connector Architecture (JCA) a été ajoutée afin de connecter J2EE aux EIS (Enterprise Information Systems). INFO JCP est une organisation ouverte créée en 1998 afin de définir les évolutions de la plateforme Java. Lorsqu’il identifie le besoin d’un nouveau composant ou d’une nouvelle API, l’initiateur (appelé "leader de la spécification") crée une JSR et forme un groupe d’experts. Ce groupe, formé de représentants de diverses sociétés ou organisations, ainsi que de personnes privées, est responsable du développement de la JSR et doit délivrer : 1) une spécification qui explique les détails et définit les bases de la JSR ; 2) une implémentation de référence (RI, Reference Implementation) ; et 3) un kit de test de compatibilité (TCK, Technology Compatibility Kit), c’est-à-dire un ensemble de tests que devront satisfaire toutes les implémentations avant de pouvoir prétendre qu’elles sont conformes à la spécification. Une fois qu’elle a été acceptée par le comité exécutif (EC, Executive Committee), la spécification est fournie à la communauté pour être implémentée. En réalité, Java EE est une JSR qui chapeaute d’autres JSR et c’est la raison pour laquelle on parle souvent de JSR umbrella.
J2EE 1.4 (JSR 151), qui est sorti en 2003, a ajouté vingt spécifications et le support des services web. EJB 2.1 permettait ainsi d’invoquer des beans de session à partir de SOAP/HTTP. Un service de temporisation a également été ajouté pour permettre aux EJB d’être appelés à des moments précis ou à intervalles donnés. Cette version fournissait un meilleur support pour l’assemblage et le déploiement des applications. Bien que ses supporters lui aient prédit un grand avenir, toutes les promesses de J2EE ne se sont pas réalisées. Les systèmes créés grâce à lui étaient trop complexes et les temps de développement, souvent sans commune mesure avec les exigences de l’utilisateur. J2EE était donc considéré comme un modèle lourd, difficile à tester, à déployer et à exécuter. C’est à cette époque que des frameworks comme Struts, Spring ou Hibernate ont commencé à émerger et à proposer une nouvelle approche dans le développement des applications. Heureusement, Java EE 5 (JSR 244) fit son apparition au deuxième trimestre de 2006 et améliora considérablement la situation. Il s’inspirait des frameworks open-source en revenant à un bon vieil objet Java (POJO, Plain Old Java Object). Les métadonnées pouvaient désormais être définies grâce à des annotations et les descripteurs XML devenaient facultatifs. Du point de vue des développeurs, EJB 3 et la nouvelle spécification JPA représentèrent donc plus un bond prodigieux qu’une évolution de la plate-forme. JSF (JavaServer Faces) fit également son apparition comme framework standard de la couche présentation et JAX-WS 2.0 remplaça JAX-RPC comme API pour les services web SOAP.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6
9
Aujourd’hui, Java EE 6 (JSR 316) poursuit sur cette voie en appliquant les concepts d’annotation, de programmation POJO et la politique "convention plutôt que configuration" à toute la plate-forme, y compris la couche web. Il fournit également un grand nombre d’innovations comme la toute nouvelle API JAX-RS 1.1, simplifie des API matures comme EJB 3.1 et en enrichit d’autres comme JPA 2.0 ou le service de temporisation. Les thèmes principaux de Java EE 6 sont la portabilité (en standardisant le nommage JNDI, par exemple), la dépréciation de certaines spécifications (via l’élagage) et la création de sous-ensembles de la plate-forme au moyen de profils. Dans ce livre, nous présenterons toutes ces améliorations et montrerons comment Java Enterprise Edition est devenu à la fois bien plus simple et bien plus riche. Standards
Java EE repose sur des standards. C’est une spécification centrale qui chapeaute un certain nombre d’autres JSR. Vous pourriez vous demander pourquoi les standards sont si importants puisque certains des frameworks Java les plus utilisés (Struts, Spring, etc.) ne sont pas standardisés. La raison est que les standards, depuis l’aube des temps, facilitent la communication et les échanges – des exemples de standards bien connus concernent les langues, la monnaie, le temps, les outils, les trains, les unités de mesure, l’électricité, le téléphone, les protocoles réseau et les langages de programmation. Quand Java est apparu, le développement d’une application web ou d’entreprise passait généralement par l’utilisation d’outils propriétaires : on créait son propre framework ou l’on s’enfermait en choisissant un framework commercial propriétaire. Puis vint l’époque des frameworks open-source, qui ne reposent pas toujours sur des standards ouverts. Vous pouvez donc utiliser une solution open-source qui vous enferme dans une seule implémentation ou en choisir une qui implémente les standards et qui sera alors portable. Java EE fournit des standards ouverts implémentés par plusieurs frameworks commerciaux (WebLogic, Websphere, MQSeries, etc.) ou open-source (GlassFish, JBoss, Hibernate, Open JPA, Jersey, etc.) pour gérer les transactions, la sécurité, les objets à état, la persistance des objets, etc. Aujourd’hui plus que jamais dans l’histoire de Java EE, votre application peut être déployée sur n’importe quel serveur d’applications conforme, moyennant quelques modifications mineures. Architecture
Java EE est un ensemble de spécifications implémentées par différents conteneurs. Ces conteneurs sont des environnements d’exécution Java EE qui fournissent cer-
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
10
Java EE 6 et GlassFish 3
tains services aux composants qu’ils hébergent : gestion du cycle de vie, injection de dépendances, etc. Les composants doivent respecter des contrats bien définis pour communiquer avec l’infrastructure de Java EE et avec les autres composants, et ils doivent être assemblés en respectant un certain standard (fichiers archives) avant d’être déployés. Java EE étant un surensemble de la plate-forme Java SE, les API de cette dernière peuvent donc être utilisées par n’importe quel composant de Java EE. La Figure 1.2 présente les relations logiques qui relient les conteneurs. Les flèches représentent les protocoles utilisés par un conteneur pour accéder à un autre. Le conteneur web, par exemple, héberge les servlets qui peuvent accéder aux EJB via le protocole RMI-IIOP. Figure 1.2 Conteneurs standard de Java EE.
Conteneur EJB
Conteneur web Servlet
RMI / IIOP
JSF
HTTP SSL
HTTP
SSL
EJB
RMI / IIOP RMI / IIOP
Conteneur Applets
Conteneur d'applications client
Applet
Application
Composants
L’environnement d’exécution de Java EE définit quatre types de composants que doivent supporter toutes les implémentations : ■■
Les applets sont des applications graphiques exécutées dans un navigateur web. Elles utilisent l’API Swing pour fournir des interfaces utilisateurs puissantes.
■■
Les applications sont des programmes exécutés sur un client. Il s’agit le plus souvent d’interfaces graphiques ou de programmes non interactifs qui ont accès à toutes les fonctionnalités de la couche métier de Java EE.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 11
■■
Les applications web (composées de servlets, de filtres de servlet, d’écouteurs d’événements web, de pages JSP et de JSF) s’exécutent dans un conteneur web et répondent aux requêtes HTTP envoyées par les clients web. Les servlets permettent également de mettre en place des services web SOAP et REST.
■■
Les EJB (Enterprise Java Beans) sont des composants permettant de traiter la logique métier en modèle transactionnel. On peut y accéder localement et à distance via RMI (ou HTTP pour les services web SOAP et REST).
Conteneurs
L’infrastructure de Java EE est découpée en domaines logiques appelés conteneurs (voir Figure 1.2). Chacun d’eux joue un rôle spécifique, supporte un ensemble d’API et offre des services à ses composants (sécurité, accès aux bases de données, gestion des transactions, injection de ressources, etc.). Les conteneurs cachent les aspects techniques et améliorent la portabilité. Selon le type d’application que vous voudrez construire, vous devrez comprendre les possibilités et les contraintes de chaque conteneur. Si, par exemple, vous devez développer une couche de présentation web, vous écrirez une application JSF et la déploierez dans un conteneur web, non dans un conteneur EJB. Si, en revanche, vous voulez qu’une application web appelle une couche métier, vous devrez sûrement utiliser à la fois un conteneur web et un conteneur EJB. La plupart des navigateurs web fournissent des conteneurs d’applets pour exécuter les composants applets. Lorsque vous développez des applets, vous pouvez donc vous concentrer sur l’aspect visuel de l’application puisque le conteneur vous fournit un environnement sécurisé grâce à un modèle de protection appelé "bac à sable" : le code qui s’exécute dans ce bac à sable n’est pas autorisé à en sortir, ce qui signifie que le conteneur empêchera un code téléchargé sur votre machine locale d’accéder aux ressources de votre système, comme les processus ou les fichiers. Le conteneur d’applications client (ACC, application client container) contient un ensemble de classes et de bibliothèques Java ainsi que d’autres fichiers afin d’ajouter l’injection, la gestion de la sécurité et le service de nommage aux applications Java SE (applications Swing, traitements non interactifs ou, simplement, une classe avec une méthode main()). ACC communique avec le conteneur EJB en utilisant le protocole RMI-IIOP et avec le conteneur web via le protocole HTTP (pour les services web, notamment).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
12
Java EE 6 et GlassFish 3
Le conteneur web (ou conteneur de servlets) fournit les services sous-jacents permettant de gérer et d’exécuter les composants web (servlets, JSP, filtres, écouteurs, pages JSF et services web). Il est responsable de l’instanciation, de l’initialisation et de l’appel des servlets et du support des protocoles HTTP et HTTPS. C’est lui qu’on utilise pour servir les pages web aux navigateurs des clients. Le conteneur EJB est responsable de la gestion de l’exécution des beans entreprise contenant la couche métier de votre application Java EE. Il crée de nouvelles instances des EJB, gère leur cycle de vie et fournit des services comme les transactions, la sécurité, la concurrence, la distribution, le nommage ou les appels asynchrones. Services
Les conteneurs fournissent les services sous-jacents à leurs composants. En tant que développeur, vous pouvez donc vous concentrer sur l’implémentation de la logique métier au lieu de résoudre les problèmes techniques auxquels sont exposées les applications d’entreprise. La Figure 1.3 montre les services fournis par chaque conteneur. Conteneur web JSP
JPA Gestion Métadonnées WS Services web JMS JAXR JAX-WS JAX-RPC
Conteneur Applets
JDBC JDBC Base de données
SAAJ
Figure 1.3
Java SE
Services fournis par les conteneurs.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 13
Les conteneurs web et EJB, par exemple, fournissent des connecteurs pour accéder à EIS, alors que le conteneur d’applet ou ACC ne le font pas. Java EE offre les services suivants : ■■
JTA. Ce service offre une API de démarcation des transactions utilisée par le conteneur et l’application. Il sert également d’interface entre le gestionnaire de transactions et un gestionnaire de ressource au niveau SPI (Service Provider Interface).
■■
JPA. Fournit l’API pour la correspondance modèle objet-modèle relationnel (ORM, Object-Relational Mapping). JPQL (Java Persistence Query Language) permet d’interroger les objets stockés dans la base de données sous-jacente.
■■
JMS. Permet aux composants de communiquer de façon asynchrone par passage de messages. Ce service fournit un système d’envoi de message fiable, point à point (P2P) ainsi que le modèle publication-abonnement.
■■
JNDI (Java Naming and Directory Interface). Cette API, incluse dans Java SE, permet d’accéder aux systèmes d’annuaires et de nommage. Votre application peut l’utiliser pour associer des noms à des objets puis les retrouver dans un annuaire. Avec elle, vous pouvez parcourir des sources de données, des fabriques JMS, des EJB et d’autres ressources. Omniprésente dans le code jusqu’à J2EE 1.4, JNDI est désormais utilisée de façon plus transparente grâce à l’injection.
■■
JavaMail. Le but de cette API consiste à simplifier l’envoi de courrier élec tronique par les applications.
■■
JAF (JavaBeans Activation Framework). Cette API, incluse dans Java SE, est un framework pour la gestion des données des différents types MIME. Elle est utilisée par JavaMail.
■■
XML. La plupart des composants Java EE peuvent éventuellement être déployés à l’aide de descripteurs de déploiements XML, et les applications doivent souvent manipuler des documents XML. JAXP (Java API for XML Processing) permet d’analyser des documents XML à l’aide des API SAX et DOM, ainsi que pour XSLT. StAX (Streaming API for XML) est une API d’analyse XML par flux.
■■
JCA. Les connecteurs permettent d’accéder à EIS à partir d’un composant Java EE, que ce soient des bases de données, des mainframes ou des programmes ERP (Enterprise Resource Planning).
■■
Sécurité. JAAS (Java Authentication and Authorization Service) fournit les services permettant d’authentifier les utilisateurs et d’assurer le contrôle de leurs
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
14
Java EE 6 et GlassFish 3
droits d’accès. JACC (Java Authorization Service Provider Contract for Containers) définit un contrat entre un serveur d’application Java EE et un fournisseur de service d’autorisation, ce qui permet de greffer des fournisseurs de services d’autorisation personnalisés dans n’importe quel produit Java EE. ■■
Services web. Java EE reconnaît les services web SOAP et REST. JAX-WS (Java API for XML Web Services) remplace JAX-RPC (Java API for XML-based RPC) et fournit le support des protocoles SOAP et HTTP. JAX-RS (Java API for RESTful Web Services) fournit le support des services web reposant sur REST.
■■
Gestion. Java EE définit des API pour gérer les conteneurs et les serveurs à l’aide d’un bean d’entreprise spécialisé. L’API JMX (Java Management Extensions) fournit également une aide à la gestion.
■■
Déploiement. La spécification de déploiement Java EE définit un contrat entre les outils de déploiement et les produits Java EE afin de standardiser le déploiement des applications.
Protocoles réseau
Comme le montre la Figure 1.3 (voir la section "Platform Overview" de la spécification Java EE 6), les composants déployés dans les conteneurs peuvent être invoqués via différents protocoles. Une servlet déployée dans un conteneur web, par exemple, peut être appelée avec HTTP ou comme un service web avec un point terminal EJB déployé dans un conteneur EJB. Voici la liste des protocoles reconnus par Java EE : ■■
HTTP. HTTP est le protocole du Web, omniprésent dans les applications modernes. L’API côté client est définie par le paquetage java.net de Java SE. L’API côté serveur de HTTP est définie par les servlets, les JSP et les interfaces JSF, ainsi que par les services web SOAP et REST. HTTPS est une combinaison de HTTP et du protocole SSL (Secure Sockets Layer).
■■
RMI-IIOP. RMI (Remote Method Invocation) permet d’appeler des objets distants indépendamment du protocole sous-jacent – avec Java SE, le protocole natif est JRMP (Java Remote Method Protocol). RMI-IIOP est une extension de RMI permettant de l’intégrer à CORBA. IDL (Java interface description language) permet aux composants des applications Java EE d’invoquer des objets CORBA externes à l’aide du protocole IIOP. Les objets CORBA peuvent avoir été écrits dans différents langages (Ada, C, C++, Cobol, etc.) et, bien sûr, en Java.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 15
Paquetages
Pour être déployés dans un conteneur, les composants doivent d’abord être empaquetés dans une archive au format standard. Java SE définit les fichiers jar (Java Archive), qui permettent de regrouper plusieurs fichiers (classes Java, descripteurs de déploiement ou bibliothèques externes) dans un seul fichier compressé (reposant sur le format ZIP). Java EE définit différents types de modules ayant leur propre format de paquetage reposant sur ce format jar commun. Un module d’application client contient des classes Java et d’autres fichiers de ressources empaquetés dans un fichier jar. Ce fichier peut être exécuté dans un environnement Java SE ou dans un conteneur d’application client. Comme tous les autres formats d’archive, le fichier jar contient éventuellement un répertoire META-INF décrivant l’archive. Le fichier META-INF/MANIFEST.MF, notamment, permet de décrire les données du paquetage. S’il est déployé dans un ACC, le descripteur de déploiement peut éventuellement se trouver dans le fichier META-INF/application-client.xml. Un module EJB contient un ou plusieurs beans de session et/ou des beans pilotés par des messages (MDB, Message Driven Bean) assemblés dans un fichier jar (souvent appelé fichier jar EJB). Ce fichier peut éventuellement contenir un descripteur de déploiement META-INF/ejb-jar.xml et ne peut être déployé que dans un conteneur EJB. Un module d’application web contient des servlets, des JSP, des pages JSF, des services web ainsi que tout autre fichier web associé (pages HTML et XHTML, feuilles de style CSS, scripts JavaScript, images, vidéos, etc.). Depuis Java EE 6, un module d’application web peut également contenir des beans EJB Lite (un sousensemble de l’API des EJB que nous décrirons au Chapitre 6). Tous ces composants sont assemblés dans un fichier jar portant l’extension .war (souvent désigné sous le terme de fichier war, ou Web Archive). L’éventuel descripteur de déploiement est défini dans le fichier WEB-INF/web.xml. Si le fichier war contient des beans EJB Lite, l’archive peut également contenir un descripteur de déploiement décrit par WEBINF/ejb-jar.xml. Les fichiers .class Java se trouvent dans le répertoire WEB-INF/ classes et les fichiers jar dépendants sont dans le répertoire WEB-INF/lib. Un module entreprise peut contenir zéro ou plusieurs modules d’applications web, zéro ou plusieurs modules EJB et d’autres bibliothèques classiques ou externes. Toutes ces composantes sont assemblées dans une archive entreprise (un fichier jar portant l’extension .ear) afin que le déploiement de ces différents modules se fasse simultanément et de façon cohérente. Le descripteur de déploiement facultatif d’un module entreprise est défini dans le fichier META-INF/application.xml. Le répertoire spécial lib sert à partager les bibliothèques entre les modules.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
16
Java EE 6 et GlassFish 3
Java Standard Edition
Il est important de bien comprendre que Java EE est un surensemble de Java SE (Java Standard Edition). Ceci signifie donc que toutes les fonctionnalités du langage Java et toutes ses API sont également disponibles dans Java EE. Java SE 6 est apparu officiellement le 11 décembre 2006. Cette version a été développée sous le contrôle de la JSR 270 ; elle apporte de nombreuses fonctionnalités supplémentaires et poursuit l’effort de simplification de la programmation initié par Java SE 5 (autoboxing, annotations, généricité, énumérations, etc.). Java SE 6 fournit de nouveaux outils de diagnostic, de gestion et de surveillance des applications ; il améliore l’API JMX et simplifie l’exécution des langages de scripts dans la machine virtuelle Java (JVM, Java Virtual Machine). Le but de cet ouvrage n’est pas de présenter Java SE 6 : consultez l’abondante documentation disponible sur le langage si vous pensez ne pas assez le maîtriser. Un bon point de départ est le livre de Dirk Louis et Peter Müller, Java SE 6 (Pearson, 2007).
Spécifications de Java EE 6 Java EE 6 est une spécification centrale définie par la JSR 316 qui contient vingthuit autres spécifications. Un serveur d’application souhaitant être compatible avec Java EE 6 doit donc implémenter toutes ces spécifications. Les Tableaux 1.1 à 1.5 énumèrent toutes leurs versions et leurs numéros de JSR. Certaines spécifications ont été élaguées, ce qui signifie qu’elles seront peut-être supprimées de Java EE 7. Tableau 1.1 : Spécification de Java Enterprise Edition
Spécification
Version
JSR
URL
Java EE
6.0
316
http://jcp.org/en/jsr/detail?id=316
Tableau 1.2 : Spécifications des services web
Spécification
Version
JSR
URL
JAX-RPC
1.1
101
http://jcp.org/en/jsr/detail?id=101
JAX-WS
2.2
224
http://jcp.org/en/jsr/detail?id=224
Élaguée X
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 17
Tableau 1.2 : Spécifications des services web (suite)
Spécification
Version
JSR
URL
JAXB
2.2
222
http://jcp.org/en/jsr/detail?id=222
JAXM
1.0
67
http://jcp.org/en/jsr/detail?id=67
StAX
1.0
173
http://jcp.org/en/jsr/detail?id=173
Services web
1.2
109
http://jcp.org/en/jsr/detail?id=109
Métadonnées des services web
1.1
181
http://jcp.org/en/jsr/detail?id=181
JAX-RS
1.0
311
http://jcp.org/en/jsr/detail?id=311
JAXR
1.1
93
http://jcp.org/en/jsr/detail?id=93
Élaguée
X
Tableau 1.3 : Spécifications web
Spécification
Version JSR
URL
JSF
2.0
314
http://jcp.org/en/jsr/detail?id=314
JSP
2.2
245
http://jcp.org/en/jsr/detail?id=245
JSTL (JavaServer 1.2 Pages Standard Tag Library)
52
http://jcp.org/en/jsr/detail?id=52
Servlet
3.0
315
http://jcp.org/en/jsr/detail?id=315
Expression Language 1.2
245
http://jcp.org/en/jsr/detail?id=245
Élaguée
Tableau 1.4 : Spécification Entreprise
Spécification
Version JSR
URL
EJB
3.1
318
http://jcp.org/en/jsr/detail?id=318
JAF
1.1
925
http://jcp.org/en/jsr/detail?id=925
JavaMail
1.4
919
http://jcp.org/en/jsr/detail?id=919
JCA
1.6
322
http://jcp.org/en/jsr/detail?id=322
JMS
1.1
914
http://jcp.org/en/jsr/detail?id=914
JPA
2.0
317
http://jcp.org/en/jsr/detail?id=317
JTA
1.1
907
http://jcp.org/en/jsr/detail?id=907
Élaguée
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
18
Java EE 6 et GlassFish 3
Tableau 1.5 : Gestion, sécurité et autres spécifications
Spécification
Version JSR
URL
Élaguée
JACC
1.1
115
http://jcp.org/en/jsr/detail?id=115
Validation Bean
1.0
303
http://jcp.org/en/jsr/detail?id=303
Annotations communes
1.0
250
http://jcp.org/en/jsr/detail?id=250
Déploiement d’applications Java EE
1.2
88
http://jcp.org/en/jsr/detail?id=88
X
Gestion Java EE
1.1
77
http://jcp.org/en/jsr/detail?id=77
X
Interface de 1.0 fournisseur de service d’authentification pour les conteneurs
196
http://jcp.org/en/jsr/detail?id=196
Support du débogage 1.0 pour les autres langages
45
http://jcp.org/en/jsr/detail?id=45
Nouveautés de Java EE 6 Maintenant que vous connaissez l’architecture interne de Java EE, vous pourriez vous demander ce qu’apporte Java EE 6. Le but principal de cette version est de poursuivre la simplification de la programmation introduite par Java EE 5. Avec Java EE 5, les EJB, les entités persistantes et les services web ont été remodelés afin d’utiliser une approche plus orientée objet (via des classes Java implémentant des interfaces Java) et pour se servir des annotations pour définir les métadonnées (les descripteurs de déploiement XML sont donc devenus facultatifs). Java EE 6 poursuit dans cette voie et applique les mêmes paradigmes à la couche web. Aujourd’hui, un bean géré par JSF est une classe Java annotée avec un descripteur XML. Java EE 6 s’applique également à simplifier la plate-forme à l’aide de profils et en supprimant certaines technologies obsolètes. Il ajoute des fonctionnalités supplémentaires aux spécifications existantes (en standardisant, par exemple, les beans de session singletons) et en ajoute de nouvelles (comme JAX-RS). Plus que jamais, les applications Java EE 6 sont portables entre les conteneurs grâce aux noms JNDI standard et à un conteneur EJB intégré.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 19
Plus léger
Le groupe d’experts pour Java EE 6 a relevé un défi intéressant : comment alléger la plate-forme tout en lui ajoutant des spécifications supplémentaires ? Aujourd’hui, un serveur d’applications doit implémenter vingt-huit spécifications pour être conforme à Java EE 6. Un développeur doit donc connaître des milliers d’API, certaines n’étant même plus pertinentes puisqu’elles ont été marquées comme élaguées. Pour rendre la plate-forme plus légère, le groupe d’experts a donc introduit les profils, l’élagage et EJB Lite (un sous-ensemble des fonctionnalités complètes d’EJB uniquement destiné aux interfaces locales, aux transactions et à la sécurité). Nous étudierons EJB Lite plus en détail au Chapitre 6. Élagage
La première version de Java EE est apparue en 1999 et, depuis, chaque nouvelle version a ajouté son lot de nouvelles spécifications (comme on l’a vu à la Figure 1.1). Cette inflation est devenue un problème en termes de taille, d’implémentation et d’apprentissage. Certaines fonctionnalités n’étaient pas très bien supportées ou peu déployées parce qu’elles étaient techniquement dépassées ou que d’autres solutions avaient vu le jour entre-temps. Le groupe d’experts a donc décidé de proposer la suppression de certaines fonctionnalités via l’élagage (pruning). Java EE 6 a adopté ce mécanisme d’élagage (également appelé "marquage pour suppression"), suivant en cela le groupe Java SE. Ce mécanisme consiste à proposer une liste de fonctionnalités qui pourraient ne plus être reconduites dans Java EE 7. Aucun de ces éléments n’est supprimé dans la version courante. Certaines fonctionnalités seront remplacées par des spécifications plus récentes (les beans entités, par exemple, sont remplacés par JPA) et d’autres quitteront simplement la spécification Java EE 7 pour continuer d’évoluer comme des JSR indépendantes (les JSR 88 et 77, par exemple). Ceci dit, les fonctionnalités élaguées suivantes sont toujours présentes dans Java EE 6 : ■■
EJB 2.x Entity Beans CMP (partie de la JSR 318). Ce modèle de composants persistants complexe et lourd des beans entités d’EJB 2.x a été remplacé par JPA.
■■
JAX-RPC (JSR 101). Il s’agissait de la première tentative de modéliser les services web SOAP comme des appels RPC. Cette spécification a désormais été remplacée par JAX-WS, qui est bien plus simple à utiliser et plus robuste.
■■
JAXR (JSR 93). JAXR est l’API dédiée aux communications avec les registres UDDI. Ce dernier étant peu utilisé, JAXR devrait quitter Java EE et continuer d’évoluer comme une JSR distincte.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
20
Java EE 6 et GlassFish 3
■■
Java EE Application Deployment (JSR 88). La JSR 88 est une spécification que les développeurs d’outils peuvent utiliser pour le déploiement sur les serveurs d’applications. Cette API n’ayant pas reçu beaucoup de soutien de la part des éditeurs, elle devrait quitter Java EE et continuer d’évoluer comme une JSR distincte.
■■
Java EE Management (JSR 77). Comme la JSR 88, la JSR 77 était une tentative de créer des outils de gestion des serveurs d’applications.
Profils
Les profils sont une innovation majeure de l’environnement Java EE 6. Leur but principal consiste à réduire la taille de la plate-forme pour qu’elle convienne mieux aux besoins du développeur. Quelles que soient la taille et la complexité de l’application que vous développez aujourd’hui, vous la déploierez sur un serveur d’applications qui vous offre les API et les services de vingt-huit spécifications. L’une des principales critiques adressées à Java EE est qu’il était trop lourd : les profils ont donc été conçus pour régler ce problème. Comme le montre la Figure 1.4, les profils sont des sous-ensembles ou des surensembles de la plate-forme et peuvent chevaucher cette dernière ou d’autres profils. Figure 1.4 Profils de la plate-forme Java EE.
Java EE 6 complet Profil X Profil web
Profil Y
Java EE 6 définit un seul profil : le profil web. Son but consiste à permettre au développeur de créer des applications web avec l’ensemble de technologies approprié. Web Profile 1.0 est spécifié dans une JSR distincte ; c’est le premier profil de la plate-forme Java EE 6. D’autres seront créés dans le futur (on pourrait penser à un profil minimal ou à un profil de portail). Le profil web, quant à lui, évoluera à son rythme et nous pourrions disposer d’une version 1.1 ou 1.2 avant la sortie de Java EE 7. Nous verrons également apparaître des serveurs d’applications compatibles Web Profile 1.0 et non plus compatibles Java EE 6. Le Tableau 1.6 énumère les spécifications contenues dans ce profil web.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 21
Tableau 1.6 : Spécifications de Web Profile 1.0
Spécification
Version
JSR
URL
JSF
2.0
314
http://jcp.org/en/jsr/detail?id=314
JSP
2.2
245
http://jcp.org/en/jsr/detail?id=245
JSTL
1.2
52
http://jcp.org/en/jsr/detail?id=52
Servlet
3.0
315
http://jcp.org/en/jsr/detail?id=315
Expression Language
1.2
EJB Lite
3.1
318
http://jcp.org/en/jsr/detail?id=318
JPA
2.0
317
http://jcp.org/en/jsr/detail?id=317
JTA
1.1
907
http://jcp.org/en/jsr/detail?id=907
Annotations communes
1.0
250
http://jcp.org/en/jsr/detail?id=250
Plus simple d’utilisation
Outre l’allègement de la plate-forme, un autre but de Java EE 6 était également de le rendre plus simple d’utilisation. Le choix de cette version a été d’appliquer ce paradigme à la couche web. Les composants de Java EE ont besoin de métadonnées pour informer le conteneur de leur comportement – avant Java EE 5, la seule solution était d’utiliser un fichier descripteur de déploiement XML. Avec l’apparition des annotations dans les EJB, les entités et les services web, il est devenu plus simple d’assembler et de déployer les composants puisqu’il y a moins de XML à écrire. Java EE 5 a donc modifié l’architecture de la couche entreprise et les composants sont passés à un modèle POJO et aux interfaces ; la couche web, en revanche, ne bénéficiait pas encore de ces améliorations. Avec Java EE 6, les servlets, les beans gérés par JSF, les convertisseurs JSF, les validateurs et les moteurs de rendus sont également des classes annotées pouvant éventuellement être assorties de descripteurs de déploiement en XML. Le Listing 1.1 montre le code d’un bean géré par JSF : vous constaterez que ce n’est, en fait, qu’une classe Java avec une seule annotation. Si vous connaissez déjà JSF, vous apprendrez avec plaisir que, dans la plupart des cas, le fichier faces-config.xml est devenu facultatif (si vous ne connaissez pas JSF, vous le découvrirez aux Chapitres 10, 11 et 12).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
22
Java EE 6 et GlassFish 3
Listing 1.1 : Un bean géré par JSF @ManagedBean public class BookController { @EJB private BookEJB bookEJB; private Book book = new Book(); private List bookList = new ArrayList(); public String doCreateBook() { book = bookEJB.createBook(book); bookList = bookEJB.findBooks(); return "listBooks.xhtml"; } }
// Getters, setters
Les EJB sont également plus simples à développer en Java EE 6. Comme le montre le Listing 1.2, une simple classe annotée sans interface suffit désormais pour accéder localement à un EJB. Les EJB peuvent également être déployés directement dans un fichier war sans avoir été au préalable assemblés dans un fichier jar. Toutes ces améliorations font des EJB les composants transactionnels les plus simples, qui peuvent servir aussi bien à des applications web minimales qu’à des applications d’entreprise complexes. Listing 1.2 : EJB sans état @Stateless public class bookEJB { @PersistenceContext(unitName = "chapter01PU") private EntityManager em; public Book findBookById(Long id) { return em.find(Book.class, id); }
}
public Book createBook(Book book) { em.persist(book); return book; }
Plus riche
D’un côté, Java EE 6 s’est allégé en introduisant les profils ; de l’autre, il s’est enrichi en ajoutant de nouvelles spécifications et en améliorant celles qui existaient
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 23
déjà. Les services web REST ont fait leur chemin dans les applications modernes et Java EE 6 suit donc les besoins des entreprises en ajoutant la nouvelle spécification JAX-RS. Comme le montre le Listing 1.3, un service web REST est une classe annotée qui répond à des actions HTTP. Vous en apprendrez plus sur JAX-RS au Chapitre 15. Listing 1.3 : Service web REST @Path("books") public class BookResource { @PersistenceContext(unitName = "chapter01PU") private EntityManager em; @GET @Produces({"application/xml", "application/json"}) public List getAllBooks() { Query query = em.createNamedQuery("findAllBooks"); List books = query.getResultList(); return books; } }
La nouvelle version de l’API de persistance (JPA 2.0) a été améliorée par l’ajout de collections de types de données simples (String, Integer, etc.), d’un verrouillage pessimiste, d’une syntaxe JPQL plus riche, d’une toute nouvelle API de définition de requêtes et par le support de la mise en cache. JPA est décrite aux Chapitres 2 à 5 de cet ouvrage. Les EJB sont plus faciles à écrire (avec des interfaces éventuelles) et à assembler (dans un fichier war) et disposent également de nouvelles fonctionnalités, comme les appels asynchrones ou un service de temporisation plus élaboré pour planifier les tâches. Un nouveau composant bean de session singleton fait également son apparition. Comme le montre le Listing 1.4, une simple annotation suffit à transformer une classe Java en singleton géré par un conteneur (une seule instance du composant par application). Les Chapitres 6 à 9 vous en apprendront plus sur ces nouvelles fonctionnalités. Listing 1.4 : Bean de session singleton @Singleton public class CacheEJB { private Map cache = new HashMap(); public void addToCache(Long id, Object object) { if (!cache.containsKey(id))
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
24
Java EE 6 et GlassFish 3
cache.put(id, object); } public Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
La couche présentation s’est également enrichie. JSF 2.0 ajoute en effet le support d’Ajax et des Facelets (voir Chapitres 10 à 12). Plus portable
Depuis sa création, le but de Java EE est de permettre le développement et le déploiement des applications sur n’importe quel serveur d’applications, sans modifier son code ou les fichiers de configuration. En réalité, ce n’est pas aussi simple que cela puisse paraître : les spécifications ne couvrent pas tous les détails et les implémentations finissent par offrir des solutions non portables. C’est ce qui s’est passé pour les noms JNDI, par exemple : lorsque l’on déployait un EJB sur GlassFish, JBoss ou WebLogic, le nom JNDI était différent parce qu’il ne faisait pas partie de la spécification et il fallait donc modifier le code en fonction du serveur d’applications utilisé. Ce problème précis est désormais corrigé car Java EE 6 spécifie une syntaxe précise des noms JNDI qui est la même sur tous les serveurs d’applications (voir Chapitre 7). Une autre difficulté avec les EJB consiste à pouvoir les tester ou à les utiliser dans un environnement Java SE. Certains serveurs d’application (comme JBoss) utilisent pour cela leurs propres implémentations. EJB 3.1 fournit désormais une API standard pour l’exécution des EJB dans un environnement Java SE (voir Chapitre 7).
L’application CD-Bookstore Tout au long de ce livre, nous présenterons des extraits de code qui manipulent des entités, des EJB, des pages JSF, des écouteurs JMS et des services web SOAP ou REST. Tous ces extraits proviennent de l’application CD-Bookstore, un site web de commerce en ligne permettant de parcourir un catalogue de livres et de CD pour les acheter. L’application interagit avec un système bancaire pour valider les cartes de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 25
crédit. Le diagramme des cas d’utilisation présenté à la Figure 1.5 décrit les acteurs et les fonctionnalités de ce système. Figure 1.5
CD BookStore
Diagramme des cas d’utilisation de l’application CD-Bookstore.
Création d'un compte
Gestion du catalogue des articles
Parcours du catalogue Recherche d'un article
Gestion des clients
Connexion et déconnexion
Employé Parcours des commandes
Création de commande
Banque
Utilisateur
Validation de carte bancaire
Modification du compte
Client
Achat d'articles
Les acteurs qui interagissent avec le système décrit à la Figure 1.5 sont les suivants : ■■
Les employés de la société, qui doivent gérer à la fois le catalogue des articles et les informations sur les clients. Ils peuvent également parcourir les commandes.
■■
Les utilisateurs, qui sont les visiteurs anonymes du site qui consultent le catalogue des livres et des CD. Pour acheter un article, ils doivent créer un compte afin de devenir clients.
■■
Les clients, qui peuvent parcourir le catalogue, modifier les informations de leur compte et acheter des articles en ligne.
■■
La banque externe, à laquelle le système délègue la validation des cartes de crédit. INFO
Le code des exemples de ce livre est disponible sur le site web des éditions Pearson (http:// www.pearson.fr), sur la page consacrée à cet ouvrage.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
26
Java EE 6 et GlassFish 3
Configuration de l’environnement de travail Ce livre contient de nombreux extraits de code et la plupart des chapitres se terminent par une section "Récapitulatif". Ces sections expliquent pas à pas comment développer, compiler, déployer, exécuter et tester unitairement un composant. Pour cela, vous aurez besoin des logiciels suivants : ■■
JDK 1.6 ;
■■
Maven 2 ;
■■
Junit 4 ;
■■
la base de données Derby 10.5 (alias JavaDB) ;
■■
le serveur d’application GlassFish v3.
JDK 1.6
Le JDK (Java Development Kit) est essentiel au développement et à l’exécution des exemples de ce livre. Il comprend un certain nombre d’outils, notamment un compilateur (javac), une machine virtuelle (java), un générateur de documentation (javadoc), des outils de gestion (Visual VM), etc. Pour l’installer, rendez-vous sur le site officiel de Sun (http://java.sun.com/javase/downloads), choisissez votre plate-forme et votre langue, puis téléchargez la distribution appropriée. Si vous travaillez sous Windows (ce livre ne traite pas des systèmes Linux et OS X), double-cliquez sur le fichier jdk-6u18-windows-i586-p.exe. Le premier écran vous demandera d’accepter la licence du logiciel et le second, présenté à la Figure 1.6, énumérera les modules du JDK que vous pouvez installer (JDK, JRE, base de données Derby, sources). Une fois l’installation terminée, il faut initialiser la variable JAVA_HOME avec le répertoire où vous avez choisi d’installer le JDK (C:\Program Files\Java\jdk1.6.0_18\ par défaut) puis ajouter le répertoire %JAVA_HOME%\bin à la variable PATH. Pour vérifier que Java est bien reconnu par votre système, tapez la commande java -version (voir Figure 1.7).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 27
Figure 1.6 Configuration de l’installation du JDK.
Figure 1.7 Affichage de la version du JDK.
Maven 2
Afin de refléter ce que vous trouverez sur le terrain, nous avons décidé d’utiliser Maven (http://maven.apache.org) pour construire les exemples de ce livre, bien que sa description complète sorte du cadre de cet ouvrage (vous trouverez de très nombreuses ressources consacrées à cet outil sur Internet ou dans les librairies). Cependant, nous introduirons quelques éléments que vous devez connaître pour comprendre et utiliser nos exemples. Historique
La construction d’une application Java EE exige plusieurs opérations : ■■
génération du code et des ressources ;
■■
compilation des classes Java et des classes de test ;
■■
assemblage du code dans une archive (jar, ear, war, etc.) avec, éventuellement, des bibliothèques jar externes.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
28
Java EE 6 et GlassFish 3
Effectuer ces tâches manuellement prend du temps et risque de produire des erreurs. Les équipes de développement ont donc recherché des moyens d’automatiser tout ce processus. En 2000, les développeurs Java commencèrent à utiliser Ant (http://ant.apache. org), qui leur permettait d’écrire des scripts de construction de leurs applications. Ant est lui-même écrit en Java et offre un grand nombre de commandes qui, à l’instar de l’outil make d’Unix, sont portables entre les différentes plates-formes. Les équipes de développement commencèrent donc à créer leurs propres scripts en fonction de leurs besoins. Cependant, Ant atteignit ses limites lorsque les projets commencèrent à impliquer des systèmes hétérogènes complexes. Les sociétés avaient du mal à industrialiser leur système de construction de leurs applications. Il n’existait pas véritablement d’outil permettant de réutiliser simplement un script de construction d’un projet à l’autre (le copier/coller était la seule méthode pour y parvenir). En 2002, la fondation Apache a mis à disposition Maven, qui non seulement résolvait tous ces problèmes mais allait également bien au-delà d’un simple outil de construction. Maven offre aux projets une solution pour les construire, des bibliothèques partagées et une plate-forme évolutive au moyen d’extensions, permettant ainsi d’assurer la qualité, de produire la documentation, de gérer les équipes de travail, etc. Fondé sur le principe de "convention plutôt que configuration", Maven introduit une description de projet standard et un certain nombre de conventions, notamment une structure de répertoires standardisée (voir Figure 1.8). Avec son architecture extensible reposant sur des extensions (appelées mojos), Maven offre de nombreux services. Figure 1.8 Structure de répertoires standard de Maven.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 29
Descripteur de projet
Maven repose sur le fait qu’une grande majorité de projets Java et Java EE ont des besoins similaires lors de la construction des applications. Un projet Maven doit respecter des standards et définir des fonctionnalités spécifiques dans un descripteur de projet ou POM (Project Object Model). Ce POM est un fichier XML (pom.xml) situé à la racine du projet. Comme le montre le Listing 1.5, l’information minimale permettant de définir l’identité d’un projet est le groupId, l’artifactId, la version et le type de paquetage. Listing 1.5 : pom.xml minimal 4.0.0 com.apress.javaee6 chapter01 1.0-SNAPSHOT jar
Un projet est souvent divisé en différents artéfacts qui sont alors regroupés sous le même groupId (comme les paquetages en Java) et identifiés de façon unique par l’artifactId. Le marqueur packaging permet à Maven de produire l’artéfact dans un format standard (jar, war, ear, etc.). Enfin, version identifie un artéfact au cours de son évolution (version 1.1, 1.2, 1.2.1, etc.). Maven impose cette numérotation des versions pour qu’une équipe puisse gérer l’évolution du développement de son projet. Maven introduit également le concept de versions SNAPSHOT (le numéro de version se termine alors par la chaîne -SNAPSHOT) pour identifier un artéfact en cours de développement. Le POM définit bien plus d’informations sur vos projets. Certaines sont purement descriptives (nom, description, etc.), d’autres concernent l’exécution de l’application, comme la liste des bibliothèques externes qu’elle utilise, etc. Enfin, le fichier pom.xml précise l’environnement de construction du projet (outils de contrôle de versions, serveur d’intégration, dépôts d’artéfacts) et tout autre processus spécifique nécessaire à la construction du projet. Gestion des artéfacts
Maven ne se contente pas de construire des artéfacts : il permet également de les archiver et de les partager. Pour ce faire, il utilise un dépôt local sur le disque dur
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
30
Java EE 6 et GlassFish 3
(%USER_HOME%/ .m2/repository par défaut) où il stocke tous les artéfacts manipulés par les descripteurs du projet. Ce dépôt local (voir Figure 1.9) est rempli soit par les artéfacts locaux du développeur (monProjet-1.1.jar, par exemple) soit par des artéfacts externes (glassfish-3.0.jar, par exemple) que Maven télécharge à partir de dépôts distants. Par défaut, Maven utilise le dépôt principal situé à l’URL http:// repo1.maven.org/maven2 pour télécharger les artéfacts manquants. Figure 1.9 Exemple de dépôt local.
Comme le montre le Listing 1.6, un projet Maven déclare ses dépendances dans le POM (groupId, artifactId, version, type). Si nécessaire, Maven les téléchargera dans le dépôt local à partir de dépôts distants. En outre, grâce aux descripteurs POM de ces artéfacts externes, Maven téléchargera également les artéfacts dont ils dépendent, etc. L’équipe de développement n’a donc pas besoin de gérer manuellement les dépendances des projets : toutes les bibliothèques nécessaires sont automatiquement ajoutées par Maven. Listing 1.6 : Dépendances dans le fichier pom.xml ... org.eclipse.persistence javax.persistence 1.1.0 provided
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 31
org.glassfish javax.ejb 3.0 provided ...
Les dépendances peuvent avoir une visibilité limitée (désignée par scope) : ■■ test.
La bibliothèque sert à compiler et à exécuter les classes de test mais n’est pas assemblée dans l’artéfact produit.
■■ provided.
La bibliothèque est fournie par l’environnement (fournisseur de persistance, serveur d’application, etc.) et ne sert qu’à compiler le code.
■■ compile.
La bibliothèque est nécessaire à la compilation et à l’exécution.
■■ runtime.
La bibliothèque n’est requise que pour l’exécution mais est exclue de la compilation (composants JSF ou bibliothèques de marqueurs JSTL, par exemple).
Modularité des projets
Pour résoudre le problème de la modularité des projets, Maven fournit un mécanisme reposant sur des modules, chaque module étant lui-même un projet. Maven peut ainsi construire un projet composé de plusieurs modules en calculant les dépendances entre eux (voir Figure 1.10). Pour faciliter la réutilisation des paramètres classiques, les descripteurs POM peuvent hériter des POM des projets parents. Figure 1.10 Un projet et ses modules.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
32
Java EE 6 et GlassFish 3
Extensions et cycle de vie
Maven utilise un cycle de vie en plusieurs phases (voir Figure 1.11) : nettoyage des ressources, validation du projet, production des sources nécessaires, compilation des classes Java, exécution des classes de test, assemblage du projet et installation de celui-ci dans le dépôt local. Ce cycle de vie constitue une ossature sur laquelle viennent s’ajouter les extensions Maven (alias mojos). Ces mojos dépendent du type de projet (un mojo pour compiler, un autre pour tester, un autre pour construire, etc.). Dans la description du projet, vous pouvez lier de nouvelles extensions à une phase du cycle de vie, modifier la configuration d’une extension, etc. Lorsque vous construisez un client d’un service web, par exemple, vous pouvez ajouter un mojo qui produit les artéfacts du service web au cours de la phase de production des sources. Figure 1.11 Cycle de vie d’un projet.
Installation
Les exemples de ce livre ont été développés avec Maven 2.2.1. Après avoir installé le JDK 1.6, assurez-vous que la variable JAVA_HOME pointe sur le répertoire de celuici puis téléchargez Maven à partir de l’URL http://maven.apache.org/, dézippez le fichier sur votre disque dur et ajoutez le répertoire apache-maven-2.2.1/bin à votre variable PATH.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 33
Puis, dans une fenêtre de commande DOS, tapez mvn -version pour tester votre installation. Comme le montre la Figure 1.12, Maven devrait afficher sa version et celle du JDK. Figure 1.12 Maven affiche sa version.
Maven a besoin d’un accès Internet pour pouvoir télécharger les extensions et les dépendances des projets à partir du dépôt principal. Si vous vous trouvez derrière un proxy, consultez la documentation pour ajuster votre configuration en conséquence. Utilisation
Voici quelques commandes que nous utiliserons pour les exemples de ce livre. Elles invoquent toute une phase différente du cycle de vie (nettoyage, compilation, installation, etc.) et utilisent le fichier pom.xml pour ajouter des bibliothèques, personna liser la compilation ou ajouter des comportements via des extensions : ■■ mvn clean.
Supprime tous les fichiers générés (classes compilées, code produit, artéfacts, etc.).
■■ mvn compile.
Compile les classes Java principales.
■■ mvn test-compile. ■■ mvn test.
Compile les classes de test.
Compile les classes de test et exécute les tests.
■■ mvn package.
Compile et exécute les tests et crée l’archive du paquetage.
■■ mvn install.
Construit et installe les artéfacts dans votre dépôt local.
■■ mvn clean install.
Nettoie et installe (remarquez que vous pouvez indiquer plusieurs commandes en les séparant par un espace).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
34
Java EE 6 et GlassFish 3
INFO Maven vous permet de compiler, d’exécuter et d’assembler les exemples de ce livre. Cependant, pour écrire le code, vous aurez besoin d’un environnement de développement intégré (EDI). Personnellement, nous utilisons IntelliJ IDEA de JetBrains, dont vous apercevrez quelques copies d’écran. Vous pouvez choisir n’importe quel IDE car cet ouvrage ne repose que sur Maven, non sur des fonctionnalités spécifiques d’IntelliJ IDEA.
JUnit 4
JUnit est un framework open-source pour l’écriture et l’exécution de tests. Parmi ses fonctionnalités, citons : ■■
les assertions pour tester des résultats attendus ;
■■
les "fixtures" pour partager des données de test communes ;
■■
les lanceurs pour exécuter les tests.
JUnit est une bibliothèque de tests unitaires qui est le standard de facto pour Java ; elle est assemblée dans un unique fichier jar que vous pouvez télécharger à partir de l’URL http://www.junit.org/ (ou utilisez la gestion des dépendances de Maven pour le récupérer). La bibliothèque contient une API complète vous permettant d’écrire vos tests unitaires, ainsi qu’un outil pour les exécuter. Ces tests unitaires aident à rendre votre code plus robuste et plus fiable. Historique
La première version de JUnit a été écrite par Erich Gamma et Kent Beck en 1998. Elle s’inspirait du framework de test Sunit de Smalltalk, également écrit par Kent Beck, et est rapidement devenue l’un des frameworks les plus connus du monde Java. Apportant les avantages des tests unitaires à une grande variété de langages, JUnit a inspiré une famille d’outils xUnit comme nUnit (.NET), pyUnit (Python), CppUnit (C++), dUnit (Delphi), et bien d’autres encore. JUnit joue un rôle important dans le développement piloté par les tests (DPT). Fonctionnement
Depuis JUnit 4, l’écriture des tests unitaires s’est simplifiée grâce à l’usage des annotations, des importations statiques et des autres fonctionnalités de Java. Par
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 35
rapport aux versions précédentes, JUnit 4 fournit un modèle de test plus simple, plus riche et plus facile d’emploi. Il introduit également des initialisations plus souples, des règles de nettoyage, des délais d’expiration et des cas de tests paramétrables. Étudions quelques-unes de ses fonctionnalités au moyen d’un exemple simple. Le Listing 1.7 représente un POJO Customer possédant quelques attributs dont une date de naissance, des constructeurs, des getters et des setters. Listing 1.7 : Une classe Customer public class Customer { private Long id; private String firstName; private String lastName private String email; private String phoneNumber; private Date dateOfBirth; private Date creationDate; // Constructeurs, getters, setters }
La classe CustomerHelper, présentée dans le Listing 1.8, fournit une méthode c alculateAge() permettant de calculer l’âge d’un client donné. Listing 1.8 : La classe CustomerHelper public class CustomerHelper { private int ageCalcResult; private Customer customer; public void calculateAge() { Date dateOfBirth = customer.getDateOfBirth(); Calendar birth = new GregorianCalendar(); birth.setTime(dateOfBirth); Calendar now = new GregorianCalendar(2001, 1, 1); ageCalcResult = now.get(Calendar.YEAR) birth.get(Calendar.YEAR); // Pas encore implémentée public Date getNextBirthDay() { return null; } public void clear() { ageCalcResult = 0; customer = null; } // Getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
36
Java EE 6 et GlassFish 3
La méthode calculateAge() utilise l’attribut dateOfBirth pour renvoyer l’âge du client. La méthode clear() réinitialise l’état de CustomerHelper et la méthode getNextBirthDay() n’est pas encore implémentée. Cette classe auxiliaire a quelques défauts : il semble notamment qu’il y ait un bogue dans le calcul de l’âge. Pour tester la méthode calculateAge(), nous utiliserions la classe JUnit TestCustomerHelper décrite dans le Listing 1.9. Listing 1.9 : Classe de test pour CustomerHelper import import import import
public class CustomerHelperTest { private static CustomerHelper customerHelper = new CustomerHelper(); @Before public void clearCustomerHelper() { customerHelper.clear(); } @Test public void notNegative() { Customer customer = new Customer(); customer.setDateNaissance(new GregorianCalendar(1975, 5, 27).getTime()); customerHelper.setCustomer(customer); customerHelper.calculateAge(); int calculatedAge = customerHelper.getAgeCalcResult(); assert calculatedAge >= 0; } @Test public void expectedValue() { int expectedAge = 33; Calendar birth = new GregorianCalendar(); birth.roll(Calendar.YEAR, expectedAge * (-1)); birth.roll(Calendar.DAY_OF_YEAR, -1); Customer customer = new Customer(); customer.setDateOfBirth(birth.getTime()); customerHelper.setCustomer(customer); customerHelper.calculateAge();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 37
assertTrue(customerHelper.getAgeCalcResult() == expectedAge); } @Test(expected = NullPointerException.class) public void emptyCustomer() { Customer customer = new Customer(); customerHelper.setCustomer(customer); customerHelper.calculateAge(); assertEquals( customerHelper.getAgeCalcResult(), -1); } @Ignore("not ready yest") @Test public void nextBirthDay() { // to do.. } }
La classe de test du Listing 1.9 contient quatre méthodes de test. La méthode expectedValue() échouera car il y a un bogue dans le calcul de l’âge effectué par la classe CustomerHelper. La méthode nextBirthDay() est ignorée car elle n’est pas encore implémentée. Les deux autres méthodes réussiront. emptyCustomer() s’attend à ce que la méthode lance une exception NullPointerException. Méthodes de test
Avec JUnit 4, les classes de test n’ont pas besoin d’hériter d’une classe quelconque. Pour être exécutée comme un cas de test, une classe JUnit doit au moins posséder une méthode annotée par @Test. Si vous tentez d’exécuter une classe qui ne comporte pas au moins une méthode @Test, vous obtiendrez une erreur (java.lang. Exception: No runnable methods). Une méthode de test doit utiliser l’annotation @Test, renvoyer void et ne prendre aucun paramètre. Ces contraintes sont vérifiées lors de l’exécution et leur non-respect provoque la levée d’une exception. L’annotation @Test peut prendre un paramètre facultatif expected pour indiquer que la méthode de test concernée doit lever une exception. Si elle ne le fait pas ou si l’exception est différente de celle annoncée, le test échoue. Dans notre exemple, une tentative de calculer l’âge d’un client vide doit provoquer la levée d’une exception NullPointerException. La méthode nextBirthDay() n’est pas implémentée dans le Listing 1.9 mais vous ne souhaitez pas pour autant que ce test échoue : vous voulez simplement l’ignorer. Pour ce faire, il suffit d’ajouter l’annotation @Ignore avant ou après l’annotation @Test.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
38
Java EE 6 et GlassFish 3
Les lanceurs de tests signaleront le nombre de tests ignorés, ainsi que le nombre de tests qui ont réussi ou échoué. Si vous voulez indiquer la raison pour laquelle un test a été ignoré, vous pouvez éventuellement passer un paramètre String contenant le message adéquat. Méthodes d’assertions
Les cas de test doivent vérifier que les objets sont conformes à ce qui est attendu. JUnit dispose pour cela d’une classe Assert contenant plusieurs méthodes. Pour l’utiliser, vous devez soit utiliser la notation préfixe (Assert.assertEquals(), par exemple) soit importer statiquement la classe Assert (c’est ce que nous avons fait dans le Listing 1.9). Comme vous pouvez le constater avec la méthode notNegative(), vous pouvez aussi vous servir du mot-clé assert de Java. Fixtures
Les fixtures sont des méthodes permettant d’initialiser et de libérer n’importe quel objet au cours des tests. JUnit utilise les annotations @Before et @After pour exécuter du code respectivement avant et après chaque test. Les méthodes annotées par @ Before et @After peuvent porter n’importe quel nom (clearCustomerHelper() ici) et une même classe de test peut en avoir plusieurs. JUnit utilise également les annotations @BeforeClass et @AfterClass pour exécuter un code spécifique une seule fois par classe. Ces méthodes doivent être uniques et statiques et sont très pratiques pour allouer et libérer des ressources coûteuses. Lancement de JUnit
Pour exécuter le lanceur de JUnit, vous devez ajouter le fichier jar de JUnit à votre variable CLASSPATH (ou ajouter une dépendance Maven). Vous pouvez alors lancer vos tests via la commande Java suivante : java –ea org.junit.runner.JUnitCore „ com.apress.javaee6.CustomerHelperTest
Notez que, lorsque l’on utilise le mot-clé assert, il faut préciser le paramètre -ea ; sinon les assertions seront ignorées. Cette commande produira le résultat suivant : JUnit version 4.5 ..E.I Time: 0.016 There was 1 failure:
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
La première information affichée est le numéro de version de JUnit (4.5, ici). Puis JUnit indique le nombre de tests exécutés (trois, ici) et le nombre d’échecs (un seul dans cet exemple). La lettre I indique qu’un test a été ignoré. Intégration de JUnit
Actuellement, JUnit est très bien intégré à la plupart des EDI (IntelliJ IDEA, Eclipse, NetBeans, etc.). Avec ces environnements, JUnit utilise généralement la couleur verte pour indiquer les tests qui ont réussi et le rouge pour signaler les échecs. La plupart des EDI fournissent également des outils pour faciliter la création des classes de test. JUnit est également intégré à Maven via l’extension Surefire utilisée au cours de la phase de test. Cette extension exécute les classes de tests JUnit d’une application et produit des rapports aux formats texte et XML. Pour lancer les tests JUnit via cette extension, faites la commande suivante : mvn test
Derby 10.5
Initialement nommé Cloudscape, le système de base de données Derby écrit en Java a été offert par IBM à la fondation Apache et est devenu open-source. De son côté, Sun Microsystems a produit sa propre distribution sous le nom de Java DB. Malgré une empreinte mémoire réduite (2 Mo), Derby est un système de base de données relationnelle entièrement fonctionnel qui supporte les transactions et peut aisément s’intégrer dans n’importe quelle solution Java. Derby a deux modes différents : intégré ou serveur réseau. Le mode intégré correspond au lancement de Derby par une simple application Java mono-utilisateur : en ce cas, il s’exécute dans la même JVM que l’application. C’est ce mode que nous utiliserons dans ce livre pendant les tests unitaires. Le mode serveur réseau correspond au lancement de Derby sous forme de processus séparé, fournissant une connectivité multi-utilisateurs. Nous utiliserons ce mode lorsque nous exécuterons les applications.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
40
Java EE 6 et GlassFish 3
Installation
L’installation de Derby est très simple ; en fait, vous constaterez qu’il est déjà installé puisqu’il est fourni avec le JDK 1.6 – au cours de l’installation de ce dernier (voir Figure 1.6), l’assistant propose d’installer par défaut Java DB. S’il n’est pas installé sur votre machine, vous pouvez récupérer les binaires à partir de l’URL http://db.apache.org. Lorsque Derby est installé, configurez la variable DERBY_HOME pour qu’elle contienne le répertoire où il a été placé sur votre système, puis ajoutez %DERBY_HOME%\bin à votre variable PATH. Pour lancer Derby en mode serveur réseau, exécutez le script %DERBY_HOME%\bin\startNetworkServer.bat : des informations s’afficheront alors sur la console pour indiquer, par exemple, le numéro du port sur lequel il attend les connexions (1527 par défaut). Derby est fourni avec plusieurs programmes utilitaires, dont sysinfo. Ouvrez une session DOS, tapez sysinfo et vous devriez voir apparaître des informations sur votre environnement Java et Derby, analogues à celles de la Figure 1.13. Figure 1.13 Résultat de sysinfo après l’installation de Derby.
Derby fournit plusieurs outils (dans le sous-répertoire bin) permettant d’interagir avec la base de données. Le plus simple est probablement ij, qui permet de saisir des commandes SQL, et dblook, qui permet de visualiser une partie ou la totalité du langage de définition des données (LDD) d’une base. Assurez-vous d’avoir lancé le serveur réseau Derby et tapez la commande ij à l’invite de commande. Puis saisissez les commandes suivantes pour créer une base de données et une table, insérer des données dans cette table et l’interroger : ij> connect ’jdbc:derby://localhost:1527/Chapter01DB;create=true’;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 41
Cette commande vous connecte à la base de données Chapter01DB. Comme celleci n’existe pas encore, nous utilisons le paramètre create=true pour demander sa création. ij> create table customer (custID int primary key, > firstname varchar(20), lastname varchar(20));
Cette commande crée une table customer avec une clé primaire et deux colonnes varchar(20) pour stocker le prénom et le nom de chaque client. Vous pouvez afficher la description de cette table à l’aide de la commande suivante : ij> describe customer; COLUMN_NAME |TYPE_NAME|DEC&|NUM&|COLUM&|COLUMN_DEF|CHAR_OCTE&|IS_NULL& -------------------------------------------------------------------------CUSTID |INTEGER |0 |10 |10 |NULL |NULL |NO FIRSTNAME |VARCHAR |NULL|NULL|20 |NULL |40 |YES LASTNAME |VARCHAR |NULL|NULL|20 |NULL |40 |YES
Maintenant que la table est créée, vous pouvez y ajouter des données à l’aide d’instructions insert : ij> insert into customer values (1, ’Fred’, ’Chene’); ij> insert into customer values (2, ’Sylvain’, ’Verin’); ij> insert into customer values (3, ’Robin’, ’Riou’);
Vous pouvez ensuite utiliser toute la puissance de SQL pour obtenir, trier ou regrouper des données : ij> select count(*) from customer; 1 ----------3 1 ligne sélectionnée ij> select * from customer where custid=3; CUSTID |FIRSTNAME |LASTNAME --------------------------------------------------3 |Robin |Riou 1 ligne sélectionnée ij> exit;
Pour obtenir le LDD de la table créée, sortez d’ij et lancez dblook pour interroger la base de données Chapter01DB : C:\> dblook -d ’jdbc:derby://localhost:1527/Chapter01DB’ -- Horodatage : 2010-01-29 19:21:09.379 -- La base de données source est : Chapter01DB -- L’URL de connexion est : „ jdbc:derby://localhost:1527/Chapter01DB -- appendLogs: false
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
42
Java EE 6 et GlassFish 3
-- ----------------------------------------------- Instructions DDL pour tables -- ---------------------------------------------CREATE TABLE "APP"."CUSTOMER" ("CUSTID" INTEGER NOT NULL, „ "FIRSTNAME" VARCHAR(20), "LASTNAME" VARCHAR(20)); -- ----------------------------------------------- Instructions DDL pour clés -- ----------------------------------------------- primaire/unique ALTER TABLE "APP"."CUSTOMER" ADD CONSTRAINT "SQL100129191119440" PRIMARY KEY ("CUSTID");
GlassFish v3
Bien qu’il s’agisse d’un serveur d’applications assez récent, GlassFish est déjà utilisé par un grand nombre de développeurs et de sociétés. Non seulement il est l’implémentation de référence de la technologie Java EE, mais c’est également lui que vous obtenez lorsque vous téléchargez le SDK Java EE de Sun. Vous pouvez également déployer des applications critiques sur GlassFish – en plus d’être un produit, GlassFish est également une communauté réunie autour de l’Open Source sur le site http://glassfish.org. Cette communauté est très réactive sur les listes de diffusion et les forums. Historique
Les origines de GlassFish remontent aux premiers jours de Tomcat, lorsque Sun et le groupe JServ firent don de cette technologie à la fondation Apache. Depuis, Sun a continué à utiliser Tomcat dans différents produits. En 2005, Sun lança le projet GlassFish, qui avait pour but le développement d’un serveur d’applications entièrement certifié Java EE. Sa première version, la 1.0, vit le jour en mai 2006. Le conteneur web de GlassFish hérite beaucoup de Tomcat (en fait, une application qui s’exécute sur Tomcat devrait également s’exécuter avec GlassFish sans avoir à la modifier). GlassFish v2 est apparu en septembre 2007 et a reçu depuis de nombreuses mises à jour. Il s’agit de la version la plus déployée actuellement. GlassFish s’efforce de ne pas modifier les habitudes des utilisateurs entre ses versions majeures et de ne pas imposer de modification du code. En outre, il n’y a aucune différence de qualité entre les versions "communautaire" et "commerciale" : les utilisateurs payants ont accès à des correctifs et à des outils de gestion supplémentaires (GlassFish Enter-
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 43
prise Manager), mais la version open-source (http://glassfish. org) et la version commerciale (http://www.sun.com/appserver) ont été testées de la même façon, ce qui facilite le basculement vers une version commerciale à n’importe quel moment dans le cycle du projet. Dans ce livre, nous utiliserons GlassFish v3. Comme il est d’usage avec l’Open Source, des versions quotidiennes et des versions "préludes" sont disponibles. Les buts principaux de cette nouvelle version majeure de GlassFish est la modularisation des fonctionnalités essentielles avec l’introduction d’un noyau reposant sur OSGi et un support complet de Java EE 6. INFO L’équipe de GlassFish a fait un effort considérable pour réaliser une documentation complète et à jour en produisant de nombreux guides : Quick Start Guide, Installation Guide, Administration Guide, Administration Reference, Application Deployment Guide, Developer’s Guide, etc. Vous pouvez les lire à l’URL http:// wiki.glassfish.java.net/Wiki.jsp?page=GlassFishDocs. Consultez également les FAQ, les How-To et le forum GlassFish pour obtenir encore plus d’informations.
Architecture de GlassFish v3
En tant que programmeur d’application (et non en tant que développeur de GlassFish), vous n’avez pas besoin de comprendre son architecture interne, mais son architecture générale et ses choix techniques peuvent vous intéresser. À partir de la version préliminaire de GlassFish v3, le serveur d’applications est construit sur un noyau modulaire reposant sur OSGi. GlassFish s’exécute directement au-dessus de l’implémentation de Felix d’Apache, mais devrait également fonctionner avec les runtimes OSGi Equinox ou Knopflerfish. HK2 (Hundred-Kilobyte Kernel) abstrait le module système OSGi pour fournir les composants qui peuvent être vus comme des services. Ceux-ci peuvent être découverts et injectés en cours d’exécution. Pour l’instant, OSGi n’est pas exposé aux développeurs Java EE. INFO OSGi est un standard pour la gestion et la découverte dynamique des composants. Les applications ou les composants peuvent être installés, lancés, arrêtés, mis à jour et désinstallés à chaud, sans nécessiter un redémarrage. Les composants peuvent également détecter dynamiquement l’ajout ou la suppression de services et s’adapter en conséquence ; Felix d’Apache, Equinox et Knopflerfish sont des implémentations d’OSGi.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
44
Java EE 6 et GlassFish 3
Cette modularité et cette extensibilité permettent à GlassFish v3 de passer d’un simple serveur web attendant des commandes d’administration à un runtime plus puissant moyennant un simple déploiement d’artéfacts comme des fichiers war (un conteneur web est chargé et lancé, puis l’application est déployée) ou des fichiers jar EJB (qui chargeront et lanceront dynamiquement le conteneur EJB). En outre, le serveur initial se lance en quelques secondes (moins de 5 secondes sur une machine récente) et vous ne payez en temps de démarrage et en consommation mémoire que ce que vous utilisez. Le lancement du conteneur web à la volée prend environ 3 secondes de plus et les déploiements s’effectuent souvent en moins de 1 seconde. Tout ceci fait de GlassFish v3 un environnement très apprécié des développeurs. Quel que soit le nombre de modules que charge dynamiquement GlassFish v3, la console d’administration, l’interface en ligne de commande et le fichier de configuration centralisé sont tous extensibles et chacun reste unique. Mentionnons également le framework Grizzly, qui était au départ un serveur HTTP non bloquant reposant sur les E/S et qui est désormais devenu l’un des éléments essentiels de GlassFish, comme le montre la Figure 1.14. Figure 1.14 Architecture de GlassFish v3.
Conteneur web
JSF
Console de gestion
JPA
Metro
Centre de mises à jour
Conteneur EJB
JMS
CLI gestion
Service de nommage
Injection
Grizzly
Configuration
Surveillance
Transaction
Sécurité
Cœur de Glassfish V (Module Subsystem) HK2
Déploiement
Clustering
OSGI Java SE
Centre de mise à jour
Lorsque vous disposez d’un serveur d’application modulaire, vous pouvez commencer à jouer avec les différents modules pour construire votre propre environnement, exactement comme vous le feriez avec les EDI et les distributions Linux ou comme vous le faites avec les extensions de Firefox. Le centre de mise à jour de GlassFish est un ensemble d’outils graphiques et en ligne de commande qui vous permettent de gérer cet environnement. La technologie derrière tout ceci s’appelle IPS (Image
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 45
Packaging System, également appelé pkg), le système de paquetage utilisé par OpenSolaris. Outre l’ensemble de modules fourni par défaut avec GlassFish, l’utilisateur peut se connecter à différents dépôts pour mettre à jour les fonctionnalités déjà installées, en ajouter de nouvelles (support de Grails, conteneur de portlet, etc.), voire ajouter des applications tierces. Dans un environnement d’entreprise, vous pouvez configurer votre propre dépôt et utiliser pkg pour lancer l’installation d’un logiciel reposant sur GlassFish. En pratique, avec GlassFish v3, le centre de mise à jour est accessible via la console d’administration, le client graphique qui se trouve dans le répertoire %GLASSFISH_ HOME%\updatetool\bin ou le programme pkg en ligne de commande. Tous trois vous permettent d’énumérer, d’ajouter et de supprimer des composants à partir d’un ensemble de dépôts. Dans le cas de pkg (qui se trouve dans le répertoire %GLASSFISH_HOME%\pkg\bin), les commandes les plus fréquentes sont pkg list, pkg install, pkg uninstall et pkg image-update. Sous-projets GlassFish
Le serveur d’applications GlassFish étant composé de tant de parties différentes, le projet a été découpé en sous-projets. Cette décomposition permet de mieux comprendre non seulement les différentes parties mais également l’adoption des fonctionnalités individuelles en dehors de l’environnement GlassFish, lorsque l’on est en mode autonome ou dans un autre conteneur. La Figure 1.15 présente un résumé de l’architecture des composantes fonctionnelles du serveur d’applications. OpenMQ, par exemple, est une implémentation open-source de JMS de qualité professionnelle. Bien qu’il soit souvent utilisé de façon autonome pour les architectures orientées messages, OpenMQ peut également être intégré de différentes façons à GlassFish (in-process, out-of-process, ou distant). Son administration peut s’effectuer via la console d’administration de GlassFish ou par l’interface asadmin en ligne de commande (voir la section "asadmin"). Le site web de sa communauté se trouve à l’URL http://openmq.dev.java.net. Metro est le cœur des services web. Cette pile complète est construite au-dessus de JAX-WS et lui ajoute des fonctionnalités avancées, comme une sécurité de bout en bout, un transport optimisé (MTOM, FastInfoset), une messagerie fiable et un comportement transactionnel pour les services web SOAP. Cette qualité de service (QoS) pour les services web repose sur des standards (OASIS, W3C), s’exprime par des politiques et ne nécessite pas l’utilisation d’une nouvelle API en plus de JAX-WS. Metro est également régulièrement testé avec les implémentations .NET
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
46
Java EE 6 et GlassFish 3
de Microsoft pour assurer l’interopérabilité entre les deux technologies. Son site communautaire se trouve à l’URL http://metro.dev.java.net. Serveur d'administration Console d'administration
Extensions du serveur web
Clients web Clients Java/C++/IIOP Conteneur d'applications client Autres serveurs d'applications compatibles Java EE
Application d'administration
Instance d'un serveur d'applications Serveur HTTP
Conteneur web
Connecteur Java EE
EIS
Écouteurs HTTP
Services web
Java Message Service
Fournisseur de messages
ORB
Conteneur EJB
Serveur HADB
Écouteurs IIOP
Gestion du cycle de vie
Gestionnaire de persistance JDBC Gestionnaire de transactions
Processus, gestion des threads, contrôle de l'exécution
Base de données
Figure 1.15 Composantes fonctionnelles de GlassFish.
Mojarra est le nom de l’implémentation de JSF dans GlassFish ; elle est disponible à l’URL http:// mojarra.dev.java.net. Jersey est l’implémentation de référence et de qualité professionnelle pour la nouvelle spécification JAX-RS. Cette spécification et son implémentation sont des nouveaux venus dans Java EE 6 et GlassFish. En fait, Jersey 1.0 est disponible via le centre de mise à jour de GlassFish v2 et v3 depuis sa sortie en 2008. Administration
GlassFish étant un serveur d’applications complet, il implémente évidemment l’intégralité des spécifications Java EE 6, mais il dispose également de fonctionnalités supplémentaires comme son administration, qui peut s’effectuer via une interface web (la "console d’administration") ou au moyen d’asadmin, une interface en ligne de commande puissante. Quasiment toute sa configuration est stockée dans un fichier nommé domain.xml (situé dans le répertoire domains\domain1\config), ce qui simplifie la recherche des erreurs. Ce fichier ne doit pas être modifié manuellement mais via l’un des deux outils d’administration, qui reposent tous les deux sur l’instrumentation JMX fournie par GlassFish.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 47
Console d’administration
La console d’administration est une interface web (voir Figure 1.16) pour le serveur d’applications. Cet outil est destiné à la fois aux administrateurs et aux développeurs et fournit une représentation graphique des objets gérés par le serveur, une visualisation améliorée des journaux, de l’état du système et de la surveillance des données. Au minimum, cette console permet de gérer la création et la modification des configurations (réglage de la JVM, niveau des journaux, réglage du pool et du cache, etc.), JDBC, JNDI, JavaMail, JMS et les ressources connecteur ainsi que les applications (déploiement). Dans le profil cluster de GlassFish, la console d’administration est améliorée pour permettre à l’utilisateur de gérer les clusters, les instances, les agents nœuds et les configurations de répartition de la charge. Une aide contextuelle est toujours disponible via le bouton d’aide situé en haut à droite de la fenêtre. Dans une installation par défaut, la console est accessible après le démarrage de GlassFish par l’URL http://localhost:4848. À partir de GlassFish v3, il est possible de configurer un utilisateur anonyme afin d’éviter de devoir s’authentifier. Si cet utilisateur n’existe pas, une installation typique utilise admin comme nom d’utilisateur et adminadmin comme mot de passe par défaut.
Figure 1.16 Console d’administration web.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
48
Java EE 6 et GlassFish 3
Outil en ligne de commande asadmin
L’interface en ligne de commande asadmin est très puissante et c’est souvent elle que l’on utilise en production car on peut écrire des scripts pour créer des instances et des ressources, déployer des applications et surveiller les données d’un système en cours d’exécution. Cette commande se trouve dans le répertoire bin de GlassFish et peut gérer plusieurs domaines de serveurs d’application locaux ou distants. Elle reconnaît plusieurs centaines de commandes mais vous n’en utiliserez probablement qu’une petite partie. Pour en avoir la liste complète, faites asadmin help. Les commandes utiles dans un profil développeur simple sont asadmin start-domain, asadmin stop-domain, asadmin deploy, asadmin deploydir et asadmin undeploy. Si vous faites une erreur de frappe, asadmin vous proposera la commande correspondante la plus proche. Tapez asadmin resource, par exemple, et vous constaterez qu’asadmin vous propose les commandes de la Figure 1.17. Avec GlassFish v3, asadmin dispose d’un historique des commandes et de la complétion de la saisie. Figure 1.17 Ligne de commande asadmin.
Installation de GlassFish
GlassFish v3 peut être installé sous différents profils (chaque profil définit un ensemble de fonctionnalités et de configurations). Le profil le plus classique en ce qui nous concerne est le profil développeur. Si vous voulez utiliser les fonctionnalités de cluster de GlassFish, en revanche, vous devrez soit l’installer sous le profil cluster, soit mettre à jour votre installation existante en choisissant "Ajouter le support Cluster" à partir de la console d’administration. Pour le moment, il n’existe que le profil développeur pour GlassFish v3 (qui est nécessaire pour exécuter les applications Java EE 6). GlassFish peut être téléchargé via différents mécanismes de distribution. Les choix les plus évidents consistent à le récupérer à partir de l’URL http://glassfish.org, à
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 1
Tour d’horizon de Java EE 6 49
l’installer avec le SDK Java EE ou à utiliser l’EDI NetBeans. Nous expliquerons ici comment le télécharger et l’installer à partir du site communautaire. Rendez-vous sur la page principale de téléchargement, https://glassfish.dev.java. net/public/downloadsindex.html, et choisissez GlassFish Server v3. Sélectionnez l’archive convenant à votre plate-forme et aux besoins de votre système d’exploitation (la distribution Unix fonctionnera avec Linux, Solaris et Mac OS X). L’installation du programme d’installation lancera l’installateur graphique qui : ■■
vous demande d’accepter les termes de la licence ;
■■
vous demande le répertoire où vous souhaitez installer GlassFish ;
■■
vous permet de configurer un nom d’utilisateur et un mot de passe pour l’administrateur (ou crée par défaut un utilisateur anonyme) ;
■■
vous permet de configurer les ports HTTP et d’administration (en vérifiant qu’ils ne sont pas déjà utilisés) ;
■■
installe et active l’outil de mise à jour (les clients pkg et updatetool).
Puis il décompresse une installation préconfigurée de GlassFish avec une configuration par défaut : le port d’administration est 4848, le port HTTP est 8080 et aucun utilisateur admin n’est configuré. L’outil de mise à jour n’est pas installé par défaut ; il le sera à partir du réseau lors du premier démarrage. Lorsqu’il a été correctement installé, GlassFish peut être lancé avec la ligne de commande asadmin suivante (voir Figure 1.18). asadmin start-domain domain1
Vous pouvez ensuite afficher la console d’administration (que nous avons montrée à la Figure 1.16) en faisant pointer votre navigateur vers http://localhost:4848 ou aller sur le serveur web par défaut via http://localhost:8080. INFO Si vous n’avez qu’un seul domaine, vous pouvez omettre le nom de domaine par défaut et lancer GlassFish uniquement avec la commande asadmin start-domain. Si vous voulez voir apparaître le journal à l’écran au lieu de consulter le fichier qui lui est consacré (domains/ domain1/logs/server.log), utilisez la commande asadmin start-domain --verbose.
GlassFish a bien d’autres fonctionnalités à offrir : je vous en montrerai quelquesunes au cours de ce livre mais je vous laisserai explorer son support des langages dynamiques (JRuby on Rails, Groovy et Grails, etc.), les services de diagnostic, les
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
50
Java EE 6 et GlassFish 3
règles de gestion, les propriétés système, la surveillance des données, le flux d’appel et les différentes configurations de sécurité. Figure 1.18 Lancement de GlassFish.
Résumé Lorsqu’une société développe une application Java et doit ajouter des fonctionnalités professionnelles comme la gestion des transactions, la sécurité, la concurrence ou la messagerie, Java EE est attractif. Il est standard, les composants sont déployés dans différents conteneurs qui offrent de nombreux services et fonctionnent avec plusieurs protocoles. Java EE 6 suit les traces de sa version précédente en ajoutant la simplicité d’utilisation de la couche web. Cette version est plus légère (grâce à l’élagage, aux profils et à EJB Lite), plus simple d’utilisation (plus besoin d’interfaces sur les EJB ou d’annotations sur la couche web), plus riche (elle ajoute de nouvelles spécifications et fonctionnalités) et, enfin, plus portable (elle inclut un conteneur EJB standardisé et autorise les noms JNDI). La deuxième partie de ce chapitre a été consacrée à la mise en place de l’environnement de développement. Ce livre contient de nombreux extraits de code et des sections "Récapitulatif". Vous aurez besoin de plusieurs outils et frameworks pour compiler, déployer, exécuter et tester ces codes : JDK 1.6, Maven 2, JUnit 4, Derby 10.5 et GlassFish v3. Ce chapitre vous a donné un bref aperçu de Java EE 6. Les suivants étudieront plus en détail ses spécifications.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
2 Persistance en Java Les applications sont composées d’une logique métier, d’interactions avec d’autres systèmes, d’interfaces utilisateur et... de persistance. La plupart des données manipulées par les applications doivent être stockées dans des bases de données pour pouvoir être ensuite récupérées et analysées. Les bases de données sont importantes : elles stockent les données métier, servent de point central entre les applications et traitent les données via des triggers ou des procédures stockées. Les données persistantes sont omniprésentes – la plupart du temps, elles utilisent les bases de données relationnelles comme moteur sous-jacent. Dans un système de gestion de base de données relationnelle, les données sont organisées en tables formées de lignes et de colonnes ; elles sont identifiées par des clés primaires (des colonnes spéciales ne contenant que des valeurs uniques) et, parfois, par des index. Les relations entre tables utilisent les clés étrangères et joignent les tables en respectant des contraintes d’intégrité. Tout ce vocabulaire est totalement étranger à un langage orienté objet comme Java. En Java, nous manipulons des objets qui sont des instances de classes ; les objets héritent les uns des autres, peuvent utiliser des collections d’autres objets et, parfois, se désignent eux-mêmes de façon récursive. Nous disposons de classes concrètes, de classes abstraites, d’interfaces, d’énumérations, d’annotations, de méthodes, d’attributs, etc. Cependant, bien que les objets encapsulent soigneusement leur état et leur comportement, cet état n’est accessible que lorsque la machine virtuelle (JVM) s’exécute : lorsqu’elle s’arrête ou que le ramasse-miettes nettoie la mémoire, tout disparaît. Ceci dit, certains objets n’ont pas besoin d’être persistants : par données persistantes, nous désignons les données qui sont délibérément stockées de façon permanente sur un support magnétique, une mémoire flash, etc. Un objet est persistant s’il peut stocker son état afin de pouvoir le réutiliser plus tard.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
52
Java EE 6 et GlassFish 3
Il existe différents moyens de faire persister l’état en Java. L’un d’eux consiste à utiliser le mécanisme de sérialisation qui consiste à convertir un objet en une suite de bits : on peut ainsi sérialiser les objets sur disque, sur une connexion réseau (notamment Internet), sous un format indépendant des systèmes d’exploitation. Java fournit un mécanisme simple, transparent et standard de sérialisation des objets via l’implémentation de l’interface java.io.Serializable. Cependant, bien qu’elle soit très simple, cette technique est assez fruste : elle ne fournit ni langage d’interrogation ni support des accès concurrents intensifs ou de la mise en cluster. Un autre moyen de mémoriser l’état consiste à utiliser JDBC (Java Database Connectivity), qui est l’API standard pour accéder aux bases de données relationnelles. On peut ainsi se connecter à une base et exécuter des requêtes SQL (Structured Query Language) pour récupérer un résultat. Cette API fait partie de la plate-forme Java depuis la version 1.1 mais, bien qu’elle soit toujours très utilisée, elle a tendance à être désormais éclipsée par les outils de correspondance entre modèle objet et modèle relationnel (ORM, Object-Relational Mapping), plus puissants. Le principe d’un ORM consiste à déléguer l’accès aux bases de données relationnelles à des outils ou à des frameworks externes qui produisent une vue orientée objet des données relationnelles et vice versa. Ces outils établissent donc une correspondance bidirectionnelle entre la base et les objets. Différents frameworks fournissent ce service, notamment Hibernate, TopLink et Java Data Objects (JDO), mais il est préférable d’utiliser JPA (Java Persistence API) car elle est intégrée à Java EE 6.
Résumé de la spécification JPA JPA 1.0 a été créée avec Java EE 5 pour résoudre le problème de la persistance des données en reliant les modèles objets et relationnels. Avec Java EE 6, JPA 2.0 conserve la simplicité et la robustesse de la version précédente tout en lui ajoutant de nouvelles fonctionnalités. Grâce à cette API, vous pouvez accéder à des données relationnelles et les manipuler à partir des EJB (Enterprise Java Beans), des composants web et des applications Java SE. JPA est une couche d’abstraction au-dessus de JDBC, qui fournit une indépendance vis-à-vis de SQL. Toutes les classes et annotations de cette API se trouvent dans le paquetage javax.persistence. Ses composants principaux sont les suivants : ■■
ORM, qui est le mécanisme permettant de faire correspondre les objets à des données stockées dans une base de données relationnelle.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 53
■■
Une API gestionnaire d’entités permettant d’effectuer des opérations sur la base de données, notamment les opérations CRUD (Create, Read, Update, Delete). Grâce à elle, il n’est plus nécessaire d’utiliser directement JDBC.
■■
JPQL (Java Persistence Query Language), qui permet de récupérer des données à l’aide d’un langage de requêtes orienté objet.
■■
Des mécanismes de transaction et de verrouillage lorsque l’on accède de façon concurrente aux données, fournis par JTA (Java Transaction API). Les transactions locales à la ressource (non JTA) sont également reconnues par JPA.
■■
Des fonctions de rappel et des écouteurs permettant d’ajouter la logique métier au cycle de vie d’un objet persistant.
Historique de la spécification
Les solutions ORM existent depuis longtemps, bien avant Java. Des produits comme TopLink ont commencé à être utilisés avec Smalltalk en 1994, avant de basculer vers Java. Les produits ORM commerciaux comme TopLink sont donc disponibles depuis les premiers jours du langage Java. Cependant, bien qu’ils aient prouvé leur utilité, ils n’ont jamais été standardisés pour cette plate-forme. Une approche comparable à ORM a bien été standardisée sous la forme de JDO, mais elle n’a jamais réussi à pénétrer le marché de façon significative. En 1998, EJB 1.0 vit le jour et fut ensuite intégré à J2EE 1.2. Il s’agissait d’un composant distribué lourd, utilisé pour la logique métier transactionnelle. CMP (Entity Container Managed Persistence) fut ensuite ajouté à EJB 1.0 et continua d’évoluer jusqu’à EJB 2.1 (J2EE 1.4). La persistance ne pouvait prendre place qu’à l’intérieur d’un conteneur, via un mécanisme d’instanciation complexe utilisant des interfaces locales ou distantes. Les capacités ORM étaient également très limitées car l’héritage était difficile à traduire en termes relationnels. Parallèlement au monde J2EE, la solution open-source Hibernate apportait des modifications surprenantes en terme de persistance car ce framework fournissait un modèle orienté objet persistant et léger. Après des années de plaintes à propos des composants Entity CMP 2.x et en réponse au succès et à la simplicité des frameworks open-source comme Hibernate, le modèle de persistance de Java EE fut entièrement revu dans Java EE 5 : JPA 1.0 était né et proposait désormais une approche légère, largement inspirée des principes de conception d’Hibernate. La spécification JPA 1.0 a donc été intégrée à EJB 3.0 (JSR 220).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
54
Java EE 6 et GlassFish 3
Aujourd’hui, avec Java EE 6, la seconde version de JPA continue dans cette voie de la simplicité tout en ajoutant de nouvelles fonctionnalités. Elle a évolué pour posséder sa propre spécification, la JSR 317. Nouveautés de JPA 2.0
Si JPA 1.0 était un modèle de persistance entièrement nouveau par rapport à son prédécesseur Entity CMP 2.x, JPA 2.0 est la suite de JPA 1.0, dont elle conserve l’approche orientée objet utilisant les annotations et, éventuellement, des fichiers de correspondance en XML. Cette seconde version ajoute de nouvelles API, étend JPQL et intègre de nouvelles fonctionnalités : ■■
Les collections de types simples (String, Integer, etc.) et d’objets intégrables (embeddable) peuvent désormais être associées à des tables distinctes alors qu’auparavant on ne pouvait associer que des collections d’entités.
■■
Les clés et les valeurs des associations peuvent désormais être de n’importe quel type de base, des entités ou des objets intégrables.
■■
L’annotation @OrderColumn permet maintenant d’avoir un tri persistant.
■■
La suppression des orphelins permet de supprimer les objets fils d’une relation lorsque l’objet parent est supprimé.
■■
Le verrouillage pessimiste a été ajouté au verrouillage optimiste, qui existait déjà.
■■
Une toute nouvelle API de définition de requêtes a été ajoutée afin de pouvoir construire des requêtes selon une approche orientée objet.
■■
La syntaxe de JPQL a été enrichie (elle autorise désormais les expressions case, par exemple).
■■
Les objets intégrables peuvent maintenant être embarqués dans d’autres objets intégrables et avoir des relations avec les entités.
■■
La notation pointée a été étendue afin de pouvoir gérer les objets intégrables avec des relations ainsi que les objets intégrables d’objets intégrables.
■■
Le support d’une nouvelle API de mise en cache a été ajouté.
Nous présenterons en détail toutes ces fonctionnalités aux Chapitres 3, 4 et 5. Implémentation de référence
EclipseLink 1.1 est une implémentation open-source de JPA 2.0, mais ce framework souple et puissant supporte également la persistance XML via JAXB (Java XML
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 55
Binding) et d’autres techniques comme SDO (Service Data Objects). Il offre un ORM, un OXM (Object XML Mapping) et la persistance des objets sur EIS (Enterprise Information Systems) à l’aide de JCA (Java EE Connector Architecture). Les origines d’EclipseLink remontent au produit TopLink d’Oracle, qui a été offert à la fondation Eclipse en 2006. C’est l’implémentation de référence de JPA et c’est le framework de persistance que nous utiliserons dans ce livre. Il est également désigné sous les termes de fournisseur de persistance ou, simplement, de fournisseur.
Comprendre les entités Lorsque l’on évoque l’association d’objets à une base de données relationnelle, la persistance des objets ou les requêtes adressées aux objets, il est préférable d’utiliser le terme d’entités plutôt que celui d’objets. Ces derniers sont des instances qui existent en mémoire ; les entités sont des objets qui ont une durée de vie limitée en mémoire et qui persistent dans une base de données. Les entités peuvent être associées à une base de données, être concrètes ou abstraites, et elles disposent de l’héritage, peuvent être mises en relation, etc. Une fois associées, ces entités peuvent être gérées par JPA. Vous pouvez stocker une entité dans la base de données, la supprimer et l’interroger à l’aide d’un langage de requête (Java Persistence Query Language, ou JPQL). Un ORM vous permet de manipuler des entités alors qu’en coulisse c’est à la base de données qu’on accède. Comme nous le verrons, une entité a un cycle de vie bien défini et, grâce aux méthodes de rappel et aux écouteurs, JPA vous permet d’associer du code métier à certains événements de ce cycle. ORM = Object-Relational Mapping
Le principe d’un ORM consiste à déléguer à des outils ou à des frameworks externes (JPA, dans notre cas) la création d’une correspondance entre les objets et les tables. Le monde des classes, des objets et des attributs peut alors être associé aux bases de données constituées de tables formées de lignes et de colonnes. Cette association offre une vue orientée objet aux développeurs, qui peuvent alors utiliser de façon transparente des entités à la place des tables. JPA utilise les métadonnées pour faire correspondre les objets à une base de données.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
56
Java EE 6 et GlassFish 3
Les métadonnées sont associées à chaque entité pour décrire son association : elles permettent au fournisseur de persistance de reconnaître une entité et d’interpréter son association. Ces métadonnées peuvent s’exprimer dans deux formats différents : ■■
Annotations. Le code de l’entité est directement annoté avec toutes sortes d’annotations décrites dans le paquetage javax.persistence.
■■
Descripteurs XML. Ils peuvent être utilisés à la place (ou en plus) des annotations. L’association est définie dans un fichier XML externe qui sera déployé avec les entités. Cette technique peut être très utile lorsque la configuration de la base de données varie en fonction de l’environnement, par exemple.
Pour faciliter les correspondances, JPA (comme de nombreuses autres fonctionnalités de Java EE 6) utilise le concept de "convention plutôt que configuration" (également appelé "configuration par exception" ou "programmation par exception"). Le principe est que JPA utilise un certain nombre de règles de correspondance par défaut (le nom de la table est le même que celui de l’entité, par exemple) : si ces règles vous satisfont, vous n’avez pas besoin de métadonnées supplémentaires (aucune annotation ni XML ne sont alors nécessaires) mais, dans le cas contraire, vous pouvez adapter la correspondance à vos propres besoins à l’aide des métadonnées. En d’autres termes, fournir une configuration est une exception à la règle. Voyons un exemple. Le Listing 2.1 présente une entité Livre avec quelques attributs. Comme vous pouvez le constater, certains sont annotés (id, titre et description) alors que d’autres ne le sont pas. Listing 2.1 : Une entité Book simple @Entity public class Book { @Id @GeneratedValue private Long id; @Column(nullable = false) private String title; private Float price; @Column(length = 2000) private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 57
Pour être reconnue comme entité, la classe Book doit être annotée par @javax. persistence.Entity (ou son équivalent XML). L’annotation @javax. persistence.Id sert à indiquer la clé primaire, et la valeur de cet identifiant est automatiquement générée par le fournisseur de persistance (@GeneratedValue). L’annotation @Column est utilisée avec certains attributs pour adapter la correspondance par défaut des colonnes (title ne peut plus contenir NULL et description a une longueur de 2 000 caractères). Le fournisseur de persistance pourra ainsi faire correspondre à l’entité Book une table BOOK (règle de correspondance par défaut), produire une clé primaire et synchroniser les valeurs des attributs vers les colonnes de la table. La Figure 2.1 montre cette association entre l’entité et la table. Figure 2.1 L’entité Book est associée à la table BOOK.
Book -id : Long -title : String -price : Float -description : String -nbOfPage : Integer -illustrations : Boolean
Association
+ID TITLE PRICE DESCRIPTION ISBN NBOFPAGE ILLUSTRATIONS
Comme nous le verrons au Chapitre 3, cette correspondance est riche et vous permet d’associer toutes sortes de choses. Le monde de la programmation orientée objet abonde de classes et d’associations entre elles (et les collections de classes). Les bases de données modélisent également les relations, mais différemment : en utilisant des clés étrangères ou des jointures. JPA dispose donc d’un ensemble de métadonnées permettant de gérer cette correspondance entre ces deux visions des relations. L’héritage peut également être traduit : bien que ce soit un mécanisme fréquemment utilisé en programmation pour réutiliser le code, ce concept est inconnu des bases de données relationnelles (elles doivent le simuler avec des clés étrangères et des contraintes). Même si cette traduction de l’héritage implique quelques contorsions, JPA l’autorise et vous offre différentes stratégies pour y parvenir. Nous les décrirons au Chapitre 3. Interrogation des entités
JPA permet de faire correspondre les entités à des bases de données et de les interroger en utilisant différents critères. La puissance de cette API vient du fait qu’elle offre la possibilité d’interroger les entités et leurs relations de façon orientée objet sans devoir utiliser les clés étrangères ou les colonnes de la base de données sousjacente. L’élément central de l’API, responsable de l’orchestration des entités, est le
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
58
Java EE 6 et GlassFish 3
gestionnaire d’entités : son rôle consiste à gérer les entités, à lire et à écrire dans une base de données et à autoriser les opérations CRUD simples sur les entités, ainsi que des requêtes complexes avec JPQL. D’un point de vue technique, le gestionnaire d’entités n’est qu’une interface dont l’implémentation est donnée par le fournisseur de persistance, EclipseLink. L’extrait de code suivant montre comment créer un gestionnaire d’entités et rendre une entité Livre persistante : EntityManagerFactory emf = Persistence.createEntityManagerFactory("chapter02PU"); EntityManager em = emf.createEntityManager(); em.persist(livre);
La Figure 2.2 montre comment l’interface EntityManager peut être utilisée par une classe (Main, ici) pour manipuler des entités (Livre, ici). Grâce à des méthodes comme persist() et find(), le gestionnaire d’entités masque les appels JDBC adressés à la base de données, ainsi que les instructions SQL INSERT ou SELECT. Main
Book -id : Long -title : String -price : Float -description : String -nbOfPage : Integer -illustrations : Boolean
Figure 2.2 Le gestionnaire d’entités interagit avec l’entité et la base de données sous-jacente.
Le gestionnaire d’entités permet également d’interroger les entités. Dans ce cas, une requête JPA est semblable à une requête sur une base de données, sauf qu’elle utilise JPQL au lieu de SQL. La syntaxe utilise la notation pointée habituelle. Pour récupérer, par exemple, tous les livres intitulés H2G2, il suffirait d’écrire : SELECT b FROM Book b WHERE b.title = ’H2G2’
Une instruction JPQL peut exécuter des requêtes dynamiques (créées à l’exécution), des requêtes statiques (définies lors de la compilation), voire des instructions SQL natives. Les requêtes statiques, également appelées requêtes nommées, sont définies par des annotations ou des métadonnées XML. L’instruction JPQL précédente peut, par exemple, être définie comme une requête nommée sur l’entité Livre.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 59
Le Listing 2.2 montre une entité Book définissant la requête nommée Title à l’aide de l’annotation @NamedQuery.
findBookBy-
Listing 2.2 : Une requête nommée findBookByTitle @Entity @NamedQuery(name = "findBookByTitle", „ query = "SELECT b FROM Book b WHERE b.title =’H2G2’") public class Book { @Id @GeneratedValue private Long id; @Column(nullable = false) private String title; private Float price; @Column(length = 2000) private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; }
// Constructeurs, getters, setters
Comme nous le verrons au Chapitre 4, la méthode EntityManager.createNamedQuery() permet d’exécuter la requête et renvoie une liste d’entités Book correspondant aux critères de recherche. Méthodes de rappel et écouteurs
Les entités sont simplement des POJO (Plain Old Java Objects) qui sont gérés ou non par le gestionnaire d’entités. Lorsqu’elles sont gérées, elles ont une identité de persistance et leur état est synchronisé avec la base de données. Lorsqu’elles ne le sont pas (elles sont, par exemple, détachées du gestionnaire d’entités), elles peuvent être utilisées comme n’importe quelle autre classe Java : ceci signifie que les entités ont un cycle de vie, comme le montre la Figure 2.3. Lorsque vous créez une instance de l’entité Book à l’aide de l’opérateur new, l’objet existe en mémoire et JPA ne le connaît pas (il peut même finir par être supprimé par le ramasse-miettes) ; lorsqu’il devient géré par le gestionnaire d’entités, son état est associé et synchronisé avec la table BOOK. L’appel de la méthode EntityManager.remove() supprime les données de la base, mais l’objet Java continue d’exister en mémoire jusqu’à ce que le ramasse-miettes le détruise. Les opérations qui s’appliquent aux entités peuvent se classer en quatre catégories : persistance, mise à jour, suppression et chargement, qui correspondent respectivement aux opérations d’insertion, de mise à jour, de suppression et de sélection dans
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
60
Java EE 6 et GlassFish 3
la base de données. Chaque opération a un événement "Pre" et "Post" (sauf le chargement, qui n’a qu’un événement "Post") qui peuvent être interceptés par le gestionnaire d’entités pour invoquer une méthode métier. Figure 2.3 Cycle de vie d’une entité. Existe en mémoire
Détaché
Géré
Supprimé
Base de données
Comme nous le verrons au Chapitre 5, il existe donc des annotations @PrePersist, @PostPersist, etc. Ces annotations peuvent être associées à des méthodes d’entités (appelées fonctions de rappel) ou à des classes externes (appelées écouteurs). Vous pouvez considérer les fonctions de rappel et les écouteurs comme des triggers d’une base de données relationnelle.
Récapitulatif Maintenant que vous connaissez un peu JPA, EclipseLink, les entités, le gestionnaire d’entités et JPQL, rassemblons le tout pour écrire une petite application qui stocke une entité dans une base de données. Nous allons donc écrire une simple entité Book et une classe Main chargée de stocker un livre. Nous la compilerons avec Maven 2 et l’exécuterons avec EclipseLink et une base de données cliente Derby. Pour montrer la simplicité des tests unitaires sur une entité, nous verrons également comment écrire une classe de test (BookTest) avec un cas de test JUnit 4 et à l’aide du mode intégré de Derby, qui nous permettra de stocker les données en utilisant une base de données en mémoire. Pour respecter la structure de répertoires de Maven, les fichiers devront être placés dans les répertoires suivants : ■■ src/main/java
pour l’entité Book et la classe Main ;
■■ src/main/resources pour le fichier persistence.xml utilisé par la classe Main ;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
■■ src/test/java
Persistance en Java 61
pour la classe BookTest, qui servira aux tests unitaires ;
■■ src/test/resources pour le fichier persistence.xml utilisé par les cas de test ; ■■ pom.xml,
le Project Object Model (POM) de Maven, qui décrit le projet et ses dépendances vis-à-vis d’autres modules et composants externes.
Écriture de l’entité Book
L’entité
présentée dans le Listing 2.3 doit être développée sous le répertoire Elle a plusieurs attributs (un titre, un prix, etc.) de types différents (String, Float, Integer et Boolean) et certaines annotations JPA : Book
src/main/java.
informe le fournisseur de persistance que cette classe est une entité et qu’il devra la gérer.
■■ @Entity ■■ @Id
définit l’attribut id comme étant la clé primaire.
informe le fournisseur de persistance qu’il devra produire automatiquement la clé primaire à l’aide des outils de la base de données sous-jacente.
■■ @GeneratedValue
précise que le titre ne pourra pas être NULL lorsqu’il sera stocké dans la base et modifie la longueur maximale par défaut de la colonne description.
■■ @Column
définit une requête nommée qui utilise JPQL pour récupérer tous les livres de la base.
■■ @NamedQuery
Listing 2.3 : Une entité Book avec une requête nommée package com.apress.javaee6.chapter02; @Entity @NamedQuery(name = "findAllBooks", query = "SELECT b FROM Book b") public class Book { @Id @GeneratedValue private Long id; @Column(nullable = false) private String title; private Float price; @Column(length = 2000) private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
62
Java EE 6 et GlassFish 3
Pour des raisons de lisibilité, nous avons omis ici les constructeurs, les getters et les setters de cette classe. Comme le montre ce code, hormis les quelques annotations, Book est un simple POJO. Écrivons maintenant une classe Main qui stockera un livre dans la base de données. Écriture de la classe Main
La classe Main présentée dans le Listing 2.4 se trouve dans le même répertoire que l’entité Livre. Elle commence par créer une instance de Book (avec le mot-clé new de Java) puis initialise ses attributs. Vous remarquerez qu’il n’y a rien de spécial ici : ce n’est que du code Java traditionnel. Puis elle utilise la classe Persistence pour obtenir une instance d’EntityManagerFactory afin de désigner une unité de persistance appelée chapter02PU que nous décrirons plus tard dans la section "Unité de persistance pour la classe Main". Cette fabrique permet à son tour de créer une instance d’EntityManager (la variable em). Comme on l’a déjà mentionné, le gestionnaire d’entités est l’élément central de JPA car il permet de créer une transaction, de stocker l’objet Book à l’aide de la méthode EntityManager.persist() puis de valider la transaction. À la fin de la méthode main() on ferme les objets EntityManager et EntityManagerFactory afin de libérer les ressources du fournisseur. Listing 2.4 : Une classe Main pour stocker une entité Book package com.apress.javaee6.chapter02; public class Main { public static void main(String[] args) { // Crée une instance de Book Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Comédie de science fiction"); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); // Obtention d’un gestionnaire d’entités et d’une transaction EntityManagerFactory emf = Persistence.createEntityManagerFactory("chapter02PU"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); // Stocke le livre dans la base tx.begin(); em.persist(book);
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 63
tx.commit(); em.close(); emf.close(); } }
Là encore, nous avons omis la gestion des exceptions pour des raisons de lisibilité. Si une exception de persistance survenait, il faudrait annuler la transaction et enregistrer un message dans le journal. Unité de persistance pour la classe Main
Comme vous pouvez le constater avec la classe Main, l’objet EntityManagerFactory a besoin d’une unité de persistance appelée chapter02PU qui doit être définie dans le fichier persistence.xml situé dans le répertoire src/main/resources/META-INF (voir Listing 2.5). Ce fichier, exigé par la spécification de JPA, est important car c’est lui qui relie le fournisseur JPA (EclipseLink dans notre cas) à la base de données (Derby). Il contient toutes les informations nécessaires pour se connecter à la base (cible, URL, pilote JDBC, nom et mot de passe de l’utilisateur) et informe le fournisseur du mode de génération de la base (create-tables signifie que les tables seront créées si elles n’existent pas). L’élément définit le fournisseur de persistance – EclipseLink ici. Listing 2.5 : Le fichier persistence.xml utilisé par la classe Main org.eclipse.persistence.jpa.PersistenceProvider com.apress.javaee6.chapter02.Book
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
64
Java EE 6 et GlassFish 3
Cette unité de persistance énumère toutes les entités qui doivent être gérées par le gestionnaire d’entités. Ici, le marqueur désigne l’entité Book. Compilation avec Maven
Vous disposez maintenant de tous les ingrédients pour lancer l’application : l’entité Book que vous voulez stocker, la classe Main qui effectue le travail à l’aide d’un gestionnaire d’entités et l’unité de persistance qui relie l’entité à la base de données Derby. Pour compiler ce code, nous utiliserons Maven au lieu d’appeler directement le compilateur javac. Vous devez donc d’abord créer un fichier pom.xml décrivant le projet et ses dépendances (JPA, notamment). Vous devrez également informer Maven que vous utilisez Java SE 6 en configurant l’extension maven-compiler-plugin comme cela est décrit dans le Listing 2.6. Listing 2.6 : Fichier pom.xml de Maven pour compiler, construire, exécuter et tester l’application 4.0.0 com.apress.javaee6 chapter02 1.0 chapter02 org.eclipse.persistence javax.persistence 1.1.0 org.eclipse.persistence eclipselink 1.1.0 org.apache.derby derbyclient 10.5.3.0
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 65
org.apache.derby derby 10.5.3.0 test junit junit 4.5 test org.apache.maven.plugins maven-compiler-plugin true 1.6 1.6
Pour compiler le code, vous avez d’abord besoin de l’API de JPA, qui définit toutes les annotations et les classes qui se trouvent dans le paquetage javax.persistence. Vous obtiendrez ces classes dans une archive jar désignée par l’identifiant d’artéfact javax.persistence et qui sera stockée dans le dépôt Maven. Le runtime EclipseLink (c’est-à-dire le fournisseur de persistance) est défini dans l’identifiant d’artéfact eclipselink. Vous avez également besoin des pilotes JDBC permettant de se connecter à Derby. L’identifiant d’artéfact derbyclient désigne l’archive jar qui contient le pilote JDBC pour se connecter à Derby lorsqu’il s’exécute en mode serveur (il est alors lancé dans un processus séparé et écoute sur un port), tandis que l’identifiant d’artéfact derby contient les classes pour utiliser Derby comme une base de données intégrée. Notez que ce dernier est réservé aux tests (test) et dépend de JUnit 4. Pour compiler les classes, ouvrez une fenêtre de commande dans le répertoire racine contenant le fichier pom.xml, puis entrez la commande Maven suivante : mvn compile
Vous devriez voir apparaître le message BUILD SUCCESSFUL vous informant que la compilation a réussi. Maven crée alors un sous-répertoire target contenant tous les fichiers class ainsi que le fichier persistence.xml.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
66
Java EE 6 et GlassFish 3
Exécution de la classe Main avec Derby
Avant d’exécuter la classe Main, vous devez lancer Derby. Pour ce faire, le moyen le plus simple consiste à se rendre dans le répertoire %DERBY_HOME%\bin et à lancer le script startNetworkServer.bat. Derby se lance et affiche les messages suivants sur la console : 2010-01-31 14:56:54.816 GMT : Le gestionnaire de sécurité a été installé au moyen de la stratégie de sécurité de serveur de base. 2010-01-31 14:56:55.562 GMT : Apache Derby Serveur réseau - 10.5.3.0 (802917) démarré et prêt à accepter les connexions sur le port 1527
Le processus Derby écoute sur le port 1527 et attend que le pilote JDBC lui envoie une instruction SQL. Pour exécuter la classe Main, vous pouvez utiliser l’interpréteur java ou la commande Maven suivante : mvn exec:java -Dexec.mainClass="com.apress.javaee6.chapter02.Main"
Lorsque vous exécutez la classe Main, plusieurs choses se passent. Tout d’abord, Derby crée automatiquement la base de données chapter02tDB dès que l’entité Book est initialisée car nous avions ajouté la propriété create=true à l’URL de JDBC dans le fichier persistence.xml :
Ce raccourci est très pratique lorsque l’on est en phase de développement car nous n’avons pas besoin d’écrire un script SQL pour créer la base. Puis la propriété eclipselink.ddl-generation demande à EclipseLink de créer automatiquement la table LIVRE. Enfin, le livre est inséré dans cette table (avec un identifiant produit automatiquement). em.persist(book);
Utilisons maintenant les commandes Derby pour afficher la structure de la table : tapez la commande ij dans une fenêtre de commandes (comme on l’a expliqué plus haut, le répertoire %DERBY_HOME%\bin doit avoir été ajouté à votre variable PATH). Cette commande lance l’interpréteur de Derby à partir duquel vous pouvez exécuter des commandes pour vous connecter à la base, afficher les tables de la base chapter02DB (show tables), vérifier la structure de la table BOOK (describe book) et même consulter son contenu à l’aide d’instructions SQL comme SELECT * FROM BOOK. C:\> ij version ij 10.5
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Comme nous avons utilisé l’annotation @GeneratedValue pour produire automatiquement un identifiant dans l’entité Book, EclipseLink a créé une table de séquence pour stocker la numérotation (table SEQUENCE). En étudiant la structure de la table BOOK, on constate que JPA a respecté certaines conventions par défaut pour nommer la table et ses colonnes d’après les noms de l’entité et de ses attributs. L’annotation @Column a redéfini certaines de ces conventions, comme la longueur de la colonne description, qui a été fixée à 2000. Écriture de la classe BookTest
On a reproché aux versions précédentes d’Entity CMP 2.x la complexité de mise en place des tests unitaires pour les composants persistants. L’un des atouts principaux de JPA est, justement, que l’on peut aisément tester les entités sans avoir besoin d’un serveur d’application ou d’une base de données. Que peut-on tester, alors ? Les entités n’ont généralement pas besoin d’être testées isolément car la plupart des méthodes sur les entités sont de simples getters ou setters ; elles contiennent peu de méthodes métier. Vérifier qu’un setter affecte une valeur à un attribut et que le getter correspondant permet de récupérer cette même valeur n’apporte pas grand-chose (sauf si cela permet de détecter un effet de bord dans les getters ou les setters). Qu’en est-il des tests des requêtes ? Certains développeurs prétendront qu’il ne s’agit pas de tests unitaires puisqu’il faut une vraie base de données pour les exécuter. Effectuer des tests séparés avec des objets factices demanderait beaucoup de travail. En outre, tester une entité à l’extérieur de tout conteneur (EJB ou conteneur de servlet) aurait des répercussions sur le code car il faudrait modifier la gestion des transactions. L’utilisation d’une base de données en mémoire et des transactions non JPA semble donc un bon compromis. Les opérations CRUD et les requêtes JPQL
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
68
Java EE 6 et GlassFish 3
peuvent en effet être testées avec une base de données très légère qui n’a pas besoin de s’exécuter dans un processus distinct (il suffit d’ajouter un fichier jar au classpath). C’est de cette façon que nous exécuterons notre classe BookTest, en utilisant le mode intégré de Derby. Maven utilise deux répertoires différents : l’un pour stocker le code de l’application, un autre pour les classes de test. La classe BookTest, présentée dans le Listing 2.7, est placée dans le répertoire src/test/java et teste que le gestionnaire d’entités peut stocker un livre dans la base de données et le récupérer ensuite. Listing 2.7 : Classe de test qui crée un livre et récupère tous les livres de la base de données public class BookTest { private static EntityManagerFactory emf; private static EntityManager em; private static EntityTransaction tx; @BeforeClass public static void initEntityManager() throws Exception { emf = Persistence.createEntityManagerFactory("chapter02PU"); em = emf.createEntityManager(); } @AfterClass public static void closeEntityManager() throws SQLException { em.close(); emf.close(); } @Before public void initTransaction() { tx = em.getTransaction(); } @Test public void createBook() throws Exception { // Création d’une instance de Livre Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Comédie de science fiction"); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false);
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 69
// Stocke le livre dans la base de données tx.begin(); em.persist(book); tx.commit(); assertNotNull("ID ne doit pas être null", book.getId()); // Récupère tous les livres de la base de données List books = em.createNamedQuery("findAllBooks").getResultList(); assertNotNull(book); } }
Comme la classe Main, BookTest doit créer une instance EntityManager à l’aide d’une fabrique EntityManagerFactory. Pour initialiser ces composants, nous nous servons des fixtures de JUnit 4 : les annotations @BeforeClass et @AfterClass permettent d’exécuter un code une seule fois, avant et après l’exécution de la classe – c’est donc l’endroit idéal pour créer et fermer une instance EntityManager. L’annotation @Before, quant à elle, permet d’exécuter un certain code avant chaque test – c’est là que nous créons une transaction. Le cas de test est représenté par la méthode createBook() car elle est précédée de l’annotation @Test de JUnit. Cette méthode stocke un livre (en appelant EntityManager.persist()) et vérifie avec assertNotNull que l’identifiant a bien été produit automatiquement par EclipseLink. En ce cas, la requête nommée findAllBooks est exécutée et l’on vérifie que la liste renvoyée n’est pas null. Unité de persistance pour la classe BookTest
Maintenant que la classe de test est écrite, vous avez besoin d’un autre fichier persistence.xml pour utiliser Derby intégré car le précédent définissait un pilote JDBC et une URL de connexion Derby en mode serveur réseau. Le fichier src/ test/resources/META-INF/persistence.xml du Listing 2.8 utilise au contraire un pilote JDBC pour le mode intégré. Listing 2.8 : Fichier persistence.xml utilisé par la classe BookTest org.eclipse.persistence.jpa.PersistenceProvider
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
70
Java EE 6 et GlassFish 3
com.apress.javaee6.chapter02.Book
Il y a d’autres différences entre les deux fichiers persistence.xml. La valeur de ddl-generation est ici drop-and-create-tables au lieu de create-tables car, avant de tester, il faut supprimer et recréer les tables afin de repartir sur une structure de base de données propre. Notez également que le niveau des journaux est FINE au lieu d’INFO car cela permet d’obtenir plus d’informations au cas où les tests échoueraient. Exécution de la classe BookTest avec Derby intégré
L’exécution du test est très simple puisqu’il suffit de se reposer sur Maven. Ouvrez une fenêtre de commande dans le répertoire où se trouve le fichier pom.xml et tapez la commande suivante : mvn test
Le niveau de journalisation des traces étant FINE, vous devriez voir apparaître de nombreuses informations indiquant que Derby crée une base et des tables en mémoire. Puis la classe BookTest est exécutée et Maven devrait vous informer du succès du test. Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.415 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO]---------------------------------------------------------------[INFO] BUILD SUCCESSFUL [INFO]---------------------------------------------------------------[INFO] Total time: 19 seconds [INFO] Finished [INFO] Final Memory: 4M/14M [INFO]----------------------------------------------------------------
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 2
Persistance en Java 71
Résumé Ce chapitre est un tour d’horizon rapide de JPA 2.0. Comme la plupart des autres spécifications de Java EE 6, JPA utilise une architecture objet simple et abandonne le modèle composant lourd de son ancêtre (EJB CMP 2.x). Ce chapitre a également présenté les entités, qui sont des objets persistants utilisant des métadonnées exprimées via des annotations ou un fichier XML. Dans la section "Récapitulatif", nous avons vu comment lancer une application JPA avec EclipseLink et Derby. Les tests unitaires jouent un rôle important dans les projets : avec JPA et des bases de données en mémoire comme Derby, la persistance peut désormais être testée très simplement. Dans les chapitres suivants, vous en apprendrez plus sur les composants principaux de JPA. Le Chapitre 3 expliquera comment associer les entités, les relations et l’héritage à une base de données. Le Chapitre 4 sera consacré à l’API du gestionnaire d’entités, à la syntaxe de JPQL et à l’utilisation des requêtes et des mécanismes de verrouillage. Le Chapitre 5, le dernier de cette présentation de JPA, expliquera le cycle de vie des entités et montrera comment ajouter une logique métier dans les fonctions de rappel et les écouteurs.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
3 ORM : Object-Relational Mapping Dans ce chapitre, nous passerons en revue les bases des ORM (Object-Relational Mapping), qui consistent essentiellement à faire correspondre des entités à des tables et des attributs à des colonnes. Nous nous intéresserons ensuite à des associations plus complexes comme les relations, la composition et l’héritage. Un modèle objet est composé d’objets interagissant ensemble ; or les objets et les bases de données utilisent des moyens différents pour stocker les informations sur ces relations (via des pointeurs ou des clés étrangères). Les bases de données relationnelles ne disposent pas naturellement du concept d’héritage et cette association entre objets et bases n’est par conséquent pas évidente. Nous irons donc dans les détails et présenterons des exemples qui montreront comment les attributs, les relations et l’héritage peuvent être traduits d’un modèle objet vers une base de données. Les chapitres précédents ont montré que les annotations étaient très utilisées dans l’édition Entreprise depuis Java EE 5 (essentiellement pour les EJB, JPA et les services web). JPA 2.0 poursuit dans cette voie et introduit de nouvelles annotations de mapping (associations), ainsi que leurs équivalents XML. Même si nous utiliserons surtout les annotations pour expliquer les différents concepts d’associations, nous présenterons également les associations au moyen de XML.
Association d’une entité Comme premier exemple, commençons par l’association la plus simple possible. Dans le modèle de persistance de JPA, une entité est un objet Java classique (POJO) : ceci signifie qu’une entité est déclarée, instanciée et utilisée comme n’importe quelle autre classe Java. Une entité possède des attributs (son état) qui peuvent être manipulés au moyen de getters et de setters. Chaque attribut est stocké dans une colonne d’une table. Le Listing 3.1 présente une entité simple.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
74
Java EE 6 et GlassFish 3
Listing 3.1 : Exemple d’entité Book @Entity public class Book { @Id private private private private private private private
Cet exemple de code issu de l’application CD-BookStore représente une entité Book dans laquelle on a omis les getters et les setters pour plus de clarté. Comme vous pouvez le constater, à part les annotations, cette entité ressemble exactement à n’importe quelle classe Java : elle a plusieurs attributs (id, title, price, etc.) de différents types (Long, String, Float, Integer et Boolean), un constructeur par défaut et des getters et setters pour chaque attribut. Les annotations vont permettre d’associer très simplement cette entité à une table dans une base de données. Tout d’abord, la classe est annotée avec @javax.persistence.Entity, ce qui permet au fournisseur de persistance de la reconnaître comme une classe persistance et non plus comme une simple classe POJO. Puis l’annotation @javax.persistence.Id définit l’identifiant unique de l’objet. JPA étant destiné à associer des objets à des tables relationnelles, les objets doivent posséder un identifiant qui sera associé à une clé primaire. Les autres attributs (title, price, description, etc.) ne sont pas annotés et seront donc stockés dans la table en appliquant une association standard. Cet exemple de code ne contient que des attributs mais, comme nous le verrons au Chapitre 5, une entité peut également avoir des méthodes métier. Notez que cette entité Book est une classe Java qui n’implémente aucune interface et qui n’hérite d’aucune classe. En fait, pour être une entité, une classe doit respecter les règles suivantes : ■■
La classe de l’entité doit être annotée par @javax.persistence.Entity (ou dénotée comme telle dans le descripteur XML).
■■
L’annotation @javax.persistence.Id doit être utilisée pour désigner une clé p rimaire simple.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 75
■■
La classe de l’entité doit posséder un constructeur sans paramètre, public ou protégé. Elle peut également avoir d’autres constructeurs.
■■
La classe de l’entité doit être une classe de premier niveau. Une énumération ou une interface ne peut pas être considérée comme une entité.
■■
La classe de l’entité ne peut pas être finale et aucune méthode ou variable d’instance persistante ne peut être finale non plus.
■■
Si une instance d’entité doit être passée par valeur sous forme d’objet détaché (via une interface distante, par exemple), la classe de l’entité doit implémenter l’interface Serializable.
L’entité Book du Listing 3.1 respectant ces règles simples, le fournisseur de persistance peut synchroniser les données entre les attributs de l’entité et les colonnes de la table BOOK. Par conséquent, si l’attribut isbn est modifié par l’application, la colonne ISBN le sera également (si l’entité est gérée, si le contexte de transaction est actif, etc.). Comme le montre la Figure 3.1, l’entité Book est stockée dans une table BOOK dont chaque colonne porte le nom de l’attribut correspondant de la classe (l’attribut isbn de type String est associé à une colonne ISBN de type VARCHAR). Ces règles d’associations par défaut sont un aspect important du principe appelé "convention plutôt que configuration" (ou "configuration par exception"). Figure 3.1
Book
Synchronisation des données entre l’entité et la table.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
76
Java EE 6 et GlassFish 3
Configuration par exception
Java EE 5 a introduit l’idée de configuration par exception. Ceci signifie que, sauf mention contraire, le conteneur ou le fournisseur doivent appliquer les règles par défaut. En d’autres termes, fournir une configuration est une exception à la règle. Cette politique permet donc de configurer une application avec un minimum d’effort. Revenons à l’exemple précédent (celui du Listing 3.1). Sans annotation, l’entité Book serait traitée comme n’importe quel POJO et ne serait pas persistante – c’est la règle : sans configuration spéciale, le comportement par défaut s’applique et il consiste évidemment à considérer que la classe Book est une classe comme les autres. Comme nous souhaitons modifier ce comportement, nous annotons la classe avec @ Entity. Il en va de même pour l’identifiant : nous avons besoin d’indiquer au fournisseur de persistance que cet attribut doit être associé à une clé primaire, et c’est la raison pour laquelle nous l’annotons avec @Id. Ce type de décision caractérise bien la politique de configuration par exception : les annotations ne sont pas nécessaires dans le cas général ; elles ne sont utilisées que pour outrepasser une convention. Ceci signifie donc que tous les autres attributs de notre classe seront associés selon les règles par défaut : ■■
Le nom de l’entité est associé à un nom de table relationnelle (l’entité Book sera donc associée à une table BOOK). Si vous voulez l’associer à une autre table, vous devrez utiliser l’annotation @Table, comme nous le verrons dans la section "Associations élémentaires".
■■
Les noms des attributs sont associés à des noms de colonnes (l’attribut id, ou la méthode getId(), est associé à une colonne ID). Si vous voulez changer ce comportement, vous devrez utiliser l’annotation @Column.
■■
Ce sont les règles JDBC qui s’appliquent pour associer les types primitifs de Java aux types de données de la base. Ainsi, un String sera associé à un VARCHAR, un Long à un BIGINT, un Boolean à un SMALLINT, etc. La taille par défaut d’une colonne associée à un String est de 255 caractères (VARCHAR(255)). Ces règles par défaut peuvent varier en fonction du SGBDR : un String est associé à un VARCHAR avec Derby, mais à un VARCHAR2 avec Oracle ; de la même façon, un Integer est associé à un INTEGER avec Derby, mais à un NUMBER avec Oracle. Les informations concernant la base de données sous-jacentes sont fournies par le fichier persistence.xml, que nous étudierons dans la section "Contexte de persistance" du Chapitre 4.
Selon toutes ces règles, l’entité Book sera donc associée à une table Derby ayant la structure décrite dans le Listing 3.2.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 77
Listing 3.2 : Structure de la table BOOK CREATE TABLE BOOK ( ID BIGINT NOT NULL, TITLE VARCHAR(255), PRICE DOUBLE(52, 0), DESCRIPTION VARCHAR(255), ISBN VARCHAR(255), NBOFPAGE INTEGER, ILLUSTRATIONS SMALLINT, PRIMARY KEY (ID) );
C’est donc un exemple d’association très simple. Les relations et l’héritage ont également des règles d’association par défaut, que nous étudierons dans la section "Association des relations". La plupart des fournisseurs de persistance, dont EclipseLink, permettent de produire automatiquement la base de données à partir des entités. Cette fonctionnalité est tout spécialement pratique lorsque l’on est en phase de développement car, avec uniquement les règles par défaut, on peut associer très simplement les données en se contentant des annotations @Entity et @Id. Cependant, la plupart du temps, nous devrons nous connecter à un SGBDR classique ou suivre des conventions de nommage strictes : c’est la raison pour laquelle JPA définit un nombre important d’annotations (ou leurs équivalents XML) – vous pourrez ainsi personnaliser chaque partie de l’association (les noms des tables et des colonnes, les clés primaires, la taille des colonnes, les colonnes NULL ou NOT NULL, etc.).
Associations élémentaires D’importantes différences existent entre la gestion des données par Java et par un SGBDR. En Java, nous utilisons des classes pour décrire à la fois les attributs qui contiennent les données et les méthodes qui accèdent et manipulent ces données. Lorsqu’une classe a été définie, nous pouvons créer autant d’instances que nécessaire à l’aide du mot-clé new. Dans un SGBDR, en revanche, seules les données sont stockées – pas les comportements (exception faite des triggers et des procédures stockées) –, et la structure du stockage est totalement différente de la structure des objets puisqu’elle utilise une décomposition en lignes et en colonnes. L’association d’objets Java à une base de données sous-jacente peut donc être simple et se contenter des règles par défaut ; parfois, cependant, ces règles peuvent ne pas convenir aux besoins, auquel cas nous sommes obligés de les outrepasser. Les annotations
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
78
Java EE 6 et GlassFish 3
des associations élémentaires permettent de remplacer les règles par défaut pour la table, les clés primaires et les colonnes, et de modifier certaines conventions de nommage ou de contenu des colonnes (valeurs non nulles, longueur, etc.). Tables
La convention établit que les noms de l’entité et de la table sont identiques (une entité Book est associée à une table BOOK, une entité AncientBook, à une table ANCIENTBOOK, etc.). Toutefois, si vous le souhaitez, vous pouvez associer vos données à une table différente, voire associer une même entité à plusieurs tables. @Table
L’annotation @javax.persistence.Table permet de modifier les règles par défaut pour les tables. Vous pouvez, par exemple, indiquer le nom de la table dans laquelle vous voulez stocker vos données, le catalogue et le schéma de la base. Le Listing 3.3 montre comment associer la table T_BOOK à l’entité Book. Listing 3.3 : Association de l’entité Book à la table T_BOOK @Entity @Table(name = "t_book") public class Book { @Id private private private private private private private
INFO Dans l’annotation @Table, le nom de la table est en minuscules (t_book). Par défaut, la plupart des SGBDR lui feront correspondre un nom en majuscules (c’est notamment le cas de Derby), sauf si vous les configurez pour qu’ils respectent la casse.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 79
@SecondaryTable
Jusqu’à maintenant, nous avons toujours supposé qu’une entité n’était associée qu’à une seule table, également appelée table primaire. Si l’on a déjà un modèle de données, en revanche, on voudra peut-être disséminer les données sur plusieurs tables, ou tables secondaires. Cette annotation permet de mettre en place cette configuration. permet d’associer une table secondaire à une entité, alors que @SecondaryTables (avec un "s") en associe plusieurs. Vous pouvez distribuer les données d’une entité entre les colonnes de la table primaire et celles des tables secondaires en définissant simplement les tables secondaires avec des annotations, puis en précisant pour chaque attribut la table dans laquelle il devra être stocké (à l’aide de l’annotation @Column, que nous décrirons dans la section "Attributs"). Le Listing 3.4 montre comment répartir les attributs d’une entité Address entre une table primaire et deux tables secondaires. @SecondaryTable
Listing 3.4 : Les attributs de l’entité Address sont répartis dans trois tables différentes @Entity @SecondaryTables({ @SecondaryTable(name = "city"), @SecondaryTable(name = "country") }) public class Address { @Id private Long id; private String street1; private String street2; @Column(table = "city") private String city; @Column(table = "city") private String state; @Column(table = "city") private String zipcode; @Column(table = "country") private String country; }
// Constructeurs, getters, setters
Par défaut, les attributs de l’entité Address seraient associés à la table primaire (qui s’appelle ADDRESS par défaut). L’annotation @SecondaryTables précise qu’il y a deux tables secondaires : CITY et COUNTRY. Vous devez ensuite indiquer dans quelle table secondaire stocker chaque attribut (à l’aide de l’annotation @Column(table="city") ou @Column(table="country")). Le résultat, comme le montre la Figure 3.2, est la création de trois tables se partageant les différents attributs mais ayant la même clé
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
80
Java EE 6 et GlassFish 3
primaire (afin de pouvoir les joindre). N’oubliez pas que Derby met en majuscules (CITY) les noms de tables en minuscules (villes). Figure 3.2 L’entité Address est associée à trois tables.
Comme vous l’avez sûrement compris, la même entité peut contenir plusieurs annotations. Si vous voulez renommer la table primaire, vous pouvez donc ajouter une annotation @Table. C’est ce que nous faisons dans le Listing 3.5. Listing 3.5 : La table primaire est renommée en T_ADDRESS @Entity @Table(name = "t_address") @SecondaryTables({ @SecondaryTable(name = "t_city"), @SecondaryTable(name = "t_country") }) public class Address { }
// Attributs, constructeur, getters, setters
INFO Vous devez être conscient de l’impact des tables secondaires sur les performances car, à chaque fois que vous accéderez à une entité, le fournisseur de persistance devra accéder à plusieurs tables et les joindre. En revanche, les tables secondaires peuvent être intéressantes si vous avez des attributs de grande taille, comme des BLOB (Binary Large Objects), car vous pourrez les isoler dans une table à part.
Clés primaires
Dans les bases de données relationnelles, une clé primaire identifie de façon unique chaque ligne d’une table. Cette clé peut être une simple colonne ou un ensemble
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 81
de colonnes. Les clés primaires doivent évidemment être uniques (et la valeur NULL n’est pas autorisée). Des exemples de clés primaires classiques sont un numéro de client, un numéro de téléphone, un numéro de commande et un ISBN. JPA exige que les entités aient un identifiant associé à une clé primaire qui suivra les mêmes règles : identifier de façon unique une entité à l’aide d’un simple attribut ou d’un ensemble d’attributs (clé composée). Une fois affectée, la valeur de la clé primaire d’une entité ne peut plus être modifiée. @Id et @GeneratedValue
Une clé primaire simple (non composée) doit correspondre à un seul attribut de la classe de l’entité. L’annotation @Id que nous avons déjà rencontrée sert à indiquer une clé simple. L’attribut qui servira de clé doit être de l’un des types suivants : ■■
Types primitifs de Java. byte, int, short, long, char.
■■
Classes enveloppes des types primitifs. Byte, Integer, Short, Long, Character.
■■
Tableau de types primitifs ou de classes enveloppes. int[], Integer[], etc.
■■
Chaîne, nombre ou dates. util.Date, java.sql.Date.
java.lang.String, java.math.BigInteger, java.
Lorsque l’on crée une entité, la valeur de cet identifiant peut être produite manuel lement par l’application, ou automatiquement par le fournisseur de persistance si l’on précise l’annotation @GeneratedValue. Celle-ci peut avoir quatre valeurs : ■■ SEQUENCE et IDENTITY précisent, respectivement, une séquence SQL de la base de
données ou une colonne identité. demande au fournisseur de persistance de stocker le nom de la séquence et sa valeur courante dans une table et d’incrémenter cette valeur à chaque fois qu’une nouvelle instance de l’entité est stockée dans la base. Derby, par exemple, crée une table SEQUENCE de deux colonnes : une pour le nom de la séquence (qui est arbitraire) et l’autre pour la valeur (un entier incrémenté automatiquement par Derby).
■■ TABLE
demande que la génération d’une clé s’effectue automatiquement par la base de données sous-jacente, qui est libre de choisir la technique la plus appropriée. C’est la valeur par défaut de l’annotation @GeneratedValue.
■■ AUTO
En l’absence de @GeneratedValue, l’application est responsable de la production des identifiants à l’aide d’un algorithme qui devra renvoyer une valeur unique. Le code
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
82
Java EE 6 et GlassFish 3
du Listing 3.6 montre comment obtenir automatiquement un identifiant. GenerationType.AUTO étant la valeur par défaut de l’annotation, nous aurions pu omettre l’élément strategy. Notez également que l’attribut id est annoté deux fois : avec @Id et avec @GeneratedValue. Listing 3.6 : L’entité Book avec un identifiant produit automatiquement @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String title; private Float price; private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters
Clés primaires composées
Lorsque l’on associe des entités, il est conseillé de dédier une seule colonne à la clé primaire. Dans certains cas, toutefois, on est obligé de passer par une clé primaire composée (pour, par exemple, créer une association avec une base de données existante ou lorsque les clés primaires doivent respecter une convention interne à l’entreprise – une date et un code pays, ou une étiquette temporelle, par exemple). Dans ce cas, nous devons créer une classe de clé primaire pour représenter la clé primaire composée. Pour ce faire, nous disposons de deux annotations pour cette classe, en fonction de la façon dont on souhaite structurer l’entité : @EmbeddedId et @IdClass. Comme nous le verrons, le résultat final est le même – on aboutira au même schéma de base de données – mais cela modifiera légèrement la façon dont on interrogera l’entité. L’application CD-BookStore, par exemple, doit fréquemment poster des articles sur la page d’accueil pour signaler de nouveaux livres, titres musicaux ou artistes. Ces articles ont un contenu, un titre et, comme ils sont écrits dans des langues différentes, un code langue (EN pour l’anglais, FR pour le français, etc.). La clé primaire des articles pourrait donc être composée du titre et du code langue car un article peut être traduit en plusieurs langues tout en gardant son titre initial. La classe de clé primaire NewsId sera donc composée de deux attributs de type String : title et language. Pour pouvoir gérer les requêtes et les collections internes, les classes de clés
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 83
primaires doivent redéfinir les méthodes equals() et hashCode() ; en outre, leurs attributs doivent être de l’un des types déjà mentionnés. Elles doivent également être publiques et implémenter Serializable si elles doivent traverser des couches de l’architecture (elles peuvent être gérées dans la couche de persistance et être utilisées dans la couche présentation, par exemple). Enfin, elles doivent posséder un constructeur par défaut (sans paramètre). @EmbeddedId
Comme nous le verrons plus loin, JPA utilise différentes sortes d’objets intégrés (embedded). Pour faire court, un objet intégré n’a pas d’identité (il n’a pas de clé primaire) et ses attributs sont stockés dans des colonnes de la table associée à l’entité qui le contient. Le Listing 3.7 présente la classe NewsId comme une classe intégrable (embeddable). Il s’agit simplement d’un objet intégré (annoté avec @Embeddable) composé de deux attributs (title et language). Cette classe doit avoir un constructeur par défaut, des getters, des setters et redéfinir equals() et hashCode(). Vous remarquerez que la classe n’a pas d’identité par elle-même (aucune annotation @Id) : c’est ce qui caractérise un objet intégrable. Listing 3.7 : La classe de clé primaire est annotée par @Embeddable @Embeddable public class NewsId { private String title; private String language; // Constructeurs, getters, setters, equals et hashcode }
L’entité News, présentée dans le Listing 3.8, doit maintenant intégrer la classe de clé primaire NewsId avec l’annotation @EmbeddedId. Toutes les annotations @EmbeddedId doivent désigner une classe intégrable annotée par @Embeddable. Listing 3.8 : L’entité intègre la classe de clé primaire avec @EmbeddedId @Entity public class News { @EmbeddedId private NewsId id; private String content; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
84
Java EE 6 et GlassFish 3
Dans le prochain chapitre, nous verrons plus précisément comment retrouver les entités à l’aide de leur clé primaire, mais le Listing 3.9 présente le principe général : la clé primaire étant une classe avec un constructeur, vous devez d’abord l’instancier avec les valeurs qui forment la clé, puis passer cet objet au gestionnaire d’entités (l’attribut em). Listing 3.9 : Code simplifié permettant de retrouver une entité à partir de sa clé primaire composée NewsId cle = new NewsId("Richard Wright est mort", "FR") News news = em.find(News.class, cle);
@IdClass
L’autre méthode pour déclarer une clé primaire composée consiste à utiliser l’annotation @IdClass. Cette approche est différente de la précédente car, ici, chaque attribut de la classe de la clé primaire doit également être déclaré dans la classe entité et annoté avec @Id. La clé primaire de l’exemple du Listing 3.10 est maintenant un objet classique qui ne nécessite aucune annotation. Listing 3.10 : La classe clé primaire n’est pas annotée public class NewsId { private String title; private String language; // Constructeurs, getters, setters, equals et hashcode }
Comme le montre le Listing 3.11, l’entité News doit simplement définir la classe de la clé primaire à l’aide de l’annotation @IdClass et annoter chaque attribut de la clé avec @Id. Pour stocker l’entité News, vous devrez maintenant donner une valeur aux attributs title et language. Listing 3.11 : L’entité définit sa classe de clé primaire avec l’annotation @IdClass @Entity @IdClass(NewsId.class) public class News { @Id private String title; @Id private String language;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Les deux approches, @EmbeddedId et @IdClass, donneront la même structure de table : celle du Listing 3.12. Les attributs de l’entité et de la clé primaire se retrouveront bien dans la même table et la clé primaire sera formée des attributs de la classe clé primaire (title et language). Listing 3.12 : Définition de la table NEWS avec une clé primaire composée create table NEWS ( CONTENT VARCHAR(255), TITLE VARCHAR(255) not null, LANGUAGE VARCHAR(255) not null, primary key (TITLE, LANGUAGE) );
L’approche @IdClass est plus sujette aux erreurs car vous devez définir chaque attribut de la clé primaire à la fois dans la classe de la clé primaire et dans l’entité, en vous assurant d’utiliser les mêmes noms et les mêmes types. L’avantage est que vous n’avez pas besoin de modifier le code de la classe de la clé primaire. Vous pourriez, par exemple, utiliser une classe existante que vous n’avez pas le droit de modifier. La seule différence visible est la façon dont vous ferez référence à l’entité dans JPQL. Dans le cas de @IdClass, vous utiliseriez un code comme celui-ci : select n.title from News n
Alors qu’avec @EmbeddedId vous écririez : select n.newsId.title from News n
Attributs
Une entité doit posséder une clé primaire (simple ou composée) pour être identifiable dans une base de données relationnelle. Elle dispose également de toutes sortes d’attributs qui forment son état, qui doit également être associé à la table. Cet état peut contenir quasiment tous les types Java que vous pourriez vouloir associer : ■■
tableaux d’octets ou de caractères (byte[], Byte[], char[], Character[]) ;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
86
Java EE 6 et GlassFish 3
■■
chaînes, grands nombres et types temporels (java.lang.String, java.math. BigInteger, java.math.BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp) ;
■■
types énumérés et types implémentant l’interface l’utilisateur ;
■■
collection de types de base et de types intégrables.
Serializable,
définis par
Bien sûr, une entité peut également avoir des attributs entités, collections d’entités ou d’instances de classes intégrables. Ceci implique d’introduire des relations entre les entités (que nous étudierons dans la section "Association des relations"). Comme nous l’avons vu, en vertu de la configuration par exception, les attributs sont associés selon des règles par défaut. Parfois, cependant, vous aurez besoin d’adapter certaines parties de cette association : c’est là que les annotations JPA (ou leurs équivalents XML) entrent une nouvelle fois en jeu. @Basic
L’annotation @javax.persistence.Basic (voir Listing 3.13) est le type d’association le plus simple avec une colonne d’une table car il redéfinit les options de base de la persistance. Listing 3.13 : Éléments de l’annotation @Basic @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface Basic { FetchType fetch() default EAGER; boolean optional() default true; }
Cette annotation a deux paramètres : optional et fetch. Le premier indique si la valeur de l’attribut peut être null – il est ignoré pour les types primitifs. Le second peut prendre deux valeurs, LAZY ou EAGER : il indique au fournisseur de persistance que les données doivent être récupérées de façon "paresseuse" (uniquement lorsque l’application en a besoin) ou "immédiate" (lorsque l’entité est chargée par le fournisseur). Considérons, par exemple, l’entité Track du Listing 3.14. Un album CD est constitué de plusieurs pistes ayant chacune un titre, une description et un fichier .WAV d’une certaine durée. Ce dernier est un BLOB qui peut occuper plusieurs mégaoctets. Lorsque nous accédons à l’entité Track, nous ne voulons pas charger immédiatement le fichier WAV : nous annotons donc l’attribut avec @Basic(fetch =
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 87
pour que ces données ne soient lues dans la base que lorsqu’elles seront vraiment nécessaires (lorsque nous accéderons à l’attribut wav via son getter, par exemple). FetchType.LAZY)
Listing 3.14 : L’entité Track avec un chargement paresseux de l’attribut wav @Entity public class Track { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String title; private Float duration; @Basic(fetch = FetchType.LAZY) @Lob private byte[] wav; @Basic(optional = true) private String description; // Constructeurs, getters, setters }
Notez que l’attribut wav de type byte[] est également annoté par @Lob afin que sa valeur soit stockée comme un LOB (Large Object) – les colonnes pouvant stocker ces types de gros objets nécessitent des appels JDBC spéciaux pour être accessibles à partir de Java. Pour en informer le fournisseur, il faut donc ajouter une annotation @Lob à l’association de base. @Column
L’annotation @javax.persistence.Column définit les propriétés d’une colonne. Grâce à elle, nous pouvons modifier le nom de la colonne (qui, par défaut, est le même que celui de l’attribut), sa taille et autoriser (ou non) la colonne à être NULL, unique, modifiable ou utilisable dans une instruction INSERT de SQL. Le Listing 3.15 montre les différents éléments de son API, avec leurs valeurs par défaut. Listing 3.15 : Éléments de l’annotation @Column @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface Column { String name() default ""; boolean unique() default false; boolean nullable() default true; boolean insertable() default true;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Pour redéfinir l’association par défaut de l’entité Book initiale, nous pouvons utiliser de différentes façons l’annotation @Column (voir Listing 3.16). Ici, nous modifions les noms des colonnes associées aux attributs title et nbOfPage, pour lesquelles nous n’autorisons pas les valeurs NULL ; nous précisons également la longueur de la colonne associée à description. Listing 3.16 : Personnalisation de l’association de l’entité Book @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(name = "book_title", nullable = false, „ updatable = false) private String title; private Float price; @Column(length = 2000) private String description; private String isbn; @Column(name = "nb_of_page", nullable = false) private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
L’entité Book du Listing 3.6 sera donc associée à la table définie dans le Listing 3.17. Listing 3.17 : Définition de la table BOOK create table BOOK ( ID BIGINT not null, BOOK_TITLE VARCHAR(255) not null, PRICE DOUBLE(52, 0), DESCRIPTION VARCHAR(2000), ISBN VARCHAR(255), NB_OF_PAGE INTEGER not null, ILLUSTRATIONS SMALLINT, primary key (ID) );
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 89
La plupart des éléments de l’annotation @column influent sur l’association. Si l’on fixe à 2 000 la longueur de l’attribut description, par exemple, la taille de la colonne correspondante sera également de 2 000. Par défaut, updatable et insertable valent true, ce qui signifie que l’on peut insérer ou modifier n’importe quel attribut dans la base de données. En les mettant à false, on demande au fournisseur de persistance de garantir qu’il n’insérera ni ne modifiera les données des colonnes associées à ces attributs lorsque l’entité sera modifiée. Notez que ceci n’implique pas que l’entité ne pourra pas être modifiée en mémoire – elle pourra l’être mais, en ce cas, elle ne sera plus synchronisée avec la base car l’instruction SQL qui sera produite (INSERT ou UPDATE) ne portera pas sur ces colonnes. @Temporal
En Java, vous pouvez utiliser java.util.Date et java.util.Calendar pour stocker des dates puis obtenir des représentations différentes, comme une date, une heure ou des millisecondes. Pour utiliser une date avec un ORM, vous pouvez utiliser l’annotation @javax.persistence.Temporal, qui a trois valeurs possibles : DATE, TIME ou TIMESTAMP. Le Listing 3.18, par exemple, définit une entité Customer contenant une date de naissance et un attribut technique qui stocke le moment exact où ce client a été ajouté au système (à l’aide d’une valeur TIMESTAMP). Listing 3.18 : Entité Customer avec deux attributs @Temporal @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @Temporal(TemporalType.DATE) private Date dateOfBirth; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; // Constructeurs, getters et setters }
L’entité Customer du Listing 3.18 sera associée à la table décrite dans le Listing 3.19. L’attribut dateOfBirth est associé à une colonne de type DATE et l’attribut creationDate, à une colonne de type TIMESTAMP.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
90
Java EE 6 et GlassFish 3
Listing 3.19 : Définition de la table CUSTOMER create table CUSTOMER ( ID BIGINT not null, FIRSTNAME VARCHAR(255), LASTNAME VARCHAR(255), EMAIL VARCHAR(255), PHONENUMBER VARCHAR(255), DATEOFBIRTH DATE, CREATIONDATE TIMESTAMP, primary key (ID) );
@Transient
Avec JPA, tous les attributs d’une classe annotée par @Entity sont automatiquement associés à une table. Si vous ne souhaitez pas associer un attribut particulier, utilisez l’annotation @javax. persistence.Transient. Ajoutons, par exemple, un attribut age à l’entité Customer (voir Listing 3.20) : l’âge pouvant être automatiquement calculé à partir de la date de naissance, il n’est pas nécessaire de stocker cet attribut, qui peut donc être déclaré comme transitoire. Listing 3.20 : Entité Customer avec un âge transitoire @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @Temporal(TemporalType.DATE) private Date dateOfBirth; @Transient private Integer age; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; }
// Constructeurs, getters et setters
Cet attribut n’aura pas de colonne AGE associée. @Enumerated
Java SE 5 a introduit les énumérations, qui sont si souvent utilisées qu’elles font partie de la vie du développeur. Les valeurs d’une énumération sont des constantes
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 91
a uxquelles est implicitement associé un numéro déterminé par leur ordre d’apparition dans l’énumération. Ce numéro ne peut pas être modifié en cours d’exécution mais sert à stocker la valeur du type énuméré dans la base de données. Le Listing 3.21 montre une énumération de types de cartes de crédit. Listing 3.21 : Énumération de types de cartes de crédit public enum CreditCardType { VISA, MASTER_CARD, AMERICAN_EXPRESS }
Les numéros affectés lors de la compilation aux valeurs de ce type énuméré seront 0 pour VISA, 1 pour MASTER_CARD et 2 pour AMERICAN_EXPRESS. Par défaut, les fournisseurs de persistance associeront ce type énuméré à la base de données en supposant que la colonne est de type Integer. Le Listing 3.22 montre une entité CreditCard qui utilise l’énumération précédente avec une association par défaut. Listing 3.22 : Association d’un type énuméré à des numéros @Entity @Table(name = "credit_card") public class CreditCard { @Id private private private private //
Les règles par défaut feront que l’énumération sera associée à une colonne de type entier et tout ira bien. Imaginons maintenant que nous ajoutions une nouvelle constante au début de l’énumération. L’affectation des numéros dépendant de l’ordre d’apparition des constantes, les valeurs déjà stockées dans la base de données ne correspondront plus à l’énumération. Une meilleure solution consiste donc à stocker le nom de la constante à la place de son numéro d’ordre. C’est ce que fait le Listing 3.23 à l’aide de l’annotation @Enumerated avec la valeur STRING (sa valeur par défaut est ORDINAL).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
92
Java EE 6 et GlassFish 3
Listing 3.23 : Association d’un type énuméré avec une chaîne @Entity @Table(name = "credit_card") public class CreditCard { @Id private String number; private String expiryDate; private Integer controlNumber; @Enumerated(EnumType.STRING) private CreditCardType creditCardType; //
Constructeurs, getters et setters
}
Désormais, la colonne CREDITCARDTYPE de la table sera de type VARCHAR et une carte Visa sera stockée sous la forme "VISA". Types d’accès
Pour l’instant, nous n’avons vu que des annotations de classes (@Entity ou @Table) et d’attributs (@Basic, @Column, @Temporal, etc.), mais les annotations qui s’appliquent à un attribut (accès au champ) peuvent également être placées sur la méthode getter correspondante (accès à la propriété). L’annotation @Id, par exemple, peut être affectée à l’attribut id ou à la méthode getId(). Il s’agit surtout ici d’une question de goût personnel et nous préférons utiliser les accès aux propriétés (getters annotés) car nous trouvons le code plus lisible : nous pouvons lire rapidement les attributs d’une entité sans être perturbés par les annotations (dans ce livre, toutefois, nous avons décidé d’annoter les attributs afin d’éviter de devoir alourdir les listings par les codes des getters). Dans certains cas comme l’héritage, cependant, ce n’est plus simplement une affaire de goût car cela peut avoir un impact sur l’association. INFO Java définit un champ comme un attribut d’instance. Une propriété est un champ avec un accesseur (getter et setter) respectant la convention des Java beans (le nom de la méthode d’accès est de la forme getXXX, setXXX ou isXXX si elle renvoie un Boolean).
Lorsque l’on choisit entre un accès au champ (attribut) ou à la propriété (getter), on choisit un type d’accès. Par défaut, c’est un type d’accès simple qui s’applique à une entité : il peut s’agir d’un accès au champ ou à la propriété, mais pas les deux.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 93
La spécification indique que le comportement d’une application qui mélangerait les emplacements des annotations sur les champs et les propriétés sans préciser explicitement le type d’accès est indéfini. Lorsque l’on utilise un accès aux champs (voir Listing 3.24), le fournisseur de persistance associe les attributs. Toutes les variables d’instance qui ne sont pas annotées par @Transient sont persistantes. Listing 3.24 : Entité Customer avec des champs annotés @Entity public class Customer { @Id @GeneratedValue private Long id; @Column(nom = "first_name", nullable = false, length = 50) private String firstName; @Column(nom = "last_name", nullable = false, length = 50) private String lastName; private String email; @Column(nom = "phone_number", length = 15) private String phoneNumber; // Constructeurs, getters et setters }
Lorsque l’on utilise un accès aux propriétés, comme dans le Listing 3.25, le fournisseur de persistance accède à l’état persistant via les méthodes getter et l’association repose sur ces getters plutôt que sur les attributs. Tous les getters non annotés par @Transient sont persistants. Listing 3.25 : Entité Client avec des propriétés annotées @Entity public class Customer { private private private private private
Long id; String firstName; String lastName; String email; String phoneNumber;
// Constructeur... @Id @GeneratedValue public Long getId() { return id; }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
En termes d’associations, les deux entités des Listings 3.24 et 3.25 sont en tout point identiques car les noms des attributs sont les mêmes que ceux des getters. Au lieu d’utiliser le type d’accès par défaut, vous pouvez également le préciser explicitement avec l’annotation @javax.persistence.Access. Cette annotation a deux valeurs possibles, FIELD ou PROPERTY, et peut être utilisée sur l’entité elle-même et/ou sur chaque attribut ou getter. Lorsque @Access(AccessType. FIELD) est appliqué à l’entité, par exemple, seules les annotations d’associations placées sur les attributs seront prises en compte par le fournisseur de persistance. Il est également possible d’annoter avec @Access(AccessType.PROPERTY) des getters individuels pour effectuer des accès par propriété. Les types d’accès explicites peuvent être très pratiques (avec les objets intégrables et l’héritage, par exemple), mais leur mélange provoque souvent des erreurs. Le Listing 3.26 montre ce qui pourrait se passer si vous mélangez ces deux types d’accès.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Cet exemple définit explicitement le type d’accès FIELD au niveau de l’entité, ce qui indique au gestionnaire de persistance qu’il ne doit traiter que les annotations sur les attributs. phoneNumber est annoté par @Column, qui limite sa taille à 15 caractères. En lisant ce code, on pourrait s’attendre à ce que le type de la colonne correspondante dans la base de données soit VARCHAR(15), mais ce ne sera pas le cas. En effet, le type d’accès a été explicitement modifié pour la méthode getter getPhoneNumber() : la longueur d’un numéro de téléphone dans la base sera donc de 555 caractères. Ici, l’AccessType.FIELD de l’entité a été écrasé par AccessType.PROPERTY et la colonne sera donc de type VARCHAR(555). Collections de types de base
Les collections sont très utilisées en Java. Dans cette section, nous étudierons les relations entre entités (qui peuvent être des collections d’entités) : essentiellement, ceci signifie qu’une entité contient une collection d’autres entités ou d’objets intégrables. En terme d’association, chaque entité est associée à sa propre table et l’on crée des références entre les clés primaires et les clés étrangères. Comme vous le savez, une
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
96
Java EE 6 et GlassFish 3
entité est une classe Java avec une identité et de nombreux autres attributs : mais comment faire pour stocker une simple collection de types Java comme des String et/ou des Integer ? Depuis JPA 2.0, il n’est plus nécessaire de créer une classe distincte car on dispose des annotations @ElementCollection et @CollectionTable. L’annotation @ElementCollection indique qu’un attribut de type java.util.Collection contient des types Java tandis que @CollectionTable permet de modifier les détails de la table de la collection – son nom, par exemple. Si cette dernière est omise, le nom de la table sera formé par la concaténation du nom de l’entité conteneur et de celui de l’attribut collection, séparés par un blanc souligné ("_", ou underscore). Utilisons une nouvelle fois l’entité Book et ajoutons-lui un attribut pour stocker des tags. De nos jours, les tags et les nuages de tags sont partout et sont très pratiques pour trier les données : dans notre exemple, nous voulons nous en servir pour décrire un livre et le retrouver rapidement. Un tag n’étant qu’une simple chaîne, l’entité Book contiendra donc une collection de chaînes pour stocker ces informations, comme le montre le Listing 3.27. Listing 3.27 : L’entité Book contient une collection de chaînes @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String title; private Float price; private String description; private String isbn; private Integer nbDePages; private Boolean illustrations; @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name="Tag") @Column(name = "Value") private ArrayList tags;
}
// Constructeurs, getters et setters
L’annotation @ElementCollection informe le fournisseur de persistance que l’attribut tags est une liste de chaînes qui devra être récupérée de façon paresseuse. En l’absence de @CollectionTable, le nom de la table sera BOOK_TAGS au lieu du nom précisé dans l’élément name de l’annotation (name = "Tag"). Vous remarquerez que nous avons ajouté une annotation @Column supplémentaire pour renommer la colonne en Value. Le résultat obtenu est représenté par la Figure 3.3.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
+ID TITLE PRICE DESCRIPTION ISBN NBOFPAGE ILLUSTRATIONS
INFO Ces annotations n’existaient pas en JPA 1.0. Cependant, il était possible de stocker une liste de types primitifs dans la base de données sous la forme d’un BLOB. En effet, java.util.ArrayList implémente Serializable et JPA sait associer automatiquement des objets Serializable à des BLOB. En revanche, lorsque l’on utilisait une collection java.util.List, on obtenait une exception car List n’implémente pas Serializable. L’annotation @ElementCollection est donc un moyen plus élégant et plus pratique de stocker les listes de types primitifs car leur stockage sous un format binaire opaque aux requêtes les rend inaccessibles.
Association des types de base
Comme les collections, les tables de hachage sont très utiles pour le stockage des données. Avec JPA 1.0, on ne pouvait pas en faire grand-chose en terme d’ORM. Désormais, les tables de hachage peuvent utiliser n’importe quelle combinaison de types de base, d’objets intégrables et d’entités comme clés ou comme valeurs : ceci apporte beaucoup de souplesse. Pour l’instant, intéressons-nous aux hachages qui utilisent des types de base. Lorsqu’un hachage emploie des types de base, vous pouvez vous servir des annotations @ElementCollection et @CollectionTable exactement comme nous venons de le voir pour les collections. En ce cas, les données du hachage sont stockées dans une table collection. Prenons l’exemple d’un CD contenant un certain nombre de pistes (voir Listing 3.28). Une piste peut être considérée comme un titre et une position (la première piste de l’album, la seconde, etc.). Vous pourriez alors utiliser un hachage de pistes utilisant un entier pour représenter la position (une clé du hachage) et une chaîne pour représenter le titre (la valeur de cette clé dans le hachage).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
98
Java EE 6 et GlassFish 3
Listing 3.28 : Album CD avec un hachage de pistes @Entity public class CD { @Id @GeneratedValue private Long id; private String title; private Float price; private String description; @Lob private byte[] cover; @ElementCollection @CollectionTable(name="track") @MapKeyColumn (name = "position") @Column(name = "title") private HashMap tracks; // Constructeurs, getters et setters }
Comme on l’a déjà indiqué, l’annotation @ElementCollection sert à indiquer que les objets du hachage seront stockés dans une table collection. L’annotation @CollectionTable, quant à elle, est utilisée ici pour modifier le nom par défaut de la table collection en TRACK. La différence avec les collections est que l’on introduit ici une nouvelle annotation, @MapKeyColumn, pour préciser l’association correspondant à la colonne clé du hachage. En son absence, le nom de cette colonne est formé par concaténation du nom de l’attribut qui référence la relation et du suffixe _KEY. Le Listing 3.28 utilise cette annotation pour la renommer en POSITION afin qu’elle porte un nom plus lisible. L’annotation @Column indique que la colonne contenant les valeurs du hachage sera nommée TITLE. Le résultat obtenu est représenté par la Figure 3.4. +ID TITLE PRICE DESCRIPTION COVER
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 99
Associations avec XML Maintenant que vous connaissez mieux les bases des associations avec les annotations, étudions celles qui utilisent XML. Si vous avez déjà utilisé un framework ORM comme Hibernate, vous savez déjà comment associer vos entités via un fichier de descripteurs de déploiement XML. Depuis le début de ce chapitre, nous n’avons pourtant pas utilisé une seule ligne de XML – uniquement des annotations. Nous n’entrerons pas trop dans les détails des associations XML car nous avons décidé de nous concentrer sur les annotations (parce qu’elles sont plus simples à utiliser dans un livre et parce que la plupart des développeurs les préfèrent à XML). Considérez simplement que toutes les annotations que nous avons présentées dans ce chapitre ont un équivalent XML : cette section serait énorme si nous les présentions toutes. Nous vous renvoyons donc au Chapitre 11 de la spécification JPA 2.0, qui présente en détail tous les marqueurs XML. Les descripteurs de déploiement XML sont une alternative aux annotations. Bien que chaque annotation ait un marqueur XML équivalent et vice versa, il y a toutefois une différence car les marqueurs XML ont priorité sur les annotations : si vous annotez un attribut ou une entité avec une certaine valeur et que vous déployiez en même temps le descripteur XML correspondant avec une valeur différente, celle de l’annotation sera ignorée. Quand utiliser les annotations plutôt que XML et pourquoi ? C’est avant tout une question de goût car les deux méthodes ont exactement le même effet. Lorsque les métadonnées sont vraiment couplées au code (une clé primaire, par exemple), les annotations sont judicieuses car, en ce cas, les métadonnées ne sont qu’un autre aspect du programme. D’autres types de métadonnées comme la longueur des colonnes ou autres détails concernant le schéma peuvent en revanche dépendre de l’environnement de déploiement (le schéma de la base peut, par exemple, varier entre les environnements de développement, de test et de production). En ce cas, il est préférable de les exprimer à l’aide de descripteurs de déploiement externes (un par environnement), afin de ne pas devoir modifier le code. Revenons à notre entité Book. Imaginons que nous travaillons dans deux environnements : nous voulons associer l’entité à la table BOOK dans l’environnement de développement et à la table BOOK_XML_MAPPING dans celui de test. La classe ne sera annotée que par @Entity (voir Listing 3.29) et ne contiendra pas d’information sur la table à laquelle elle est associée (elle ne contiendra donc pas d’annotation @Table). L’annotation @Id définit une clé primaire produite automatiquement et @Column fixe la taille de la description à 500 caractères.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
100
Java EE 6 et GlassFish 3
Listing 3.29 : L’entité Book ne contient que quelques annotations @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String title; private Float price; @Column(length = 500) private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
Vous pouvez modifier les associations de n’importe quelle donnée de l’entité en utilisant un fichier book_mapping.xml (voir Listing 3.30) qui doit respecter un certain schéma XML. Le marqueur
, par exemple, permet de modifier le nom de la table à laquelle sera associée l’entité (BOOK_XML_MAPPING au lieu du nom BOOK par défaut). Dans le marqueur , vous pouvez adapter les attributs en précisant non seulement les noms ou les tailles de leurs colonnes, mais également leurs relations avec d’autres entités. Dans notre exemple, nous modifions les noms des colonnes title et nbOfPage. Listing 3.30 : Association utilisant le fichier META-INF/book_mapping.xml
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 101
Il ne faut jamais oublier que XML a priorité sur les annotations. Même si l’attribut description est annoté par @Column(length = 500), la longueur de la colonne utilisée sera celle définie dans le fichier book_mapping.xml, c’est-à-dire 2 000. Pensez à toujours consulter le descripteur de déploiement XML en cas de doute. La fusion des métadonnées XML et des métadonnées des annotations fera que l’entité Book sera finalement associée à la table BOOK_XML_MAPPING, dont la structure est définie dans le Listing 3.31. Listing 3.31 : Structure de la table BOOK_XML_MAPPING create table BOOK_XML_MAPPING ( ID BIGINT not null, BOOK_TITLE VARCHAR(255) not null, DESCRIPTION VARCHAR(2000), NB_OF_PAGE INTEGER not null, PRICE DOUBLE(52, 0), ISBN VARCHAR(255), ILLUSTRATIONS SMALLINT, primary key (ID) );
Il ne manque plus qu’une information pour que ceci fonctionne : vous devez référencer le fichier book_mapping.xml dans le fichier persistence.xml à l’aide d’un marqueur . Le fichier persistence.xml définit le contexte de persistance de l’entité et la base de données à laquelle elle sera associée : le fournisseur de contenu a absolument besoin de ce fichier pour pouvoir retrouver les associations XML externes. Déployez l’entité Book avec ces deux fichiers XML (placés dans le répertoire META-INF), et c’est fini (voir Listing 3.32). Listing 3.32 : Fichier persistence.xml faisant référence à un fichier d’association externe org.eclipse.persistence.jpa.PersistenceProvider com.apress.javaee6.chapter03.Book META-INF/book_mapping.xml
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
102
Java EE 6 et GlassFish 3
Objets intégrables Dans la section "Clés primaires composées" plus haut dans ce chapitre, nous avons rapidement vu comment une classe pouvait être intégrée pour servir de clé primaire avec l’annotation @EmbeddedId. Les objets intégrables sont des objets qui n’ont pas d’identité persistante par eux-mêmes. Une entité peut contenir des collections d’objets intégrables ainsi qu’un simple attribut d’une classe intégrable : dans les deux cas, ils seront stockés comme faisant partie de l’entité et partageront son identité. Ceci signifie que chaque attribut de l’objet intégré est associé à la table de l’entité. Il s’agit donc d’une relation de propriété stricte (une composition) : quand l’entité est supprimée, l’objet intégré disparaît également. Cette composition entre deux classes passe par des annotations. La classe incluse utilise @Embeddable et l’entité qui inclut utilise @Embedded. Prenons l’exemple d’un client possédant un identifiant, un nom, une adresse e-mail et une adresse. Tous ces attributs pourraient se trouver dans une entité Customer (voir Listing 3.34 un peu plus loin) mais, pour des raisons de modélisation, ils sont répartis en deux classes : Customer et Address. Cette dernière n’ayant pas d’identité propre mais étant simplement une composante de l’état de Customer, c’est une bonne candidate au statut de classe intégrable (voir Listing 3.33). Listing 3.33 : La classe Address est une classe intégrable @Embeddable public class Address { private private private private private private
String String String String String String
street1; street2; city; state; zipcode; country;
// Constructors, getters, setters }
Comme vous pouvez le constater à la lecture du Listing 3.33, la classe Address est annotée comme étant non pas une entité mais une classe intégrable – l’annotation
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 103
indique qu’Address peut être intégrée dans une classe entité (ou dans une autre classe intégrable). À l’autre extrémité de la composition, l’entité Customer doit utiliser l’annotation @Embedded pour indiquer qu’Address est un attribut persistant qui sera stocké comme composante interne et qu’il partage son identité (voir Listing 3.34). @Embeddable
Listing 3.34 : L’entité Customer intègre un objet Address @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @Embedded private Address address; // Constructors, getters, setters }
Chaque attribut d’Address est associé à la table de l’entité Customer. Il n’y aura donc qu’une seule table qui aura la structure définie dans le Listing 3.35. Comme nous le verrons plus loin dans la section "Clés primaires composées", les entités peuvent redéfinir les attributs des objets qu’elles intègrent (avec l’annotation @AttributeOverrides). Listing 3.35 : Structure de la table CUSTOMER avec tous les attributs d’Address create table CUSTOMER ( ID BIGINT not null, LASTNAME VARCHAR(255), PHONENUMBER VARCHAR(255), EMAIL VARCHAR(255), FIRSTNAME VARCHAR(255), STREET2 VARCHAR(255), STREET1 VARCHAR(255), ZIPCODE VARCHAR(255), STATE VARCHAR(255), COUNTRY VARCHAR(255), CITY VARCHAR(255), primary key (ID) );
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
104
Java EE 6 et GlassFish 3
Types d’accès d’une classe intégrable
Le type d’accès d’une classe intégrable est déterminé par celui de la classe entité dans laquelle elle est intégrée. Si cette entité utilise explicitement un type d’accès par propriété, l’objet intégrable utilisera implicitement un accès par propriété aussi. Vous pouvez préciser un type d’accès différent pour une classe intégrable au moyen de l’annotation @Access. Dans le Listing 3.36, l’entité Customer et la classe Address (voir Listing 3.37) utilisent des types d’accès différents. Listing 3.36 : L’entité Customer avec un type d’accès par champ @Entity @Access(AccessType.FIELD) public class Customer { @Id @GeneratedValue private Long id; @Column(name = "first_name", nullable = false, length = 50) private String firstName; @Column(name = "last_name", nullable = false, length = 50) private String lastName; private String email; @Column(name = "phone_number", length = 15) private String phoneNumber; @Embedded private Address address; }
// Constructeurs, getters, setters
Listing 3.37 : La classe intégrable utilise un type d’accès par propriété @Embeddable @Access(AccessType.PROPERTY) public class Address { private private private private private private
String String String String String String
street1; street2; city; state; zipcode; country;
// Constructeurs @Column(nullable = false) public String getStreet1() {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 105
return street1; } public void setStreet1(String street1) { this.street1 = street1; } public String getStreet2() { return street2; } public void setStreet2(String street2) { this.street2 = street2; } @Column(nullable = false, length = 50) public String getCity() { return city; } public void setCity(String city) { this.city = city; } @Column(length = 3) public String getState() { return state; } public void setState(String state) { this.state = state; } @Column(name = "zip_code", length = 10) public String getZipcode() { return zipcode; } public void setZipcode(String zipcode) { this.zipcode = zipcode; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } }
Il est fortement conseillé de configurer le type d’accès des classes intégrables afin d’éviter les erreurs d’association qui pourraient se produire lorsqu’une telle classe est intégrée dans plusieurs entités. Étendons notre modèle en ajoutant une entité Order (voir Figure 3.5). La classe Address est maintenant intégrée à la fois par Customer (pour représenter l’adresse personnelle du client) et dans Order (pour représenter l’adresse de livraison).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
106
Java EE 6 et GlassFish 3
. Figure 3.5
Customer
Address
Order
Address est intégrée par Customer et Order.
Chaque entité définit un type d’accès différent : Customer utilise un accès par champ et Order, un accès par propriété. Le type d’accès d’un objet intégrable étant déterminé par celui de la classe entité dans laquelle il est déclaré, Address sera associée de deux façons différentes, ce qui peut poser des problèmes. Pour les éviter, le type d’accès d’Address doit être indiqué explicitement. INFO Les types d’accès explicites sont également très utiles avec l’héritage. Par défaut, les entités filles héritent du type d’accès de leur entité parente. Dans une hiérarchie d’entités, il est cependant possible d’accéder à chacune différemment des autres : l’ajout d’une annotation @Access permet de redéfinir localement le type d’accès par défaut utilisé dans la hiérarchie.
Correspondance des relations Le monde de la programmation orientée objet est rempli de classes et de relations entre classes. Ces relations sont structurelles car elles lient des objets d’un certain type à des objets d’autres types, permettant ainsi à un objet de demander à un autre de réaliser une action. Il existe plusieurs types d’associations entre les classes. Premièrement, une relation a une direction. Elle peut être unidirectionnelle (un objet peut aller vers un autre) ou bidirectionnelle (un objet peut aller vers un autre et vice versa). En Java, on utilise le point (.) pour naviguer entre les objets. Lorsque l’on écrit, par exemple, customer.getAddress().getCountry(), on navigue d’un objet Customer vers un objet Address puis un objet Country. En UML (Unified Modeling Language), une relation unidirectionnelle entre deux classes est représentée par une flèche indiquant la direction. À la Figure 3.6, par exemple, Class1 (la source) peut naviguer vers Class2 (la cible), mais pas l’inverse. Figure 3.6
Class1
Class2
Relation unidirectionnelle entre deux classes.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 107
Comme le montre la Figure 3.7, une relation bidirectionnelle n’utilise pas de flèche : Class1 peut naviguer vers Class2 et vice versa. En Java, ce type de relation est représenté par une classe Class1 ayant un attribut instance de Class2 et par une classe Class2 ayant un attribut instance de Class1. Figure 3.7
Class1
Class2
Relation bidirectionnelle entre deux classes.
Une relation a également une cardinalité. Chaque extrémité peut préciser le nombre d’objets impliqués dans cette relation. Le diagramme UML de la Figure 3.8 indique, par exemple, qu’une instance de Class1 est en relation avec zéro ou plusieurs instances de Class2. Figure 3.8 Cardinalité des relations entre classes.
Class1
1
0..*
Class2
En UML, une cardinalité est un intervalle de valeurs compris entre un minimum et un maximum : 0..1 signifie qu’il y aura au minimum zéro objet et au maximum un objet, 1 signifie qu’il n’y aura qu’une et une seule instance, 1..*, qu’il y aura une ou plusieurs instances et 3..6, qu’il y aura entre trois et six objets. En Java, une relation qui représente plusieurs objets utilise les collections de java.util.Collection, java.util.Set, java.util.List ou java.util.Map. Une relation a un propriétaire. Dans une relation unidirectionnelle, ce propriétaire est implicite : à la Figure 3.6, il est évident que le propriétaire est Class1. Dans une relation bidirectionnelle comme celle de la Figure 3.7, il faut en revanche l’indiquer explicitement en désignant le côté propriétaire, qui spécifie l’association physique, et le côté opposé (non propriétaire). Dans les sections qui suivent, nous verrons comment associer des collections d’objets avec les annotations JPA. Relations dans les bases de données relationnelles
Dans le monde relationnel, les choses sont différentes puisque, à proprement parler, une base de données relationnelle est un ensemble de relations (également appelées tables) : tout est modélisé sous forme de table – pour modéliser une relation, vous ne disposez ni de listes, ni d’ensembles, ni de tables de hachage : vous n’avez que des
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
108
Java EE 6 et GlassFish 3
tables. Une relation entre deux classes Java sera représentée dans la base de données par une référence à une table qui peut être modélisée de deux façons : avec une clé étrangère (une colonne de jointure) ou avec une table de jointure. À titre d’exemple, supposons qu’un client n’ait qu’une seule adresse, ce qui implique une relation 1–1. En Java, la classe Customer aurait donc un attribut Address ; dans le monde relationnel, vous pourriez avoir une table CUSTOMER pointant vers une table ADDRESS via une clé étrangère, comme le montre la Figure 3.9. Address
Customer Clé primaire Firstname
Clé primaire
Street
City
Country
Lastname Clé étrangère
11
Aligre
Paris
France
1
James
Rorisson
11
12
Balham
London
UK
2
Dominic
Johnson
12
13
Alfama
Lisbon
Portugal
3
Maca
Macaron
13
Figure 3.9 Une relation entre deux tables utilisant une colonne de jointure.
La seconde méthode consiste à utiliser une table de jointure. La table CUSTOMER de la Figure 3.10 ne stocke plus la clé étrangère vers ADDRESS mais utilise une table intermédiaire pour représenter la relation liant ces deux tables. Cette liaison est constituée par les clés primaires des deux tables. Address
Customer Clé primaire Firstname
Clé primaire
Street
City
Country
Lastname Clé étrangère
11
Aligre
Paris
France
1
James
Rorisson
11
12
Balham
London
UK
2
Dominic
Johnson
12
13
Alfama
Lisbon
Portugal
3
Maca
Macaron
13
Table de jointure Customer Pk Address Pk 1
11
2
12
3
13
Figure 3.10 Relation utilisant une table de jointure.
On n’utilise pas une table de jointure pour représenter une relation 1–1 car cela pourrait avoir des conséquences sur les performances (il faudrait toujours accéder à la troisième table pour obtenir l’adresse d’un client) ; elles sont généralement réservées aux relations 1–N ou N–M. Comme nous le verrons dans la section suivante,
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 109
JPA utilise ces deux méthodes pour associer les relations entre les objets à une base de données. Relations entre entités
Revenons maintenant à JPA. La plupart des entités doivent pouvoir référencer ou être en relation avec d’autres entités : c’est ce que produisent les diagrammes utilisés pour modéliser les applications professionnelles. JPA permet d’associer ces relations de sorte qu’une entité puisse être liée à une autre dans un modèle relationnel. Comme pour les annotations d’associations élémentaires, que nous avons déjà étudiées, JPA utilise une configuration par exception pour ces relations : il utilise un mécanisme par défaut pour stocker une relation mais, si cela ne vous convient pas, vous disposez de plusieurs annotations pour adapter l’association à vos besoins. La cardinalité d’une relation entre deux entités peut être 1–1, 1–N, N–1 ou N–M. Les annotations des associations correspondantes sont donc nommées @OneToOne, @ OneToMany, @ManyToOne et @ManyToMany. Chacune d’elles peut être utilisée de façon unidirectionnelle ou bidirectionnelle : le Tableau 3.1 énumère toutes les combinaisons possibles. Tableau 3.1 : Combinaisons possibles entre cardinalités et directions
Cardinalité
Direction
1–1
Unidirectionnelle
1–1
Bidirectionnelle
1–N
Unidirectionnelle
N–1/1–N
Bidirectionnelle
N–1
Unidirectionnelle
N–M
Unidirectionnelle
N–M
Bidirectionnelle
Vous pouvez constater que unidirectionnel et bidirectionnel sont des concepts répétitifs qui s’appliquent de la même façon à toutes les cardinalités. Vous verrez bientôt la différence entre les relations unidirectionnelles et bidirectionnelles, puis comment implémenter certaines de ces combinaisons, dont nous ne décrirons qu’un sous-ensemble : les expliquer toutes serait répétitif. L’important est de comprendre comment traduire la cardinalité et la direction en relations.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
110
Java EE 6 et GlassFish 3
Unidirectionnelle et bidirectionnelle
Du point de vue de la modélisation objet, la direction entre les classes est naturelle. Dans une relation unidirectionnelle, un objet A pointe uniquement vers un objet B alors que, dans une relation bidirectionnelle, ils se font mutuellement référence. Cependant, comme le montre l’exemple suivant d’un client et de son adresse, un peu de travail est nécessaire lorsque l’on veut représenter une relation bidirectionnelle dans une base de données. Dans une relation unidirectionnelle, une entité Customer a un attribut de type Address (voir Figure 3.11). Cette relation ne va que dans un seul sens : on dit que le client est le propriétaire de la relation. Du point de vue de la base de données, ceci signifie que la table CUSTOMER contiendra une clé étrangère (une colonne de jointure) pointant vers la table ADDRESS. Par ailleurs, le propriétaire de la relation peut personnaliser la traduction de cette relation : si vous devez modifier le nom de la clé étrangère, par exemple, cette annotation aura lieu dans l’entité Customer (le propriétaire). Figure 3.11
Address
Relation unidirectionnelle entre Customer et Address.
Comme on l’a mentionné précédemment, les relations peuvent également être bidirectionnelles. Pour naviguer entre Address et Customer, nous devons ajouter un attribut Customer à l’entité Address (voir Figure 3.12). Notez que les attributs représentant une relation n’apparaissent pas dans les diagrammes UML. Figure 3.12 Relation bidirectionnelle entre Customer et Address.
En termes de Java et d’annotations, ceci revient à avoir deux associations de type 1–1 dans les deux directions opposées. Nous pouvons donc considérer une relation bidirectionnelle comme une paire de relations unidirectionnelles allant dans les deux sens (voir Figure 3.13).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 111
Figure 3.13
Address
Customer
Relation bidirectionnelle représentée par deux relations unidirectionnelles.
Comment associer tout cela à une base de données ? Qui est le propriétaire de cette relation bidirectionnelle ? À qui appartient l’information sur la colonne ou la table de jointure ? Si les relations unidirectionnelles ont un côté propriétaire, les bidirectionnelles ont à la fois un côté propriétaire et un côté opposé, qui doivent être indiqués explicitement par l’élément mappedBy des annotations @OneToOne, @OneToMany et @ManyToMany. mappedBy identifie l’attribut propriétaire de la relation ; il est obligatoire pour les relations bidirectionnelles. Pour illustrer tout ceci, comparons du code Java à sa traduction dans la base de données. Comme vous pouvez le constater dans la partie gauche de la Figure 3.14, les deux entités pointent l’une vers l’autre au moyen d’attributs : Customer possède un attribut address annoté par @OneToOne et l’entité Address a un attribut customer également annoté. Dans la partie droite de cette figure se trouvent les tables C USTOMER et ADDRESS. CUSTOMER est la table propriétaire de la relation car elle contient la clé étrangère vers ADDRESS. @Entity public class C
}
{
@Id @GeneratedValue private Long i ; private String firstName; private String lastName; private String email; private String phoneNumber; @On T O @JoinColumn(name = " s private Address address;
@Entity public class Address {
}
@Id @GeneratedValue private Long id; private String street1; private String street2; private String city; private String state; private String zipcode; private String country; @ n ( p d = " d private Customer customer;
+ID LASTNAME PHONENUMBER EMAIL FIRSTNAME #ADDRESS FK
Figure 3.14 Code de Customer et Address avec leur correspondance dans la base de données.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
112
Java EE 6 et GlassFish 3
L’entité Address utilise l’élément mappedBy de son annotation @OneToOne. Ici, mappedBy indique que la colonne de jointure (address) est déclarée à l’autre extrémité de la relation. De son côté, l’entité Customer définit la colonne de jointure avec l’annotation @JoinColumn et renomme la clé étrangère en address_fk. Customer est l’extrémité propriétaire de la relation et, en tant que telle, elle est la seule à définir l’association de la colonne de jointure. Address est l’extrémité opposée et c’est donc la table de l’entité propriétaire qui contient la clé étrangère (la table CUSTOMER a une colonne ADDRESS_FK). Il existe un élément mappedBy pour les annotations @OneToOne, @OneToMany et @ManyToMany, mais pas pour @ManyToOne. INFO Si vous connaissez Hibernate, vous pouvez considérer que l’élément mappedBy de JPA est l’équivalent de l’attribut inverse de Hibernate, qui indique l’extrémité à ignorer dans une relation.
@OnetoOne unidirectionnelle
Une relation 1–1 unidirectionnelle entre deux entités a une référence de cardinalité 1 qui ne peut être atteinte que dans une seule direction. Reprenons l’exemple d’un client et de son adresse en supposant qu’il n’a qu’une seule adresse (cardinalité 1). Il faut pouvoir naviguer du client (la source) vers l’adresse (la cible) pour savoir où habite le client. Dans le modèle de la Figure 3.15, nous n’avons pas besoin de faire le trajet inverse (on n’a pas besoin de savoir quel client habite à une adresse donnée). Figure 3.15 Un client a une seule adresse.
Comme vous pouvez le constater à la lecture des Listings 3.38 et 3.39, ces deux entités utilisent un nombre minimal d’annotations – @Entity plus @Id et @GeneratedValue pour la clé primaire, c’est tout... Grâce à la configuration par exception, le fournisseur de persistance les associera à deux tables et ajoutera une clé étrangère pour représenter la relation (allant du client à l’adresse). Cette relation 1–1 est déclenchée par le fait qu’Address est déclarée comme une entité et qu’elle est incluse dans l’entité Customer sous la forme d’un attribut. Il n’y a donc pas besoin d’annotation @OneToOne car le comportement par défaut suffit (voir Listings 3.40 et 3.41). Listing 3.40 : La table CUSTOMER avec une clé étrangère vers ADDRESS create table CUSTOMER ( ID BIGINT not null, FIRSTNAME VARCHAR(255), LASTNAME VARCHAR(255), EMAIL VARCHAR(255),
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Listing 3.41 : La table ADDRESS create table ADDRESS ( ID BIGINT not null, STREET1 VARCHAR(255), STREET2 VARCHAR(255), CITY VARCHAR(255), STATE VARCHAR(255), ZIPCODE VARCHAR(255), COUNTRY VARCHAR(255), primary key (ID) );
Comme vous le savez, si un attribut n’est pas annoté, JPA lui applique les règles d’association par défaut. La colonne de clé étrangère s’appellera donc ADDRESS_ID (voir Listing 3.40), qui est la concaténation du nom de l’attribut (address, ici), d’un blanc souligné et du nom de la clé primaire de la table destination (ici, la colonne ID de la table ADDRESS). Notez également que, dans le langage de définition des données, la colonne ADDRESS_ID peut, par défaut, recevoir des valeurs NULL : par défaut, une relation 1–1 est donc associée à zéro (NULL) ou une valeur. Il existe deux annotations permettant d’adapter l’association d’une relation 1–1. La première est @OneToOne (car la cardinalité de la relation est un) : elle permet de modifier certains attributs de la relation elle-même, comme la façon dont elle sera parcourue. Son API est décrite dans le Listing 3.42. Listing 3.42 : API de l’annotation @OneToOne @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface OneToOne { Class targetEntity() default void.class; CascadeType[] cascade() default {}; FetchType fetch() default EAGER; boolean optional() default true; String mappedBy() default ""; boolean orphanRemoval() default false; }
L’autre annotation s’appelle @JoinColumn (son API ressemble beaucoup à celle de @Column). Elle permet de personnaliser la colonne de jointure, c’est-à-dire la clé
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 115
étrangère, du côté du propriétaire de la relation. Le Listing 3.43 présente un exemple d’utilisation de ces deux annotations. Listing 3.43 : L’entité Customer avec une association de relation personnalisée @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @OneToOne (fetch = FetchType.LAZY) @JoinColumn(name = "add_fk", nullable = false) private Address address; // Constructeurs, getters, setters }
Dans le Listing 3.43, on utilise @JoinColumn pour renommer la colonne de clé étrangère en ADD_FK et rendre la relation obligatoire en refusant les valeurs NULL(nullable=false). L’annotation @OneToOne, quant à elle, demande au fournisseur de persistance de parcourir la relation de façon paresseuse. @OnetoMany unidirectionnelle
Dans une relation 1–N, l’objet source référence un ensemble d’objets cibles. Une commande, par exemple, est composée de plusieurs lignes de commande (voir Figure 3.16). Inversement, une ligne de commande pourrait faire référence à la commande dont elle fait partie à l’aide d’une annotation @ManyToOne. Dans la figure, Order est l’extrémité "One" (la source) de la relation et OrderLine est son extrémité "Many" (la cible). Figure 3.16 Une commande compte plusieurs lignes.
La cardinalité est multiple et la navigation ne se fait que dans le sens Order vers OrderLine. En Java, cette multiplicité est décrite par les interfaces Collection, List et Set du paquetage java.util. Le Listing 3.44 présente le code de l’entité Order avec une relation 1–N vers OrderLine (voir Listing 3.45).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
116
Java EE 6 et GlassFish 3
Listing 3.44 : L’entité Order contient des OrderLine @Entity public class Order { @Id @GeneratedValue private Long id; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; private List orderLines; // Constructeurs, getters, setters }
Le code du Listing 3.44 n’utilise pas d’annotation particulière car il repose sur le paradigme de configuration par exception. Le fait qu’une entité ait un attribut qui soit une collection d’un type d’une autre entité déclenche une association OneToMany par défaut. Les relations 1–N unidirectionnelles utilisent par défaut une table de jointure pour représenter la relation ; cette table est une liste de couples de clés étrangères : une clé fait référence à la table ORDER et est du même type que sa clé primaire, l’autre désigne la table ORDER_LINE. Cette table de jointure s’appelle par défaut ORDER_ORDER_ LINE et possède la structure décrite à la Figure 3.17. Si vous n’aimez pas le nom de la table de jointure ou celui des clés étrangères, ou si vous voulez associer la relation à une table existante, vous pouvez vous servir des annotations de JPA pour redéfinir ces valeurs par défaut. Le nom d’une colonne de jointure est formé par défaut par la concaténation du nom de l’entité, d’un blanc souligné et du nom de la clé primaire désignée par la clé étrangère. Comme l’annotation @JoinColumn permet de modifier le nom des colonnes de clés étrangères, @JoinTable fait de même pour la table de jointure. Vous pouvez également utiliser l’annotation @OneToMany (voir Listing 3.46), qui, comme @OneToOne, permet de personnaliser la relation elle-même (mode de parcours, etc.).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
+ID CREATIONDATE
ORM : Object-Relational Mapping 117
ORDER
ORDER LINE
bigint
Nullable = false
timestamp
Nullable = true
+ID
bigint
ITEM
varchar(255) Nullable = true
Nullable = false
UNITPRICE QUANTITY
double integer
Nullable = true Nullable = true
ORDER ORDER LINE +#ORDER ID +#ORDERLINES ID
bigint bigint
Nullable = false Nullable = false
Figure 3.17 Table de jointure entre ORDER et ORDER_LINE.
Dans l’API de l’annotation @JoinTable présentée dans le Listing 3.46, vous pouvez remarquer deux attributs de type @JoinColumn : joinColumns et inverseJoinColumns. Ils permettent de différencier l’extrémité propriétaire de la relation et son extrémité opposée. L’extrémité propriétaire de la relation est décrite dans l’élément joinColumns et, dans notre exemple, désigne la table ORDER. L’extrémité opposée, la cible de la relation, est précisée par l’élément inverseJoinColumns et désigne la table ORDER_LINE. Dans l’entité Order (voir Listing 3.47), vous pouvez ajouter les annotations @OneToMany et @JoinTable pour l’attribut orderLines afin de modifier le nom de la table de jointure en JND_ORD_LINE (au lieu d’ORDER_ORDER_LINE) et pour renommer les deux colonnes de clé étrangère. Listing 3.47 : Entité Order avec une relation 1–N annotée @Entity public class Order { @Id @GeneratedValue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
118
Java EE 6 et GlassFish 3
private Long id; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; @OneToMany @JoinTable(name = "jnd_ord_line", joinColumns = @JoinColumn(name = "order_fk"), inverseJoinColumns = @JoinColumn(name = "order_line_fk") ) private List orderLines; // Constructors, getters, setters
L’entité Order du Listing 3.47 sera associée à la table de jointure décrite dans le Listing 3.48. Listing 3.48 : Structure de la table de jointure create table JND_ORD_LINE ( ORDER_FK BIGINT not null, ORDER_LINE_FK BIGINT not null, primary key (ORDER_FK, ORDER_LINE_FK), foreign key (ORDER_LINE_FK) references ORDER_LINE(ID), foreign key (ORDER_FK) references ORDER(ID) );
La règle par défaut pour une relation 1–N unidirectionnelle consiste à utiliser une table de jointure, mais il est très facile (et utile si vous utilisez une base de données existante) de faire en sorte d’utiliser des clés étrangères (via une colonne de jointure). Pour cela, l’entité Order doit utiliser une annotation @JoinColumn à la place de @JoinTable, comme le montre le Listing 3.49. Listing 3.49 : Entité Order avec une colonne de jointure @Entity public class Order { @Id @GeneratedValue private Long id; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; @OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "order_fk") private List orderLines; // Constructeurs, getters, setters }
Le code de l’entité OrderLine ne change pas, il est identique à celui du Listing 3.45. Vous remarquerez qu’ici l’annotation @OneToMany redéfinit le mode de parcours par défaut (en le fixant à EAGER au lieu de LAZY). En utilisant @JoinColumn, la relation unidirectionnelle est ensuite traduite en utilisant une clé étrangère. Cette clé est
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 119
renommée en ORDER_FK et existe dans la table cible (ORDER_LINE). On obtient alors la structure représentée à la Figure 3.18. Il n’y a plus de table de jointure et la liaison entre les deux tables s’effectue grâce à la clé étrangère ORDER_FK. +ID CREATIONDATE
Figure 3.18 Colonne de jointure entre Order et OrderLine.
@ManytoMany bidirectionnelle
Une relation N–M bidirectionnelle intervient lorsqu’un objet source fait référence à plusieurs cibles et qu’une cible fait référence à plusieurs sources. Un album CD, par exemple, est créé par plusieurs artistes, et un même artiste peut apparaître sur plusieurs albums. Côté Java, chaque entité contiendra donc une collection d’entités cibles. En terme de base de données relationnelle, la seule façon de représenter une relation N–M consiste à utiliser une table de jointure (une colonne de jointure ne peut pas convenir) ; comme nous l’avons vu précédemment, il faut également définir explicitement le propriétaire d’une relation bidirectionnelle à l’aide de l’élément mappedBy. Si l’on suppose que l’entité Artist est propriétaire de la relation, ceci implique que CD est l’extrémité opposée (voir Listing 3.50) et qu’elle doit utiliser l’élément mappedBy de son annotation @ManyToMany. Ici, mappedBy indique au fournisseur de persistance qu’appearsOnCDs est le nom de l’attribut correspondant dans l’entité propriétaire de la relation. Listing 3.50 : Un CD est créé par plusieurs artistes @Entity public class CD { @Id @GeneratedValue private Long id; private String title; private Float price; private String description; @ManyToMany(mappedBy = "appearsOnCDs") private List createdByArtists; // Constructeurs, getters, setters
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
120
Java EE 6 et GlassFish 3
Si Artist est propriétaire de la relation (voir Listing 3.51), c’est donc à cette entité qu’il revient de personnaliser la table de jointure via les annotations @JoinTable et @JoinColumn. Listing 3.51 : Un artiste apparaît sur plusieurs CD @Entity public class Artist { @Id @GeneratedValue private Long id; private String firstName; private String lastName; @ManyToMany @JoinTable(name = "jnd_art_cd", joinColumns = @JoinColumn(name = "artist_fk"), inverseJoinColumns = @JoinColumn(name = "cd_fk")) private List appearsOnCDs; // Constructors, getters, setters
La table de jointure entre Artist et CD est renommée en JND_ART_CD et les noms de ses colonnes sont également modifiés. L’élément joinColumns fait référence à l’extrémité propriétaire (l’Artist), tandis qu’inverseJoinColumns désigne l’extrémité inverse (le CD). La structure de la base obtenue est présentée à la Figure 3.19. ARTIST
Figure 3.19 Les tables Artist, CD et la table de jointure.
Dans une relation N–M et 1–1 bidirectionnelle, chaque extrémité peut, en fait, être considérée comme la propriétaire de la relation. Quoi qu’il en soit, l’autre extrémité doit inclure l’élément mappedBy : dans le cas contraire, le fournisseur considérera que les deux extrémités sont propriétaires et traitera cette relation comme deux relations 1–N unidirectionnelles distinctes. Ici, cela produirait donc quatre tables : ARTIST et CD et deux tables de jointures, ARTIST_CD et CD_ARTIST. On ne peut pas non plus utiliser un élément mappedBy des deux côtés d’une relation.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 121
Chargement des relations
Toutes les annotations que nous avons vues (@OneToOne, @OneToMany, @ManyToOne et @ManyToMany) définissent un attribut de chargement qui précise que les objets associés doivent être chargés immédiatement (chargement "glouton") ou plus tard (chargement "paresseux") et qui influe donc sur les performances. Selon l’application, certaines relations sont utilisées plus souvent que d’autres : dans ces situations, vous pouvez optimiser les performances en chargeant les données de la base lors de la première lecture de l’entité (glouton) ou uniquement lorsqu’elle est utilisée (paresseux). À titre d’exemple, prenons deux cas extrêmes. Supposons que nous ayons quatre entités toutes reliées les unes aux autres avec des cardinalités différentes (1–1, 1–N). Dans le premier cas (voir Figure 3.20), elles ont toutes des relations "gloutonnes", ce qui signifie que, dès que l’on charge Class1 (par une recherche par ID ou par une requête), tous les objets qui en dépendent sont automatiquement chargés en mémoire, ce qui peut avoir certaines répercussions sur votre système. Figure 3.20
Class1
Quatre entités avec des relations gloutonnes.
1
Class2
gloutonne
1..*
Class3
gloutonne
1..*
Class4
gloutonne
Dans le scénario opposé, toutes les relations utilisent un chargement paresseux (voir Figure 3.21). Lorsque l’on charge Class1, rien d’autre n’est placé en mémoire (sauf les attributs directs de Class1, bien sûr). Il faut explicitement accéder à Class2 (via la méthode getter, par exemple) pour que le fournisseur de persistance charge les données à partir de la base, etc. Pour manipuler le graphe complet des objets, il faut donc appeler explicitement chaque entité : class1.getClass2().getClass3().getClass4()
Figure 3.21 Quatre entités avec des relations paresseuses.
Class1
1 paresseuse
Class2
1..* paresseuse
Class3
1..*
Class4
paresseuse
Mais ne pensez pas qu’EAGER est le Mal et LAZY, le Bien. EAGER placera toutes les données en mémoire à l’aide d’un petit nombre d’accès à la base (le fournisseur de persistance utilisera sûrement des jointures pour extraire ces données). Avec LAZY, vous ne risquez plus de remplir la mémoire puisque vous contrôlez les objets qui sont chargés, mais vous devrez faire plus d’accès à la base à chaque fois.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
122
Java EE 6 et GlassFish 3
Le paramètre fetch est très important car, mal utilisé, il peut pénaliser les performances. Chaque annotation a une valeur fetch par défaut que vous devez connaître et changer si elle ne convient pas (voir Tableau 3.2). Tableau 3.2 : Stratégie de chargements par défaut
Annotation
Stratégie de chargement par défaut
@OneToOne
EAGER
@ManyToOne
EAGER
@OneToMany
LAZY
@ManyToMany
LAZY
Lorsque vous chargez une commande (Order) dans votre application, vous avez toujours besoin d’accéder aux lignes de cette commande (OrderLine). Il peut donc être avantageux de changer le mode de chargement par défaut de l’annotation @OneToMany en EAGER (voir Listing 3.52). Listing 3.52 : Order est en relation "gloutonne" vers OrderLine @Entity public class Order { @Id @GeneratedValue private Long id; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; @OneToMany(fetch = FetchType.EAGER) private List orderLines; // Constructeurs, getters, setters }
Tri des relations
Avec les relations 1–N, les entités gèrent des collections d’objets. Du point de vue de Java, ces collections ne sont généralement pas triées et les bases de données relationnelles ne garantissent pas non plus d’ordre sur leurs tables. Si vous voulez obtenir une liste triée, vous devez donc soit trier la collection dans votre programme, soit utiliser une requête JPQL avec une clause Order By. Pour le tri des relations, JPA dispose de mécanismes plus simples reposant sur les annotations.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 123
@OrderBy
L’annotation @OrderBy permet d’effectuer un tri dynamique : les éléments de la collection seront triés lors de leur récupération à partir de la base de données. L’exemple de l’application CD-BookStore permet à un utilisateur d’écrire des articles à propos de musique et de livres : ces articles sont affichés sur le site web et peuvent ensuite être commentés (voir Listing 3.53). Comme nous souhaitons que ces commentaires apparaissent chronologiquement, nous devons les ordonner. Listing 3.53 : Entité Comment avec une date de publication @Entity public class Comment { @Id @GeneratedValue private Long id; private String nickname; private String content; private Integer note; @Column(name = "posted_date") @Temporal(TemporalType.TIMESTAMP) private Date postedDate; // Constructeurs, getters, setters }
Les commentaires sont modélisés par l’entité Comment du Listing 3.53. Ils ont un contenu, sont postés par un visiteur anonyme (identifié par un pseudo) et ont une date de publication de type TIMESTAMP automatiquement créée par le système. Dans l’entité News du Listing 3.54, nous trions la liste des commentaires par ordre décroissant des dates de publication en combinant l’annotation @OrderBy avec @OneToMany. Listing 3.54 : Les commentaires d’une entité News sont triés par ordre décroissant des dates de publication @Entity public class News { @Id @GeneratedValue private Long id; @Column(nullable = false) private String content; @OneToMany(fetch = FetchType.EAGER) @OrderBy("postedDate desc") private List comments; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
124
Java EE 6 et GlassFish 3
L’annotation @OrderBy prend en paramètre les noms des attributs sur lesquels portera le tri (postedDate, ici) et la méthode (représentée par la chaîne ASC ou DESC pour signifier, respectivement, un tri croissant ou décroissant). Vous pouvez utiliser plusieurs paires attribut/méthode en les séparant par des virgules : OrderBy("postedDate desc, note asc"), par exemple, demande de trier d’abord sur les dates de publication (par ordre décroissant), puis sur le champ note (par ordre croissant). Cette annotation n’a aucun impact sur l’association dans la base de données – le fournisseur de persistance est simplement informé qu’il doit utiliser une clause order by lorsque la collection est récupérée. @OrderColumn
JPA 1.0 supportait le tri dynamique avec l’annotation @OrderBy mais ne permettait pas de maintenir un ordre persistant. JPA 2.0 règle ce problème à l’aide d’une nouvelle annotation, @OrderColumn (voir Listing 3.55), qui informe le fournisseur de persistance qu’il doit gérer la liste triée à l’aide d’une colonne séparée contenant un index. Listing 3.55 : L’API de @OrderColumn est semblable à celle de @Column @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface OrderColumn { String name() default ""; boolean nullable() default true; boolean insertable() default true; boolean updatable() default true; String columnDefinition() default ""; boolean contiguous() default true; int base() default 0; String table() default ""; }
Reprenons l’exemple des articles et de leurs commentaires en les modifiant légèrement. Cette fois-ci, l’entité Comment du Listing 3.56 n’a plus d’attribut postedDate : il n’y a donc plus moyen de trier chronologiquement les commentaires. Listing 3.56 : Entité Comment sans date de publication @Entity public class Comment { @Id @GeneratedValue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
private private private private
ORM : Object-Relational Mapping 125
Long id; String nickname; String content; Integer note;
// Constructeurs, getters, setters }
L’entité News présentée dans le Listing 3.57 peut alors annoter la relation avec @OrderColumn afin que le fournisseur de persistance associe l’entité News à une table contenant une colonne supplémentaire pour stocker l’ordre. Listing 3.57 : L’ordre des commentaires est maintenant persistant @Entity public class News { @Id @GeneratedValue private Long id; @Column(nullable = false) private String content; @OneToMany(fetch = FetchType.EAGER) @OrderColumn("posted_index") private List comments; // Constructeurs, getters, setters }
Dans le Listing 3.57, @OrderColumn renomme la colonne supplémentaire en POSTED_INDEX. Si ce nom n’était pas redéfini, cette colonne porterait un nom formé de la concaténation de l’entité référencée et de la chaîne _ORDER (COMMENT_ORDER, ici). Le type de cette colonne doit être numérique. Cette annotation a des conséquences sur les performances car le fournisseur de persistance doit maintenant également gérer les modifications de l’index. Il doit maintenir le tri après chaque insertion, suppression ou réordonnancement. Si des données sont insérées au milieu d’une liste triée, le fournisseur devra retrier tout l’index. Les applications portables ne devraient pas supposer qu’une liste est toujours triée dans la base sous prétexte que certains SGBDR optimisent automatiquement leurs index pour que les données des tables apparaissent dans le bon ordre. Elles doivent plutôt utiliser soit @OrderColumn, soit @OrderBy. Ces deux annotations ne peuvent pas être utilisées en même temps.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
126
Java EE 6 et GlassFish 3
Traduction de l’héritage Depuis leur création, les langages orientés objet utilisent le paradigme de l’héritage. C++ autorise l’héritage multiple alors que Java ne permet d’hériter que d’une seule classe. En programmation orientée objet, les développeurs réutilisent souvent le code en héritant des attributs et des comportements de classes existantes. Nous venons de voir que les relations entre entités ont des équivalents directs dans les bases de données. Ce n’est pas le cas avec l’héritage car ce concept est totalement inconnu du modèle relationnel. Il impose donc plusieurs contorsions pour être traduit dans un SGBDR. Pour représenter un modèle hiérarchique dans un modèle relationnel plat, JPA propose trois stratégies possibles : ■■
Une seule table par hiérarchie de classes. L’ensemble des attributs de toute la hiérarchie des entités est mis à plat et regroupé dans une seule table (il s’agit de la stratégie par défaut).
■■
Jointures entre sous-classes. Dans cette approche, chaque entité de la hiérarchie, concrète ou abstraite, est associée à sa propre table.
■■
Une table par classe concrète. Chaque entité concrète de la hiérarchie est associée à une table. INFO
Le support de la stratégie une table par classe concrète est encore facultatif avec JPA 2.0. Les applications portables doivent donc l’éviter tant que ce support n’a pas été officiellement déclaré comme obligatoire dans toutes les implémentations.
Tirant parti de la simplicité d’utilisation des annotations, JPA 2.0 fournit un support déclaratif pour définir et traduire les hiérarchies d’héritage comprenant des entités concrètes, des entités abstraites, des classes traduites et des classes transitoires. L’annotation @Inheritance s’applique à une entité racine pour imposer une stratégie d’héritage à cette classe et à ses classes filles. JPA traduit aussi la notion objet de redéfinition qui permet aux attributs de la classe racine d’être redéfinis dans les classes filles. Dans la section suivante, nous verrons également comment utiliser les types d’accès avec l’héritage afin de mélanger les accès par champ et par propriété.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 127
Stratégies d’héritage
JPA propose trois stratégies pour traduire l’héritage. Lorsqu’il existe une hiérarchie d’entités, sa racine est toujours une entité qui peut définir la stratégie d’héritage à l’aide de l’annotation @Inheritance. Si elle ne le fait pas, c’est la stratégie par défaut, consistant à créer une seule table par hiérarchie, qui s’applique. Pour expliquer chacune de ces stratégies, nous étudierons comment traduire les entités CD et Book, qui héritent toutes les deux de l’entité Item (voir Figure 3.22). Figure 3.22
est l’entité racine ; elle possède un identifiant qui servira de clé primaire et dont héritent les entités CD et Book. Chacune de ces classes filles ajoute des attributs supplémentaires comme l’ISBN pour Book ou la durée totale d’un album pour CD. Item
Stratégie utilisant une seule table
Il s’agit de la stratégie de traduction de l’héritage par défaut, dans laquelle toutes les entités de la hiérarchie sont associées à la même table. Il n’est donc pas nécessaire d’utiliser l’annotation @Inheritance sur l’entité racine, comme le montre le code d’Item dans le Listing 3.58. Listing 3.58 : L’entité Item définit une stratégie d’héritage avec une seule table @Entity public class Item { @Id @GeneratedValue protected Long id; @Column(nullable = false) protected String title; @Column(nullable = false)
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Item est la classe parente des entités Book (voir Listing 3.59) et CD (voir Listing 3.60).
Ces entités héritent des attributs d’Item ainsi que de la stratégie d’héritage par défaut : elles n’ont donc pas besoin d’utiliser l’annotation @Inheritance. Listing 3.59 : Book hérite d’Item @Entity public class Book extends Item { private private private private
Listing 3.60 : CD hérite d’Item @Entity public class CD extends Item { private String musicCompany; private Integer numberOfCDs; private Float totalDuration; private String gender; // Constructeurs, getters, setters }
Sans l’héritage, ces trois entités seraient traduites en trois tables distinctes. Avec la stratégie de traduction de l’héritage par une seule table, elles finiront toutes dans la même table portant par défaut le nom de la classe racine : ITEM. La structure de cette table est décrite à la Figure 3.23. Comme vous pouvez le constater, la table ITEM rassemble tous les attributs des entités Item, Book et CD. Cependant, elle contient une colonne supplémentaire qui n’est liée à aucun des attributs des entités : la colonne discriminante, DTYPE. La table ITEM sera remplie d’articles, de livres et de CD. Lorsqu’il accède aux données, le fournisseur de persistance doit savoir à quelle entité appartient chaque ligne afin d’instancier la classe d’objet appropriée (Item, Book ou CD) : la colonne discriminante est donc là pour préciser explicitement le type de chaque colonne.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 129
Figure 3.23 Structure de la table ITEM.
ITEM
+ID
bigint
Nullable = false
DTYPE
varchar(31)
Nullable = true
TITLE
varchar(255)
Nullable = false
PRICE DESCRIPTION
double varchar(255)
Nullable = false Nullable = true
ILLUSTRATIONS
smallint
Nullable = true
ISBN
varchar(255)
Nullable = true
NBOFPAGE
integer
Nullable = true
PUBLISHER MUSICCOMPANY
varchar(255) varchar(255)
Nullable = true Nullable = true
NUMBEROFCDS TOTALDURATION
integer double
Nullable = true Nullable = true
GENDER
varchar(255)
Nullable = true
La Figure 3.24 montre un fragment de la table ITEM contenant quelques données. Comme vous pouvez le constater, la stratégie avec une seule table a quelques défauts. On voit, par exemple, que toutes les colonnes ne sont pas utiles à toutes les entités : la première ligne stocke les données d’une entité Item (comme l’indique sa colonne DTYPE) or les instances d’Item n’ont qu’un titre, un prix et une description (voir Listing 3.58) ; elles n’ont pas de compagnie de disque, d’ISBN, etc. Ces colonnes resteront donc toujours vides. Figure 3.24
ID
DTYPE TITLE
PRICE DESCRIPTION
Fragment de contenu de la table ITEM.
1
Item
Pen
2,10
Beautiful black pen
2 3 4 5
CD CD Book Book
SoulTrane ZootAllures The robots of dawn H2G2
23,50 18 22,30 17,50
Fantastic jazz album One of the best of Zappa Robots everywhere Funny IT book ;0)
MUSIC COMPANY
ISBN
... ...
Prestige Warner 0-554-456 1-278-983
... ... ... ...
La colonne discriminante s’appelle DTYPE par défaut, est de type String (traduit en VARCHAR) et contient le nom de l’entité. Si ce comportement ne vous convient pas, vous pouvez utiliser l’annotation @DiscriminatorColumn pour modifier le nom et le type de cette colonne. Dans le Listing 3.61, nous avons renommé la colonne discriminante en DISC (au lieu de DTYPE) et modifié son type en Char au lieu de String ; chaque entité change également sa valeur discriminante en I pour Item, B pour Book (voir Listing 3.62) et C pour CD (voir Listing 3.63). Listing 3.61 : Item redéfinit la colonne discriminante @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn (name = "disc", discriminatorType = DiscriminatorType.CHAR) @ DiscriminatorValue("I")
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
130
Java EE 6 et GlassFish 3
public class Item { @Id @GeneratedValue private Long id; private String title; private Float price; private String description; // Constructeurs, getters, setters }
L’entité racine Item définit la colonne discriminante pour toute la hiérarchie à l’aide de l’annotation @DiscriminatorColumn. Elle change ensuite sa propre valeur en I avec l’annotation @DiscriminatorValue. Les entités filles doivent uniquement redéfinir leur propre valeur discriminante. Listing 3.62 : La valeur discriminante de Book est maintenant B @Entity @DiscriminatorValue("B") public class Book extends Item { private private private private
Listing 3.63 : La valeur discriminante de CD est maintenant C @Entity @DiscriminatorValue("C") public class CD extends Item { private private private private
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 131
Figure 3.25
ID
DISC TITLE
PRICE DESCRIPTION
La table ITEM avec un nom et des valeurs différentes pour la colonne discriminante.
1 2 3 4 5
I C C B B
2,10 23,50 18 22,30 17,50
Pen SoulTrane ZootAllures The robots of dawn H2G2
MUSIC COMPANY
Beautiful black pen Fantastic jazz album Prestige One of the best of Zappa Warner Robots everywhere Funny IT book ;0)
ISBN
...
0-554-456 1-278-983
... ... ... ... ...
Cette stratégie de table unique est la stratégie par défaut ; c’est la plus facile à comprendre et elle fonctionne bien lorsque la hiérarchie est relativement simple et stable. En revanche, elle a quelques défauts : l’ajout de nouvelles entités dans la hiérarchie ou d’attributs dans des entités existantes implique d’ajouter des colonnes à la table, de migrer les données et de modifier les index. Cette stratégie exige également que les colonnes des entités filles puissent recevoir la valeur NULL : si l’ISBN de l’entité Book n’était pas nullable, par exemple, on ne pourrait pas insérer de CD car l’entité CD n’a pas d’attribut ISBN. Stratégie par jointure
Dans cette stratégie, chaque entité de la hiérarchie est associée à sa propre table. L’entité racine est traduite dans une table définissant la clé primaire qui sera utilisée par toutes les tables de la hiérarchie, ainsi qu’une colonne discriminante. Chaque sous-classe est représentée par une table distincte contenant ses propres attributs (non hérités de la classe racine) et une clé primaire qui fait référence à celle de la table racine. Les classes filles n’ont en revanche pas de colonne discriminante. Pour implémenter une stratégie par jointure, on utilise l’annotation @Inheritance comme dans le Listing 3.64 (le code de CD et Book n’est pas modifié). Listing 3.64 : L’entité Item avec une stratégie par jointure @Entity @Inheritance(strategy = InheritanceType.JOINED) public class Item { @Id @GeneratedValue protected Long id; protected String title; protected Float price; protected String description; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
132
Java EE 6 et GlassFish 3
Du point de vue du développeur, la stratégie par jointure est naturelle car chaque entité, qu’elle soit abstraite ou concrète, sera traduite dans une table distincte. La Figure 3.26 montre comment seront transposées les entités Item, Book et CD. BOOK
Figure 3.26 Traduction de l’héritage avec une stratégie par jointure.
Vous pouvez là aussi utiliser les annotations @DiscriminatorColumn et @DiscriminatorValue dans l’entité racine pour personnaliser la colonne discriminante et ses valeurs (la colonne DTYPE de la table ITEM). La stratégie par jointure est intuitive et proche de ce que vous connaissez du mécanisme d’héritage. Cependant, elle a un impact sur les performances des requêtes. En effet, son nom vient du fait que, pour recréer une instance d’une sous-classe, il faut joindre sa table à celle de la classe racine. Plus la hiérarchie est profonde, plus il faudra donc de jointures pour recréer l’entité feuille. Stratégie une table par classe
Dans cette stratégie (une table par classe concrète), chaque entité est traduite dans sa propre table, comme avec la stratégie par jointure. La différence est qu’ici tous les attributs de l’entité racine seront également traduits en colonnes de la table associée à l’entité fille. Du point de vue de la base de données, cette stratégie utilise donc un modèle dénormalisé. Ici, il n’y a pas de table partagée, pas de colonne partagée ni de colonne discriminante. La seule exigence est que toutes les tables de la hiérarchie doivent partager la même clé primaire. Adapter notre exemple à cette stratégie consiste simplement à préciser TABLE_PER_ CLASS dans l’annotation @Inheritance de l’entité racine Item (voir Listing 3.65). Listing 3.65 : L’entité Item avec une stratégie une table par classe @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public class Item { @Id @GeneratedValue protected Long id;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
La Figure 3.27 montre les tables ITEM, BOOK et CD obtenues. Vous remarquerez que BOOK et CD dupliquent les colonnes ID, TITLE, PRICE et DESCRIPTION de la table ITEM et que les tables ne sont pas liées. +ID TITLE PRICE ILLUSTRATIONS DESCRIPTION ISBN NBOFPAGE PUBLISHER
Figure 3.27 Les tables BOOK et CD dupliquent les colonnes d’ITEM.
Chaque table peut être redéfinie en annotant chaque entité avec @Table. Cette stratégie est performante lorsque l’on interroge des instances d’une seule entité car l’on se retrouve alors dans un scénario comparable à l’utilisation de la stratégie à une seule table – la requête ne porte que sur une table. L’inconvénient est que les requêtes polymorphiques à travers une hiérarchie de classes sont plus coûteuses que les deux autres stratégies : pour, par exemple, trouver tous les articles, dont les livres et les CD, il faut interroger toutes les tables des sous-classes avec une opération en utilisant une UNION, ce qui est coûteux lorsqu’il y a beaucoup de données. Redéfinition des attributs
Avec la stratégie une table par classe, les colonnes de la classe racine sont dupliquées dans les classes filles en portant le même nom. Un problème se pose donc si l’on utilise une base existante où ces colonnes ont des noms différents. Pour le résoudre, JPA utilise l’annotation @AttributeOverride pour redéfinir l’association de la colonne et @AttributeOverrides pour en redéfinir plusieurs. Pour renommer les colonnes ID, TITLE et DESCRIPTION dans les tables BOOK et CD, par exemple, le code de l’entité Item ne change pas, mais Book (voir Listing 3.66) et CD (voir Listing 3.67) doivent utiliser l’annotation @AttributeOverride.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
134
Java EE 6 et GlassFish 3
Listing 3.66 : Book redéfinit certaines colonnes d’Item @Entity @AttributeOverrides({ @AttributeOverride(name = "id", column = @Column(name = "book_id")), @AttributeOverride(name = "title", column = @Column(name = "book_title")), @AttributeOverride(name = "description", column = @Column(name = "book_description")) }) public class Book extends Item { private private private private
Ici, il faut redéfinir plusieurs attributs et donc utiliser @AttributeOverrides, qui prend en paramètre un tableau d’annotations @AttributeOverride. Chacune d’elles désigne un attribut de l’entité Item et redéfinit l’association de la colonne à l’aide d’une annotation @Column. Ainsi, name = "title" désigne l’attribut title d’Item et @Column(name = "cd_title") informe le fournisseur de persistance que cet attri-
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 135
but doit être traduit par une colonne CD_TITLE. Le résultat obtenu est présenté à la Figure 3.28. +BOOK_ID
BOOK
bigint
Nullable
ITEM
Nullable
CD
false
+ID
bigint
false
+CD_ID
BOOK TITLE varchar(255) Nullable BOOK DESCRIPTION varchar(255) Nullable
true true
TITLE PRICE
varchar(255) Nullable double Nullable
true true
CD TITLE varchar(255) Nullable CD DESCRIPTION varchar(255) Nullable
Figure 3.28 Les tables BOOK et CD ont redéfini des colonnes d’ITEM.
INFO Dans la section "Classes intégrables" de ce chapitre, nous avons vu qu’un objet intégrable pouvait être partagé par plusieurs entités (Address était intégré dans Customer et Order). Les objets intégrables étant des composantes à part entière de l’entité qui les intègre, leurs colonnes seront également dupliquées dans les tables de chaque entité. Vous pouvez alors utiliser l’annotation @AttributeOverrides si vous avez besoin de redéfinir les colonnes des objets intégrables.
Type de classes dans une hiérarchie d’héritage
L’exemple utilisé pour expliquer les stratégies de traduction de l’héritage n’utilise que des entités, mais les entités n’héritent pas que d’entités. Une hiérarchie de classes peut contenir un mélange d’entités, de classes qui ne sont pas des entités (classes transitoires), d’entités abstraites et de superclasses déjà traduites. Hériter de ces différents types de classes a un impact sur la traduction de la hiérarchie. Entités abstraites
Dans les exemples précédents, l’entité Item était concrète. Elle était annotée par @ Entity et ne comprenait pas de mot-clé abstract ; mais une classe abstraite peut également être désignée comme une entité. Elle ne diffère d’une entité concrète que parce qu’elle ne peut pas être directement instanciée avec le mot-clé new, mais elle fournit une structure de données que partageront toutes ses entités filles (Book et CD) et elle respecte les stratégies de traduction d’héritage. Du point de vue du fournisseur de persistance, la seule différence se situe du côté de Java, pas dans la correspondance en table.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
136
Java EE 6 et GlassFish 3
Non-entités
Les non-entités sont également appelées classes transitoires, ce qui signifie qu’elles sont des POJO. Une entité peut hériter d’une non-entité ou peut être étendue par une non-entité. La modélisation objet et l’héritage permettent de partager les états et les comportements ; dans une hiérarchie de classes, les non-entités peuvent donc servir à fournir une structure de données commune à leurs entités filles. L’état d’une superclasse non entité n’est pas persistant car il n’est pas géré par le fournisseur de persistance (n’oubliez pas que la condition pour qu’une classe le soit est la présence de l’annotation @Entity). Comme le montre le Listing 3.68, Item est désormais une non-entité. Listing 3.68 : Item est un simple POJO sans annotation @Entity public class Item { protected String title; protected Float price; protected String description; // Constructeurs, getters, setters }
L’entité Book du Listing 3.69 hérite d’Item ; le code Java peut donc accéder aux attributs title, price et description ainsi qu’à toutes les méthodes d’Item. Que cette dernière soit concrète ou abstraite n’aura aucune influence sur la traduction finale. Listing 3.69 : L’entité Book hérite d’un POJO @Entity public class Book extends Item { @Id @GeneratedValue private Long id; private String isbn; private String publisher; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 137
est une entité qui hérite d’Item, mais seuls les attributs de Book seront stockés dans une table. Aucun attribut d’Item n’apparaît dans la structure de la table du Listing 3.70. Pour qu’un Book soit persistant, vous devez créer une instance de Book, initialiser les attributs que vous souhaitez (title, price, isbn, publisher, etc.), mais seuls ceux de Book (id, isbn, etc.) seront stockés. Book
Listing 3.70 : La table BOOK ne contient aucun des attributs d’Item create table BOOK ( ID BIGINT not null, ILLUSTRATIONS SMALLINT, ISBN VARCHAR(255), NBOFPAGE INTEGER, PUBLISHER VARCHAR(255), primary key (ID) );
Superclasse "mapped"
JPA définit un type de classe spéciale, appelée superclasse "mapped", qui partage son état, son comportement ainsi que les informations de traduction des entités qui en héritent. Cependant, les superclasses "mapped" ne sont pas des entités, elles ne sont pas gérées par le fournisseur de persistance, n’ont aucune table qui leur soit associée et ne peuvent pas être interrogées ni faire partie d’une relation ; en revanche, elles peuvent fournir des propriétés de persistance aux entités qui en héritent. Les superclasses "mapped" ressemblent aux classes intégrables, sauf qu’elles peuvent être utilisées avec l’héritage. Elles sont annotées par @MappedSuperclass. Dans le Listing 3.71, la classe racine Item est annotée par @MappedSuperclass, pas par @Entity. Elle définit une stratégie de traduction de l’héritage (JOINED) et annote certains de ces attributs avec @Column. Cependant, les superclasses "mapped" n’étant pas associées à des tables, l’annotation @Table n’est pas autorisée. Listing 3.71 : Item est une superclasse "mapped" @MappedSuperclass @Inheritance(strategy = InheritanceType.JOINED) public class Item { @Id @GeneratedValue protected Long id; @Column(length = 50, nullable = false) protected String title;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Comme vous pouvez le constater, les attributs title et description sont annotés par @Column. Le Listing 3.72 montre l’entité Book qui hérite d’Item. Listing 3.72 : Book hérite d’une superclasse "mapped" @Entity public class Book extends Item { private private private private
Cette hiérarchie sera traduite en une seule table. Item n’est pas une entité et n’a donc aucune table associée. Les attributs d’Item et de Book seront traduits en colonnes de la table BOOK – les superclasses "mapped" partageant également leurs informations de traduction, les annotations @Column d’Item seront donc héritées. Le Listing 3.73 montre que les colonnes TITLE et DESCRIPTION de la table BOOK ont bien été modifiées selon les annotations d’Item. Listing 3.73 : Structure de la table BOOK create table BOOK ( ID BIGINT not null, TITLE VARCHAR(50) not null, PRICE DOUBLE(52, 0), ILLUSTRATIONS SMALLINT, DESCRIPTION VARCHAR(2000), ISBN VARCHAR(255), NBOFPAGE INTEGER, PUBLISHER VARCHAR(255), primary key (ID) );
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 3
ORM : Object-Relational Mapping 139
Résumé Grâce à la configuration par exception, il n’y a pas besoin de faire grand-chose pour traduire des entités en tables : il faut simplement informer le fournisseur de persistance qu’une classe est une entité (avec @Entity) et qu’un attribut est un identifiant (avec @Id), et JPA s’occupe du reste. Ce chapitre aurait été bien plus court s’il s’était contenté du comportement par défaut, mais JPA fournit également un grand nombre d’annotations pour adapter le moindre détail de l’ORM. Les annotations élémentaires permettent d’adapter la traduction des attributs (@Basic, etc.) ou des classes. Vous pouvez ainsi modifier le nom de la table ou le type de la clé primaire, voire empêcher le stockage avec l’annotation @Transient. À partir de JPA 2.0, il devient possible de stocker dans la base de données des collections de types de base ou d’objets intégrables. Selon votre modèle, vous pouvez traduire des relations (@OneToOne, @ManyToMany, etc.) de directions et de cardinalités différentes. Il en va de même pour l’héritage (@Inheritance, @MappedSuperclass, etc.), où vous pouvez choisir entre plusieurs stratégies pour traduire une hiérarchie d’entités et de non-entités. @Temporal,
Ce chapitre s’est intéressé à la partie statique de JPA, à la façon d’associer des entités à des tables. Le chapitre suivant présentera les aspects dynamiques : l’interrogation de ces entités.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
4 Gestion des objets persistants L’API de persistance de Java, JPA, a deux aspects. Le premier est la possibilité d’associer des objets à une base de données relationnelle. La configuration par exception permet aux fournisseurs de persistance de faire l’essentiel du travail sans devoir ajouter beaucoup de code, mais la richesse de JPA tient également à la possibilité d’adapter ces associations à l’aide d’annotations ou de descriptions XML. Que ce soit une modification simple (changer le nom d’une colonne, par exemple) ou une adaptation plus complexe (pour traduire l’héritage), JPA offre un large spectre de possibilités. Vous pouvez donc associer quasiment n’importe quel modèle objet à une base de données existante. Le second aspect concerne l’interrogation de ces objets une fois qu’ils ont été associés à une base. Élément central de JPA, le gestionnaire d’entités permet de manipuler de façon standard les instances des entités. Il fournit une API pour créer, rechercher, supprimer et synchroniser les objets avec la base de données et permet d’exécuter différentes sortes de requêtes JPQL sur les entités, comme des requêtes dynamiques, statiques ou natives. Le gestionnaire d’entités autorise également la mise en place de mécanismes de verrouillage sur les données. Le monde des bases de données relationnelles repose sur SQL (Structured Query Language). Ce langage de programmation a été conçu pour faciliter la gestion des données relationnelles (récupération, insertion, mise à jour et suppression), et sa syntaxe est orientée vers la manipulation de tables. Vous pouvez ainsi sélectionner des colonnes de tables constituées de lignes, joindre des tables, combiner les résultats de deux requêtes SQL à l’aide d’une union, etc. Ici, il n’y a pas d’objets mais uniquement des lignes, des colonnes et des tables. Dans le monde Java, où l’on manipule des objets, un langage conçu pour les tables (SQL) doit être un peu déformé pour convenir à un langage à objets (Java). C’est là que JPQL (Java Persistence Query Language) entre en jeu.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
142
Java EE 6 et GlassFish 3
JPQL est le langage qu’utilise JPA pour interroger les entités stockées dans une base de données relationnelle. Sa syntaxe ressemble à celle de SQL mais opère sur des objets entités au lieu d’agir directement sur les tables. JPQL ne voit pas la structure de la base de données sous-jacente et ne manipule ni les tables ni les colonnes – uniquement des objets et des attributs. Il utilise pour cela la notation pointée que connaissent bien tous les développeurs Java. Dans ce chapitre, nous verrons comment gérer les objets persistants. Nous apprendrons comment réaliser les opérations CRUD (Create, Read, Update et Delete) avec le gestionnaire d’entités et créerons des requêtes complexes en JPQL. La fin du chapitre expliquera comment JPA gère la concurrence d’accès aux données.
Interrogation d’une entité Comme premier exemple, étudions une requête simple : trouver un livre par son identifiant. Le Listing 4.1 présente une entité Book utilisant l’annotation @Id pour informer le fournisseur de persistance que l’attribut id doit être associé à une clé primaire. Listing 4.1 : Entité Book simple @Entity public class Book { @Id private private private private private private private
L’entité Book contient les informations pour l’association. Ici, elle utilise la plupart des valeurs par défaut : les données seront donc stockées dans une table portant le même nom que l’entité (BOOK) et chaque attribut sera associé à une colonne homonyme. Nous pouvons maintenant utiliser une classe Main distincte (voir Listing 4.2) qui utilise l’interface javax.persistence.EntityManager pour stocker une instance de Book dans la table.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 143
Listing 4.2 : Classe Main pour stocker et récupérer une entité Book public class Main { public static void main(String[] args) { // 1-Création d’une instance de l’entité Book. Book book = new Book(); book.setId(1234L); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction by Douglas Adams."); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); // 2- Création d’un gestionnaire d’entités et d’une // transaction. EntityManagerFactory emf = Persistence.createEntityManagerFactory("chapter04PU"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); // 3-Stockage du livre dans la base de données. tx.begin(); em.persist(book); tx.commit(); // 4-Récupération du livre par son identifiant. book = em.find(Book.class, 1234L); System.out.println(book); em.close(); emf.close(); } }
La classe Main du Listing 4.2 utilise quatre étapes pour stocker un livre dans la base de données puis le récupérer. 1. Création d’une instance de l’entité Book. Les entités sont des POJO gérés par le fournisseur de persistance. Du point de vue de Java, une instance de classe doit être créée avec le mot-clé new, comme n’importe quel POJO. Il faut bien insister sur le fait qu’à ce stade le fournisseur de persistance ne connaît pas encore l’objet Book. 2. Création d’un gestionnaire d’entités et d’une transaction. C’est la partie importante du code car on a besoin d’un gestionnaire d’entités pour les manipuler. On crée donc d’abord une fabrique de gestionnaires d’entités pour l’unité de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
144
Java EE 6 et GlassFish 3
persistance chapter04PU. Cette fabrique sert ensuite à fournir un gestionnaire (la variable em) qui permettra de créer une transaction (la variable tx), puis à stocker et à récupérer un objet Book. 3. Stockage du livre dans la base de données. Le code lance une transaction (tx.begin()) et utilise la méthode EntityManager.persist() pour insérer une instance de Book. Lorsque la transaction est validée (tx.commit()), les données sont écrites dans la base de données. 4. Récupération d’un livre par son identifiant. Là encore, on utilise le gestionnaire d’entités afin de retrouver un livre à partir de son identifiant à l’aide de la méthode EntityManager.find(). Vous remarquerez que ce code ne contient aucune requête SQL ou JPQL ni d’appels JDBC. La Figure 4.1 montre l’interaction entre ces composants. La classe Main interagit avec la base de données sous-jacente via l’interface EntityManager, qui fournit un ensemble de méthodes standard permettant de réaliser des opérations sur l’entité Book. En coulisse, cet EntityManager utilise le fournisseur de persistance pour interagir avec la base de données. Lorsque l’on appelle l’une des méthodes de l’EntityManager, le fournisseur de persistance produit et exécute une instruction SQL via le pilote JDBC correspondant. Main
Book -id : Long -title : String -price : Float -description : String -nbOfPage : Integer -illustrations : Boolean
Figure 4.1 Le gestionnaire d’entités interagit avec l’entité et la base de données sous-jacente.
Quel pilote JDBC utiliser ? Comment se connecter à la base ? Quel est le nom de la base ? Toutes ces informations sont absentes du code précédent. Lorsque la classe Main crée une fabrique EntityManagerFactory, elle lui passe le nom d’une unité de persistance en paramètre – chapter04PU ici. Cette unité de persistance indique au gestionnaire d’entités le type de la base à utiliser et les paramètres de connexion :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 145
toutes ces informations sont précisées dans le fichier ting 4.3) qui doit être déployé avec les classes.
persistence.xml
(voir Lis-
Listing 4.3 : Le fichier persistence.xml définit l’unité de persistance org.eclipse.persistence.jpa.PersistenceProvider com.apress.javaee6.chapter04.Book
L’unité de persistance chapter04PU définit une connexion JDBC pour la base de données Derby nommée chapter04DB. Elle se connecte à cette base sous le compte utilisateur APP avec le mot de passe APP. Le marqueur demande au fournisseur de persistance de gérer la classe Book. Pour que ce code fonctionne, le SGBDR Derby doit s’exécuter sur le port 1527 et les classes Book et Main doivent avoir été compilées et déployées avec ce fichier META-INF/persistence.xml. Si vous avez activé la trace d’exécution, vous verrez apparaître quelques instructions SQL mais, grâce à l’API d’EntityManager, votre code manipule des objets de façon orientée objet, sans instruction SQL ni appel JDBC.
Le gestionnaire d’entités Le gestionnaire d’entités est une composante essentielle de JPA. JPA gère l’état et le cycle de vie des entités et les interroge dans un contexte de persistance. C’est également lui qui est responsable de la création et de la suppression des instances d’entités persistantes et qui les retrouve à partir de leur clé primaire. Il peut les verrouiller
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
146
Java EE 6 et GlassFish 3
pour les protéger des accès concurrents en utilisant un verrouillage optimiste ou pessimiste et se servir de requêtes JPQL pour rechercher celles qui répondent à certains critères. Lorsqu’un gestionnaire d’entités obtient une référence à une entité, celle-ci est dite gérée. Avant cela, elle n’était considérée que comme un POJO classique (elle était détachée). L’avantage de JPA est que les entités peuvent être utilisées comme des objets normaux par différentes couches de l’application et devenir gérées par le gestionnaire d’entités lorsqu’il faut charger ou insérer des données dans la base. Lorsqu’une entité est gérée, il devient possible d’effectuer des opérations de persistance : le gestionnaire d’entités synchronisera automatiquement l’état de l’entité avec la base de données. Lorsqu’une entité est détachée (non gérée), elle redevient un simple POJO et peut être utilisée par les autres couches (une couche de présentation JSF, par exemple) sans que son état ne soit synchronisé avec la base. Le véritable travail de persistance commence avec le gestionnaire d’entités. L’interface javax.persistence.EntityManager est implémentée par un fournisseur de persistance qui produira et exécutera des instructions SQL. Le Listing 4.4 présente son API de manipulation des entités. Listing 4.4 : EntityManager API public interface EntityManager { public public public public
void persist(Object entity); T merge(T entity); void remove(Object entity); T find(Class entityClass, Object primaryKey); T find(Class entityClass, Object primaryKey, LockModeType lockMode); public T find(Class entityClass, Object primaryKey, LockModeType lockMode, Map properties); public T getReference(Class entityClass, Object primaryKey); public void flush(); public void setFlushMode(FlushModeType flushMode); public FlushModeType getFlushMode();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 147
public void lock(Object entity, LockModeType lockMode); public void lock(Object entity, LockModeType lockMode, Map properties); public void refresh(Object entity); public void refresh(Object entity, LockModeType lockMode); public void refresh(Object entity, LockModeType lockMode, Map properties); public void clear(); public void detach(Object entity); public boolean contains(Object entity); public Map getProperties(); public Set getSupportedProperties(); public public public public public
Query Query Query Query Query
createQuery(String qlString); createQuery(QueryDefinition qdef); createNamedQuery(String name); createNativeQuery(String sqlString); createNativeQuery(String sqlString, Class resultClass); public Query createNativeQuery(String sqlString, String resultSetMapping); public void joinTransaction(); public T unwrap(Class cls); public Object getDelegate(); public QueryBuilder getQueryBuilder(); }
Dans la section suivante, nous verrons comment obtenir une instance d’EntityManager. Ne soyez pas effrayé par l’API du Listing 4.4 : ce chapitre expliquera la plupart de ces méthodes. Obtenir un gestionnaire d’entités
Le gestionnaire d’entités est l’interface centrale pour interagir avec les entités, mais l’application doit d’abord en obtenir un. Selon que l’on soit dans un environnement géré par un conteneur (comme nous le verrons au Chapitre 6 avec les EJB) ou géré par une application, le code peut être très différent. Dans le premier cas, par exemple, c’est le conteneur qui gère les transactions, ce qui signifie que l’on n’a pas besoin d’appeler explicitement les opérations commit() ou rollback(), contrairement à un environnement géré par l’application.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
148
Java EE 6 et GlassFish 3
Le terme "géré par l’application" signifie que c’est l’application qui est responsable de l’obtention explicite d’une instance d’EntityManager et de la gestion de son cycle de vie (elle doit fermer le gestionnaire d’entités lorsqu’elle n’en a plus besoin, par exemple). Dans le Listing 4.2, nous avons vu comment une classe qui s’exécutait dans l’environnement Java SE obtenait une instance du gestionnaire : elle utilise la classe Persistence pour lancer une fabrique EntityManagerFactory associée à une unité de persistance (chapter04PU), et cette fabrique sert ensuite à créer un gestionnaire d’entités. L’utilisation d’une fabrique pour créer un gestionnaire d’entités est assez simple, mais ce qui différencie un environnement géré par l’application d’un environnement géré par un conteneur est la façon dont on obtient cette fabrique. Dans un environnement géré par un conteneur, l’application s’exécute dans une servlet ou dans un conteneur d’EJB. Avec Java EE, la méthode la plus classique pour obtenir un gestionnaire d’entités consiste alors soit à utiliser l’annotation @PersistenceContext pour en injecter un, soit à utiliser JNDI. Un composant qui s’exécute dans un conteneur (servlet, EJB, service web, etc.) n’a en revanche pas besoin de créer ou de fermer le gestionnaire d’entités puisque son cycle de vie est géré par le conteneur. Le Listing 4.5 montre le code d’une session sans état dans laquelle on injecte une référence à l’unité de persistance chapter04PU. Listing 4.5 : Injection d’une référence à un gestionnaire d’entités dans un EJB sans état @Stateless public class BookBean { @PersistenceContext(unitName = "chapter04PU") private EntityManager em; public void createBook() { // Création d’une instance de Book. Book book = new Book(); book.setId(1234L); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction by Douglas Adams."); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); // Stockage de l’instance dans la base de données. em.persist(book);
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 149
// Récupération d’une instance par son identifiant. book = em.find(Book.class, 1234L); System.out.println(book); } }
Le code du Listing 4.5 est bien plus simple que celui du Listing 4.2 : il n’y a pas besoin des objets Persistence ou EntityManagerFactory car le gestionnaire d’entités est injecté par le conteneur. En outre, les beans sans état gérant les transactions, il n’est pas non plus nécessaire d’appeler explicitement commit() ou rollback(). Nous reviendrons sur ce style de gestionnaire d’entités au Chapitre 6. Contexte de persistance
Avant d’explorer en détail l’API de l’EntityManager, vous devez avoir compris un concept essentiel : le contexte de persistance, qui est l’ensemble des instances d’entités gérées à un instant donné. Dans un contexte de persistance, il ne peut exister qu’une seule instance d’entité avec le même identifiant de persistance – si, par exemple, une instance de Book ayant l’identifiant 1234 existe dans le contexte de persistance, aucun autre livre portant cet identifiant ne peut exister dans le même contexte. Seules les entités contenues dans le contexte de persistance sont gérées par le gestionnaire d’entités – leurs modifications seront reflétées dans la base de données. Le gestionnaire d’entités modifie ou consulte le contexte de persistance à chaque appel d’une méthode de l’interface javax.persistence.EntityManager. Lorsque la méthode persist() est appelée, par exemple, l’entité passée en paramètre sera ajoutée au contexte de persistance si elle ne s’y trouve pas déjà. De même, lorsque l’on recherche une entité à partir de son identifiant, le gestionnaire d’entités vérifie d’abord si elle existe déjà dans le contexte de persistance. Ce contexte peut donc être considéré comme un cache de premier niveau : c’est un espace réduit où le gestionnaire stocke les entités avant d’écrire son contenu dans la base de données. Les objets ne vivent dans le contexte de persistance que le temps de la transaction. La configuration d’un gestionnaire d’entités est liée à la fabrique qui l’a créé. Que l’on se trouve dans un environnement géré par l’application ou par un conteneur, la fabrique a besoin d’une unité de persistance pour créer le gestionnaire. Celle-ci, définie dans le fichier META-INF/persistence.xml (voir Listing 4.6), précise les informations nécessaires pour la connexion à la base de données et donne la liste
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
150
Java EE 6 et GlassFish 3
des entités qui pourront être gérées dans un contexte persistant. Elle porte un nom (chapter04PU) et a un ensemble d’attributs. Listing 4.6 : Unité de persistance avec un ensemble d’entités gérables org.eclipse.persistence.jpa.PersistenceProvider com.apress.javaee6.chapter04.Book com.apress.javaee6.chapter04.Customer com.apress.javaee6.chapter04.Address
L’unité de persistance est le pont qui relie le contexte de persistance et la base de données. D’un côté, les marqueurs donnent la liste des entités pouvant être gérées dans le contexte de persistance, de l’autre, le fichier donne toutes les informations permettant de se connecter physiquement à la base. Ici, nous sommes dans un environnement géré par l’application (transaction-type="RESOURCE_ LOCAL"). Dans un environnement géré par un conteneur, le fichier persistence.xml définirait une source de données à la place des informations de connexion et le type de transaction serait JTA (transaction-type="JTA"). Manipulation des entités
Le gestionnaire d’entités sert également à créer des requêtes JPQL complexes pour récupérer une ou plusieurs entités. Lorsqu’elle manipule des entités uniques, l’interface EntityManager peut être considérée comme un DAO (Data Access Object) générique permettant d’effectuer les opérations CRUD sur n’importe quelle entité (voir Tableau 4.1).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 151
Tableau 4.1 : Méthodes de l’interface EntityManager pour manipuler les entités
Méthode
Description
void persist(Object entity)
Crée une instance gérée et persistante.
T find(Class entityClass, Object primaryKey)
Recherche une entité de la classe et de la clé indiquées.
T getReference(Class entityClass, Object primaryKey)
Obtient une instance dont l’état peut être récupéré de façon paresseuse.
void remove(Object entity)
Supprime l’instance d’entité du contexte de persistance et de la base de données.
T merge(T entity)
Fusionne l’état de l’entité indiquée dans le contexte de persistance courant.
void refresh(Object entity)
Rafraîchit l’état de l’instance à partir de la base de données en écrasant les éventuelles modifications apportées à l’entité.
void flush()
Synchronise le contexte de persistance avec la base de données.
void clear()
Vide le contexte de persistance. Toutes les entités gérées deviennent détachées.
void clear(Object entity)
Supprime l’entité indiquée du contexte de persistance.
boolean contains(Object entity)
Teste si l’instance est une entité gérée appartenant au contexte de persistance courant
Pour mieux comprendre ces méthodes, nous utiliserons un exemple simple d’une relation 1–1 unidirectionnelle entre un client et son adresse. Les deux entités Customer (voir Listing 4.7) et Address (voir Listing 4.8) ont des identifiants produits automatiquement (grâce à l’annotation @GeneratedValue), et Customer récupère l’Address de façon paresseuse (c’est-à-dire uniquement lorsqu’il en a besoin). Listing 4.7 : L’entité Customer avec une relation 1–1 unidirectionnelle avec Address @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Ces deux entités seront traduites dans la base de données avec la structure présentée à la Figure 4.2. Notez que la colonne ADDRESS_FK est la colonne de type clé étrangère permettant d’accéder à ADDRESS.
Pour plus de visibilité, les fragments de code utilisés dans la section suivante supposent que l’attribut em est de type EntityManager et que tx est de type EntityTransaction. Rendre une entité persistante
Rendre une entité persistante signifie que l’on insère les données dans la base si elles ne s’y trouvent pas déjà (sinon une exception sera lancée). Pour ce faire, il faut créer une instance de l’entité avec l’opérateur new, initialiser ses attributs, lier une entité
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 153
à une autre lorsqu’il y a des relations et, enfin, appeler la méthode EntityManager. persist() comme le montre le cas de test JUnit du Listing 4.9. Listing 4.9 : Rendre persistant un Customer avec une Address Customer customer = new Customer("Antony", "Balla", "[email protected]"); Address address = new Address("Ritherdon Rd", "London", "8QE", "UK"); customer.setAddress(address); tx.begin(); em.persist(customer); em.persist(address); tx.commit(); assertNotNull(customer.getId()); assertNotNull(address.getId());
Dans le Listing 4.9, le client et l’adresse ne sont que deux objets qui résident dans la mémoire de la JVM. Tous les deux ne deviennent des entités gérées que lorsque le gestionnaire d’entités (la variable em) les prend en compte en les rendant persistantes (em.persist(customer)). Dès cet instant, les deux objets deviennent candidats à une insertion dans la base de données. Lorsque la transaction est validée (tx.commit()), les données sont écrites dans la base : une ligne d’adresse est ajoutée à la table ADDRESS et une ligne client, à la table CUSTOMER. L’entité Customer étant la propriétaire de la relation, sa table contient une clé étrangère vers ADDRESS. Les deux expressions assertNotNull testent que les deux entités ont bien reçu un identifiant (fourni automatiquement par le fournisseur de persistance grâce aux annotations). Notez l’ordre d’appel des méthodes persist() : on rend d’abord le client persistant, puis son adresse. Si l’on avait fait l’inverse, le résultat aurait été le même. Plus haut, nous avons écrit que l’on pouvait considérer le gestionnaire d’entités comme un cache de premier niveau : tant que la transaction n’est pas validée, les données restent en mémoire et il n’y a aucun accès à la base. Le gestionnaire d’entités met les données en cache et, lorsqu’il est prêt, les écrit dans l’ordre qu’attend la base de données (afin de respecter les contraintes d’intégrité). À cause de la clé étrangère que contient la table CUSTOMER, l’instruction insert dans ADRESS doit s’effectuer en premier, suivie de celle de CUSTOMER.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
154
Java EE 6 et GlassFish 3
INFO La plupart des entités de ce chapitre n’implémentent pas l’interface Serializable car elles n’en ont tout simplement pas besoin pour être rendues persistantes. Elles sont passées par référence d’une méthode à l’autre et ce n’est que lorsqu’il faut les rendre persistantes que l’on appelle la méthode EntityManager.persist(). Si, toutefois, vous devez passer des entités par valeur (appel distant, conteneur EJB externe, etc.), celles-ci doivent implémenter l’interface java.io.Serializable pour indiquer au compilateur qu’il faut que tous les champs de l’entité soient sérialisables afin qu’une instance puisse être sérialisée dans un flux d’octets et passée par RMI (Remote Method Invocation).
Recherche par identifiant
Nous pouvons utiliser deux méthodes pour trouver une entité par son identifiant. La première est EntityManager.find(), qui prend deux paramètres : la classe de l’entité et l’identifiant (voir Listing 4.10). Cet appel renvoie l’entité si elle a été trouvée, null sinon. Listing 4.10 : Recherche d’un client par son identifiant Customer customer = em.find(Customer.class, 1234L); if (customer!= null) { // Traiter l’objet }
La seconde méthode est getReference() (voir Listing 4.11). Elle ressemble beaucoup à find() car elle prend les mêmes paramètres, mais elle permet de récupérer une référence d’entité à partir de sa clé primaire, pas à partir de ses données. Cette méthode est prévue pour les situations où l’on a besoin d’une instance d’entité gérée, mais d’aucune autre donnée que la clé primaire de l’entité recherchée. Lors d’un appel à getReference(), les données de l’état sont récupérées de façon paresseuse, ce qui signifie que, si l’on n’accède pas à l’état avant que l’entité soit détachée, les données peuvent être manquantes. Cette méthode lève l’exception EntityNotFoundException si elle ne trouve pas l’entité. Listing 4.11 : Recherche d’un client par référence try { Customer customer = em.getReference(Customer.class, 1234L); // Traiter l’objet } catch(EntityNotFoundException ex) { // Entité non trouvée }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 155
Suppression d’une entité
La méthode EntityManager.remove() supprime une entité qui est alors également ôtée de la base, détachée du gestionnaire d’entités et qui ne peut plus être synchronisée avec la base. En termes d’objets Java, l’entité reste accessible tant qu’elle ne sort pas de la portée et que le ramasse-miettes ne l’a pas supprimée. Le code du Listing 4.12 montre comment supprimer une entité après l’avoir créée. Listing 4.12 : Création et suppression d’entités Customer et Address Customer customer = new Customer("Antony", "Balla", "[email protected]"); Address address = new Address("Ritherdon Rd", "London", "8QE", "UK"); customer.setAddress(address); tx.begin(); em.persist(customer); em.persist(address); tx.commit(); tx.begin(); em.remove(customer); tx.commit(); // Les données sont supprimées de la base // mais l’objet reste accessible assertNotNull(customer);
Le code du Listing 4.12 crée une instance de Customer et d’Address, lie l’adresse au client (customer.setAddress(address)) et les rend persistantes. Dans la base de données, la ligne du client est liée à son adresse via une clé étrangère. Puis le code ne supprime que l’entité Customer : selon la configuration de la suppression en cascade, l’instance d’Address peut être laissée intacte alors qu’aucune autre entité ne la référence plus – la ligne d’adresse est alors orpheline. Suppression des orphelins
Pour des raisons de cohérence des données, il faut éviter de produire des orphelins car ils correspondent à des lignes de la base de données qui ne sont plus référencées par aucune autre table et qui ne sont donc plus accessibles. Avec JPA vous pouvez demander au fournisseur de persistance de supprimer automatiquement les orphelins ou de répercuter en cascade une opération de suppression. Si une entité cible (Address) appartient uniquement à une source (Customer) et que cette source soit supprimée par l’application, le fournisseur doit également supprimer la cible.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
156
Java EE 6 et GlassFish 3
Les relations 1–1 ou 1–N disposent d’une option demandant la suppression des orphelins. Dans notre exemple, il suffit d’ajouter l’élément orphanRemoval=true à l’annotation @OneToOne, comme dans le Listing 4.13. Listing 4.13 : L’entité Customer gère la suppression des Address orphelines @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; @OneToOne (fetch = FetchType.LAZY, orphanRemoval = true) private Address address; // Constructeurs, getters, setters
}
Désormais, le code du Listing 4.12 supprimera automatiquement l’entité Address lorsque le client sera supprimé. L’opération de suppression intervient au moment de l’écriture dans la base de données (lorsque la transaction est validée). Synchronisation avec la base de données
Jusqu’à maintenant, la synchronisation avec la base s’est effectuée lorsque la transaction est validée. Le gestionnaire d’entités est un cache de premier niveau qui attend cette validation pour écrire les données dans la base, mais que se passe-t-il lorsqu’il faut insérer un client et une adresse ? tx.begin(); em.persist(customer); em.persist(address); tx.commit();
Toutes les modifications en attente exigent une instruction SQL et les deux insert ne seront produits et rendus permanents que lorsque la transaction sera validée par commit(). Pour la plupart des applications, cette synchronisation automatique suffit : on ne sait pas exactement quand le fournisseur écrira vraiment les données dans la base, nous pouvons être sûrs que l’écriture aura lieu lorsque la transaction sera validée. Bien que la base de données soit synchronisée avec les entités dans le contexte de persistance, nous pouvons explicitement écrire des données dans la base (avec flush()) ou, inversement, rafraîchir des données à partir de la base (avec refresh()). Si des données sont écrites dans la base à un instant précis et que l’application appelle plus tard la méthode rollback() pour annuler la transaction, les données écrites seront supprimées de la base.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 157
Écriture de données
La méthode EntityManager.flush force le fournisseur de persistance à écrire les données dans la base ; elle permet donc de déclencher manuellement le même processus que celui utilisé en interne par le gestionnaire d’entités lorsqu’il écrit le contexte de persistance dans la base de données. tx.begin(); em.persist(customer); em.flush(); em.persist(address); tx.commit();
Il se passe deux choses intéressantes dans le code précédent. La première est que em.flush() n’attendra pas que la transaction soit validée pour écrire le contexte de persistance dans la base de données : une instruction insert sera produite et exécutée à l’instant de l’appel à flush(). La seconde est que ce code ne fonctionnera pas à cause des contraintes d’intégrité. Sans écriture explicite, le gestionnaire d’entités met en cache toutes les modifications, les ordonne et les exécute de façon cohérente du point de vue de la base. Avec une écriture explicite, l’instruction insert sur la table CUSTOMER s’exécutera mais la contrainte d’intégrité sur la clé étrangère (la colonne ADDRESS_FK de CUSTOMER) sera violée et la transaction sera donc annulée. Les données écrites seront alors supprimées de la base. Vous devez donc faire attention lorsque vous utilisez des écritures explicites et ne les utiliser que lorsqu’elles sont nécessaires. Rafraîchissement d’une entité
La méthode refresh() effectue une synchronisation dans la direction opposée de flush(), c’est-à-dire qu’elle écrase l’état courant d’une entité gérée avec les données qui se trouvent dans la base. Son utilisation typique consiste à annuler des modifications qui ont été faites sur l’entité en mémoire. L’extrait de classe de test du Listing 4.14 recherche un client par son identifiant, modifie son prénom et annule ce changement en appelant la méthode refresh(). Listing 4.14 : Rafraîchissement de l’entité Customer à partir de la base de données Customer customer = em.find(Customer.class, 1234L); assertEquals(customer.getFirstName(), "Antony"); customer.setFirstName("William"); em.refresh(customer); assertEquals(customer.getFirstName(), "Antony");
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
158
Java EE 6 et GlassFish 3
Contenu du contexte de persistance
Le contexte de persistance contient les entités gérées. Grâce à l’interface EntityManager, vous pouvez tester si une entité est gérée et supprimer toutes les entités du contexte de persistance. Contains
La méthode EntityManager.contains() renvoie un Boolean indiquant si une instance d’entité particulière est actuellement gérée par le gestionnaire d’entités dans le contexte de persistance courant. Le cas de test du Listing 4.15 rend un Customer persistant – on peut immédiatement vérifier que l’entité est gérée (em. contains(customer) renvoie true). Puis on appelle la méthode remove() pour supprimer cette entité de la base de données et du contexte de persistance ; l’appel à em.contains(customer) renvoie alors false. Listing 4.15 : Cas de test pour vérifier que l’entité Customer se trouve dans le contexte de persistance Customer customer = new Customer("Antony", "Balla", "[email protected]"); tx.begin(); em.persist(customer); tx.commit(); assertTrue(em.contains(customer)); tx.begin(); em.remove(customer); tx.commit(); assertFalse(em.contains(customer));
Clear et Detach
La méthode clear() porte bien son nom car elle vide le contexte de persistance : toutes les entités qui étaient gérées deviennent donc détachées. La méthode detach(Object entity) supprime l’entité indiquée du contexte de persistance – après cette éviction, les modifications apportées à cette entité ne seront plus synchronisées avec la base de données. Le Listing 4.16 crée une entité, vérifie qu’elle est gérée, puis la supprime du contexte de persistance et vérifie qu’elle est bien détachée.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 159
Listing 4.16 : Test si l’entité Customer se trouve dans le contexte de persistance Customer customer = new Customer("Antony", "Balla", "[email protected]"); tx.begin(); em.persist(customer); tx.commit(); assertTrue(em.contains(customer)); em.detach(customer); assertFalse(em.contains(customer));
La méthode clear() peut agir sur tout le contexte de persistance (clear uniquement sur une entité (clear(Object entity)).
())
ou
Fusion d’une entité
Une entité détachée n’est plus associée à un contexte de persistance. Si vous voulez la gérer, vous devez la fusionner. Prenons l’exemple d’une entité devant s’afficher dans une page JSF. L’entité est d’abord chargée à partir de la base de données dans la couche de persistance (elle est gérée), elle est renvoyée par un appel d’un EJB local (elle est détachée car le contexte de transaction s’est terminé), la couche de présentation l’affiche (elle est toujours détachée), puis elle revient pour être mise à jour dans la base de données. Cependant, à ce moment-là, l’entité est détachée et doit donc être attachée à nouveau – fusionnée – afin de synchroniser son état avec la base. Le Listing 4.17 simule cette situation en vidant le contexte de persistance avec clear() afin de détacher l’entité. Listing 4.17 : Nettoyage du contexte de persistance Customer customer = new Customer("Antony", "Balla", "[email protected]"); tx.begin(); em.persist(customer); tx.commit(); em.clear(); // Modifie une valeur d’une entité détachée. customer.setFirstName("William"); tx.begin(); em.merge(customer); tx.commit();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
160
Java EE 6 et GlassFish 3
Le Listing 4.17 crée un client et le rend persistant. L’appel em.clear() force le détachement de l’entité client mais les entités détachées continuent de vivre en dehors du contexte de persistance dans lequel elles étaient ; par contre, la synchronisation de leur état avec celui de la base de données n’est plus garantie. Le setter customer. setFirstName("William") est donc exécuté sur une entité détachée et les données ne sont pas modifiées dans la base. Pour répercuter cette modification, il faut réattacher l’entité (c’est-à-dire la fusionner) avec un appel à em.merge(customer). Modification d’une entité
Bien que la modification d’une entité soit simple, elle peut en même temps être difficile à comprendre. Comme nous venons de le voir, vous pouvez utiliser EntityManager.merge() pour attacher une entité et synchroniser son état avec la base de données. Lorsqu’une entité est gérée, les modifications qui lui sont apportées seront automatiquement reflétées mais, si elle ne l’est pas, vous devez appeler explicitement merge(). Le Listing 4.18 rend persistant un client prénommé Antony. Lorsque la méthode est appelée, l’entité devient gérée et toutes les modifications qui lui seront désormais appliquées seront donc répercutées dans la base de données. L’appel de la méthode setFirstName() modifie l’état de l’entité. Le gestionnaire d’entités met en cache toutes les actions exécutées à partir de tx.begin() et ne les répercute dans la base que lorsque la transaction est validée avec tx.commit().
em.persist()
Listing 4.18 : Modification du prénom d’un client Customer customer = new Customer("Antony", "Balla", "[email protected]"); tx.begin(); em.persist(customer); customer.setFirstName("William"); tx.commit();
Répercussion d’événements
Par défaut, chaque opération du gestionnaire d’entités ne s’applique qu’à l’entité passée en paramètre à l’opération. Parfois, cependant, on souhaite propager son action à ses relations – c’est ce que l’on appelle répercuter un événement. Jusqu’à présent, nos exemples reposaient sur ce comportement par défaut : le Listing 4.19, par exemple, crée un client en instanciant une entité Customer et une entité Address,
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 161
en les liant (avec customer. deux persistantes.
setAddress(address)),
puis en les rendant toutes les
Listing 4.19 : Rendre Customer et son Address persistantes Customer customer = new Customer("Antony", "Balla", "[email protected]"); Address address = new Address("Ritherdon Rd", "London", "8QE", "UK"); customer.setAddress(address); tx.begin(); em.persist(customer); em.persist(address); tx.commit();
Comme il existe une relation entre Customer et Address, on peut répercuter l’action persist() du client vers son adresse. Ceci signifie qu’un appel à em.persist(customer) répercutera l’événement persist à l’entité Address si elle autorise la propagation de ce type d’événement. Le code peut donc être allégé en ôtant l’instruction em.persist(address), comme le montre le Listing 4.20. Listing 4.20 : Propagation d’un événement persist à Address Customer customer = new Customer("Antony", "Balla", "[email protected]"); Address address = new Address("Ritherdon Rd", "London", "8QE", "UK"); customer.setAddress(address); tx.begin(); em.persist(customer); tx.commit();
Sans cette répercussion, le client serait persistant, mais pas son adresse. Pour que cette répercussion ait lieu, l’association de la relation doit donc être modifiée. Les annotations @OneToOne, @OneToMany, @ManyToOne et @ManyToMany disposent d’un attribut cascade pouvant recevoir un tableau d’événements à propager. Nous devons donc modifier l’association de l’entité Customer (voir Listing 4.21) en ajoutant un attribut cascade à l’annotation @OneToOne. Ici, on ne se contente pas de propager persist, on fait de même pour l’événement remove, afin que la suppression d’un client entraîne celle de son adresse. Listing 4.21 : L’entité Customer propage les événements persist et remove @Entity public class Customer {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Le Tableau 4.2 énumère les événements que vous pouvez propager vers une cible de relation. Vous pouvez même tous les propager en utilisant le type CascadeType.ALL. Tableau 4.2 : Événements pouvant être propagés
Type
Description
PERSIST
Propage les opérations persist à la cible de la relation.
REMOVE
Propage les opérations remove à la cible de la relation.
MERGE
Propage les opérations merge à la cible de la relation.
REFRESH
Propage les opérations refresh à la cible de la relation.
CLEAR
Propage les opérations clear à la cible de la relation.
ALL
Propage toutes les opérations précédentes.
L’API de cache
La plupart des spécifications (pas seulement JAVA EE) s’intéressent beaucoup aux fonctionnalités et considèrent le reste, comme les performances, l’adaptabilité ou la mise en cluster, comme des détails d’implémentation. Les implémentations doivent respecter strictement la spécification mais peuvent également ajouter des fonctionnalités spécifiques. Un parfait exemple pour JPA serait la gestion d’un cache. Jusqu’à JPA 2.0, la mise en cache n’était pas mentionnée dans la spécification. Comme on l’a déjà évoqué, le gestionnaire d’entités est un cache de premier niveau utilisé pour traiter les données afin qu’elles conviennent à la base de données et pour mettre en cache les entités en cours d’utilisation. Ce cache permet de réduire le nombre de requêtes SQL de chaque transaction – si un objet est modifié plusieurs
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 163
fois au cours de la même transaction, le gestionnaire d’entités ne produira qu’une seule instruction UPDATE à la fin de cette transaction –, mais un cache de premier niveau n’est pas un cache de performance. Toutes les implémentations de JPA utilisent un cache de performance (appelé cache de second niveau) pour optimiser les accès à la base de données, les requêtes, les jointures, etc. Les caches de second niveau réduisent le trafic avec la base de données car ils conservent les objets en mémoire et les rendent disponibles à toute l’application. Chaque implémentation utilise sa propre technique de cache – en développant ses propres mécanismes ou en utilisant des solutions open-source. Le cache peut être distribué sur un cluster ou non – en fait, tout est possible puisque la spécification ne dit rien sur le sujet. JPA 2.0 reconnaît la nécessité d’un cache de second niveau et a donc ajouté des opérations de gestion du cache dans une API standard. Celle-ci, présentée dans le Listing 4.22, est minimaliste – le but de JPA n’est pas de standardiser un cache pleinement fonctionnel – mais elle permet d’interroger et de supprimer des entités d’un cache de second niveau de façon standard. Comme EntityManager, javax. persistence.Cache est une interface implémentée par le système de cache du fournisseur de persistance. Listing 4.22 : API de cache public interface Cache { // Teste si le cache contient les données de l’entité indiquée. public boolean contains(Class cls, Object primaryKey); // Supprime du cache les données de l’entité indiquée. public void evict(Class cls, Object primaryKey); // Ôte du cache les données des entités de la classe indiquée. public void evict(Class cls); // Vide le cache. public void evictAll(); }
JPQL Nous venons de voir comment manipuler séparément les entités avec l’API d’EntityManager. Vous savez maintenant comment récupérer une entité à partir de son
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
164
Java EE 6 et GlassFish 3
identifiant, la supprimer, modifier ses attributs, etc. Mais rechercher une entité par son identifiant est assez limité (ne serait-ce que parce qu’il vous faut connaître cet identifiant...) : en pratique, vous aurez plutôt besoin de récupérer une entité en fonction de critères autres que son identifiant (par son nom ou son ISBN, par exemple) ou de récupérer un ensemble d’entités satisfaisant certaines conditions (tous les clients qui habitent en France, par exemple). Cette possibilité est inhérente aux bases de données relationnelles et JPA dispose d’un langage permettant ce type d’interactions : JPQL. JPQL (Java Persistence Query Language) sert à définir des recherches d’entités persistantes indépendamment de la base de données sous-jacente. C’est un langage de requête qui s’inspire de SQL (Structured Query Language), le langage standard pour interroger les bases de données relationnelles. La différence principale est que le résultat d’une requête SQL est un ensemble de lignes et de colonnes (une table) alors que celui d’une requête JPQL est une entité ou une collection d’entités. Sa syntaxe est orientée objet et est donc plus familière aux développeurs ne connaissant que ce type de programmation. Ils peuvent ainsi manipuler un modèle objet en utilisant la notation pointée classique (maClasse.monAttribut, par exemple) et oublier la structure des tables. En coulisse, JPQL utilise un mécanisme de traduction pour transformer une requête JPQL en langage compréhensible par une base de données SQL. La requête s’exécute sur la base de données sous-jacente avec SQL et des appels JDBC, puis les instances d’entités sont initialisées et sont renvoyées à l’application – tout ceci de façon simple et à l’aide d’une syntaxe riche. La requête JPQL la plus simple qui soit sélectionne toutes les instances d’une seule entité : SELECT b FROM Book b
Si vous connaissez SQL, cette instruction devrait vous sembler familière. Au lieu de sélectionner le résultat à partir d’une table, JPQL sélectionne des entités, Book ici. La clause FROM permet également de donner un alias à cette entité : ici, b est un alias de Book. La clause SELECT indique que le type de la requête est l’entité b (Book). L’exécution de cette instruction produira donc une liste de zéros ou plusieurs instances de Book. Pour restreindre le résultat, on utilise la clause WHERE afin d’introduire un critère de recherche :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 165
SELECT b FROM Book b WHERE b.title = "H2G2"
L’alias sert à naviguer dans les attributs de l’entité via l’opérateur point. L’entité Book ayant un attribut persistant nommé title de type String, b.title désigne donc l’attribut title de l’entité Book. L’exécution de cette instruction produira une liste de zéros ou plusieurs instances de Book ayant pour titre H2G2. La requête la plus simple est formée de deux parties obligatoires : les clauses SELECT et FROM. La première définit le format du résultat de la requête tandis que la seconde indique l’entité ou les entités à partir desquelles le résultat sera obtenu. Une requête peut également contenir des clauses WHERE, ORDER BY, GROUP BY et HAVING pour restreindre ou trier le résultat. La syntaxe complète de SELECT est définie dans le Listing 4.23. Il existe également les instructions DELETE et UPDATE, qui permettent respectivement de supprimer et de modifier plusieurs instances d’une classe d’entité. Select
La clause SELECT porte sur une expression qui peut être une entité, un attribut d’entité, une expression constructeur, une fonction agrégat ou toute séquence de ce qui précède. Ces expressions sont les briques de base des requêtes et servent à atteindre les attributs des entités ou à traverser les relations (ou une collection d’entités) via la notation pointée classique. Le Listing 4.23 définit la syntaxe d’une instruction SELECT. Listing 4.23 : Syntaxe de l’instruction SELECT SELECT FROM [WHERE ] [ORDER BY ] [GROUP BY ] [HAVING ]
Une instruction SELECT simple renvoie une entité. Si une entité Customer a un alias c, par exemple, SELECT c renverra une entité ou une liste d’entités. SELECT c FROM Customer c
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
166
Java EE 6 et GlassFish 3
Une clause SELECT peut également renvoyer des attributs. Si l’entité Customer a un attribut firstName, SELECT c.firstName renverra un String ou une collection de String contenant les prénoms. SELECT c.firstName FROM Customer c
Pour obtenir le prénom et le nom d’un client, il suffit de créer une liste contenant les deux attributs correspondants : SELECT c.firstName, c.lastName FROM Customer c
Si l’entité Customer est en relation 1–1 avec Address, c.address désigne l’adresse du client et le résultat de la requête suivante renverra donc non pas une liste de clients mais une liste d’adresses : SELECT c.address FROM Customer c
Les expressions de navigation peuvent être reliées les unes aux autres pour traverser des graphes d’entités complexes. Avec cette technique, nous pouvons construire des expressions comme c.address.country.code afin de désigner le code du pays de l’adresse d’un client. SELECT c.address.country.code FROM Customer c
L’expression SELECT peut contenir un constructeur afin de renvoyer une instance de classe Java initialisée avec le résultat de la requête. Cette classe n’a pas besoin d’être une entité, mais le constructeur doit être pleinement qualifié et correspondre aux attributs. SELECT NEW com.apress.javaee6.CustomerDTO(c.firstName, c.lastName, c.address.street1) FROM Customer c
Le résultat de cette requête sera une liste d’objets CustomerDTO instanciés avec l’opérateur new et initialisés avec le prénom, le nom et la rue des clients. L’exécution des requêtes précédentes renverra soit une valeur unique, soit une collection de zéros ou plusieurs entités (ou attributs) pouvant contenir des doublons. Pour supprimer ces derniers, il faut utiliser l’opérateur DISTINCT : SELECT DISTINCT c FROM Customer c
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 167
SELECT DISTINCT c.firstName FROM Customer c
Le résultat d’une requête peut être le résultat d’une fonction agrégat appliquée à une expression. La clause SELECT peut utiliser les fonctions agrégats AVG, COUNT, MAX, MIN et SUM. En outre, leurs résultats peuvent être regroupés par une clause GROUP BY et filtrés par une clause HAVING. SELECT COUNT(c) FROM Customer c
Les clauses SELECT, WHERE et HAVING peuvent également utiliser des expressions scalaires portant sur des nombres (ABS, SQRT, MOD, SIZE, INDEX), des chaînes (CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH) et des dates (CURRENT_DATE, CURRENT_ TIME, CURRENT_TIMESTAMP). From
La clause FROM d’une requête définit les entités en déclarant des variables d’identification ou alias qui pourront être utilisés dans les autres clauses (SELECT, WHERE, etc.). Sa syntaxe est simplement formée du nom de l’entité et de son alias. Dans l’exemple suivant, l’entité est Customer et l’alias est c : SELECT c FROM Customer c
Where
La clause WHERE d’une requête est formée d’une expression conditionnelle permettant de restreindre le résultat d’une instruction SELECT, UPDATE ou DELETE. Il peut s’agir d’une expression simple ou d’un ensemble d’expressions conditionnelles permettant de filtrer très précisément la requête. La façon la plus simple de restreindre le résultat d’une requête consiste à utiliser un attribut d’une entité. L’instruction suivante, par exemple, sélectionne tous les clients prénommés Vincent : SELECT c FROM Customer c WHERE c.firstName = ’Vincent’
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
168
Java EE 6 et GlassFish 3
Vous pouvez restreindre encore plus les résultats en utilisant les opérateurs logiques AND et OR. L’exemple suivant utilise AND pour sélectionner tous les clients prénommés Vincent qui habitent en France : SELECT c FROM Customer c WHERE c.firstName = ’Vincent’ AND c.address.country = ’France’
La clause WHERE utilise également les opérateurs de comparaison =, >, >=, 18 ORDER BY c.age DESC
Le tri peut utiliser plusieurs expressions. SELECT c FROM Customer c WHERE c.age > 18 ORDER BY c.age DESC, c.address.country ASC
Group By et Having
La clause GROUP BY permet de regrouper des valeurs du résultat en fonction d’un ensemble de propriétés. Les entités sont alors divisées en groupes selon les valeurs
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
170
Java EE 6 et GlassFish 3
de l’expression de la clause GROUP BY. Pour, par exemple, regrouper les clients par pays et les compter, on utilisera la requête suivante : SELECT c.address.country, count(c) FROM Customer c GROUP BY c.address.country
GROUP BY définit les expressions de regroupement (c.address.country) qui serviront à agréger et à compter les résultats. Notez que les expressions qui apparaissent dans la clause GROUP BY doivent également apparaître dans la clause SELECT.
La clause HAVING définit un filtre qui s’appliquera après le regroupement des résultats, un peu comme une seconde clause WHERE qui filtrerait le résultat de GROUP BY. En ajoutant une clause HAVING à la requête précédente, on peut n’obtenir que les pays ayant plus de 100 clients. SELECT c.address.country, count(c) FROM Customer c GROUP BY c.address.country HAVING count(c) > 100
GROUP BY
et HAVING ne peuvent apparaître que dans une clause SELECT.
Suppressions multiples
Nous savons supprimer une entité à l’aide de la méthode EntityManager.remove() et interroger une base de données pour obtenir une liste d’entités correspondant à certains critères. Pour supprimer un ensemble, nous pourrions donc exécuter une requête et parcourir son résultat pour supprimer séparément chaque entité. Bien que ce soit un algorithme tout à fait valide, ses performances seraient désastreuses car il implique trop d’accès à la base. Il existe une meilleure solution : les suppressions multiples. JPQL sait effectuer des suppressions multiples sur les différentes instances d’une classe d’entité précise, ce qui permet de supprimer un grand nombre d’entités en une seule opération. L’instruction DELETE ressemble à l’instruction SELECT car elle peut utiliser une clause WHERE et prendre des paramètres. Elle renvoie le nombre d’entités concernées par l’opération. Sa syntaxe est décrite dans le Listing 4.24. Listing 4.24 : Syntaxe de l’instruction DELETE DELETE FROM [[AS] ] [WHERE ]
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 171
L’instruction suivante, par exemple, supprime tous les clients âgés de moins de 18 ans : DELETE FROM Customer c WHERE c.age < 18
Mises à jour multiples
L’instruction UPDATE permet de modifier toutes les entités répondant aux critères de sa clause WHERE. Sa syntaxe est décrite dans le Listing 4.25. Listing 4.25 : Syntaxe de l’instruction UPDATE UPDATE [[AS] ] SET {, }* [WHERE ]
L’instruction suivante, par exemple, modifie le prénom de tous nos jeunes clients en "trop jeune" : UPDATE Customer c SET c.firstName = ’TROP JEUNE’ WHERE c.age < 18
Requêtes Nous connaissons maintenant la syntaxe de JPQL et savons comment écrire ses instructions à l’aide de différentes clauses (SELECT, FROM, WHERE, etc.) : le problème consiste maintenant à les intégrer dans une application. Pour ce faire, JPA 2.0 permet d’intégrer quatre sortes de requêtes dans le code, chacune correspondant à un besoin différent : ■■
Les requêtes dynamiques. Ce sont les requêtes les plus faciles car il s’agit simplement de chaînes de requêtes JPQL indiquées dynamiquement au moment de l’exécution.
■■
Les requêtes nommées. Ce sont des requêtes statiques et non modifiables.
■■
Les requêtes natives. Elles permettent d’exécuter une instruction SQL native à la place d’une instruction JPQL.
■■
API des critères. Ce nouveau concept a été introduit par JPA 2.0.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
172
Java EE 6 et GlassFish 3
Le choix entre ces quatre types est centralisé au niveau de l’interface EntityManager, qui dispose de plusieurs méthodes fabriques (voir Tableau 4.3) renvoyant toutes une interface Query. Tableau 4.3 : Méthodes d’EntityManager pour créer des requêtes
Méthode
Description
Query createQuery(String jpqlString)
Crée une instance de Query permettant d’exécuter une instruction JPQL pour des requêtes dynamiques.
Query createQuery(QueryDefinition qdef)
Crée une instance de Query permettant d’exécuter une requête par critère.
Query createNamedQuery(String name)
Crée une instance de Query permettant d’exécuter une requête nommée (en JPQL ou en SQL natif).
Query createNativeQuery(String sqlString)
Crée une instance de Query permettant d’exécuter une instruction SQL native.
Query createNativeQuery(String sqlString, Class resultClass)
Crée une instance de Query permettant d’exécuter une instruction SQL native en lui passant la classe du résultat attendu.
Une API complète permet de contrôler l’implémentation de Query obtenue par l’une de ces méthodes. L’API Query, présentée dans le Listing 4.26, est utilisable avec les requêtes statiques (requêtes nommées) et les requêtes dynamiques en JPQL, ainsi qu’avec les requêtes natives en SQL. Cette API permet également de lier des paramètres aux requêtes et de contrôler la pagination. Listing 4.26 : API Query public interface Query { // Exécute une requête et renvoie un résultat. public List getResultList(); public Object getSingleResult(); public int executeUpdate(); // Initialise les paramètres de la requête. public Query setParameter(String name, Object value); public Query setParameter(String name, Date value, TemporalType temporalType); public Query setParameter(String name, Calendar value, TemporalType temporalType);
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 173
public Query setParameter(int position, Object value); public Query setParameter(int position, Date value, TemporalType temporalType); public Query setParameter(int position, Calendar value, TemporalType temporalType); public Map getNamedParameters(); public List getPositionalParameters(); // Restreint le nombre de résultats renvoyés par une requête. public Query setMaxResults(int maxResult); public int getMaxResults(); public Query setFirstResult(int startPosition); public int getFirstResult(); // Fixe et obtient les "hints" d’une requête. public Query setHint(String hintName, Object value); public Map getHints(); public Set getSupportedHints(); // Fixe le mode flush pour l’exécution de la requête. public Query setFlushMode(FlushModeType flushMode); public FlushModeType getFlushMode(); // Fixe le mode de verrouillage utilisé par la requête. public Query setLockMode(LockModeType lockMode); public LockModeType getLockMode(); // Permet d’accéder à l’API spécifique du fournisseur. public T unwrap(Class cls); }
Les méthodes les plus utilisées de cette API sont celles qui exécutent la requête. Ainsi, pour effectuer une requête SELECT, vous devez choisir entre deux méthodes en fonction du résultat que vous voulez obtenir : ■■
La méthode getResultList() exécute la requête et renvoie une liste de résultats (entités, attributs, expressions, etc.).
■■
La méthode getSingleResult() exécute la requête et renvoie un résultat unique.
Pour exécuter une mise à jour ou une suppression, utilisez la méthode executeUpdate(), qui exécute la requête et renvoie le nombre d’entités concernées par son exécution. Comme nous l’avons vu plus haut dans la section "JPQL", une requête peut prendre des paramètres nommés (:monParam, par exemple) ou positionnels (?1, par exemple). L’API Query définit plusieurs méthodes setParameter() pour initialiser ces paramètres avant l’exécution d’une requête.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
174
Java EE 6 et GlassFish 3
Une requête peut renvoyer un grand nombre de résultats. Selon l’application, ceux-ci peuvent être traités tous ensemble ou par morceaux (une application web, par exemple, peut vouloir n’afficher que dix lignes à la fois). Pour contrôler cette pagination, l’interface Query définit les méthodes setFirstResult() et setMaxResults(), qui permettent respectivement d’indiquer le premier résultat que l’on souhaite obtenir (en partant de zéro) et le nombre maximal de résultats par rapport à ce point précis. Le mode flush indique au fournisseur de persistance comment gérer les modifications et les requêtes en attente. Deux modes sont possibles : AUTO et COMMIT. Le premier (qui est également celui par défaut) précise que c’est au fournisseur de s’assurer que les modifications en attente soient visibles par le traitement de la requête. COMMIT est utilisé lorsque l’on souhaite que l’effet des modifications apportées aux entités n’écrase pas les données modifiées dans le contexte de persistance. Les
requêtes
peuvent
être
verrouillées
par
un
appel
à
la
méthode
set LockMode(LockModeType).
Les sections qui suivent décrivent les trois types de requêtes en utilisant quelquesunes des méthodes que nous venons de décrire. Requêtes dynamiques
Les requêtes dynamiques sont définies à la volée par l’application lorsqu’elle en a besoin. Elles sont créées par un appel à la méthode EntityManager.createQuery(), qui prend en paramètre une chaîne représentant une requête JPQL. Dans le code qui suit, la requête JPQL sélectionne tous les clients de la base. Le résultat étant une liste, on utilise la méthode getResultList() pour renvoyer une liste d’entités Customer (List). Si vous savez que la requête ne renverra qu’une seule entité, utilisez plutôt la méthode getSingleResult() car cela vous évitera de devoir ensuite extraire cette entité d’une liste. Query query = em.createQuery("SELECT c FROM Customer c"); List customers = query.getResultList();
La chaîne contenant la requête peut également être élaborée dynamiquement par l’application – en cours d’exécution – à l’aide de l’opérateur de concaténation et en fonction de certains critères. String jpqlQuery = "SELECT c FROM Customer c"; if (someCriteria)
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 175
jpqlQuery += " WHERE c.firstName = ’Vincent’"; query = em.createQuery(jpqlQuery); List customers = query.getResultList();
La requête précédente récupère les clients prénommés Vincent, mais vous voudrez peut-être pouvoir choisir ce prénom et le passer en paramètre : vous pouvez le faire en utilisant des noms ou des positions. Dans l’exemple suivant, on utilise un paramètre nommé :fname (notez le préfixe deux-points) dans la requête et on le lie à une valeur avec la méthode setParameter() : jpqlQuery = "SELECT c FROM Customer c"; if (someCriteria) jpqlQuery += " where c.firstName = :fname"; query = em.createQuery(jpqlQuery); query.setParameter("fname", "Vincent"); List customers = query.getResultList();
Notez que le nom de paramètre fname ne contient pas le symbole deux-points utilisé dans la requête. Le code équivalent avec un paramètre positionnel serait le suivant : jpqlQuery = "SELECT c FROM Customer c"; if (someCriteria) jpqlQuery += " where c.firstName = ?1"; query = em.createQuery(jpqlQuery); query.setParameter(1, "Vincent"); List customers = query.getResultList();
Si vous voulez paginer la liste des clients par groupes de dix, utilisez la méthode setMaxResults() de la façon suivante : Query query = em.createQuery("SELECT c FROM Customer c"); query.setMaxResults(10); List customers = query.getResultList();
Le problème des requêtes dynamiques est le coût de la traduction de la chaîne JPQL en instruction SQL au moment de l’exécution. La requête étant créée à l’exécution, elle ne peut pas être prévue à la compilation : à chaque appel, le fournisseur de persistance doit donc analyser la chaîne JPQL, obtenir les métadonnées de l’ORM et produire la requête SQL correspondante. Ce surcoût de traitement des requêtes dynamiques peut donc être un problème : lorsque cela est possible, utilisez plutôt des requêtes statiques (requêtes nommées). Requêtes nommées
Les requêtes nommées sont différentes des requêtes dynamiques parce qu’elles sont statiques et non modifiables. Bien que cette nature statique n’offre pas la souplesse
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
176
Java EE 6 et GlassFish 3
des requêtes dynamiques, l’exécution des requêtes nommées peut être plus efficace car le fournisseur de persistance peut traduire la chaîne JPQL en SQL au démarrage de l’application au lieu d’être obligé de le faire à chaque fois que la requête est exécutée. Les requêtes nommées sont exprimées dans les métadonnées via une annotation @ NamedQuery ou son équivalent XML. Cette annotation prend deux éléments : le nom de la requête et son contenu. Dans le Listing 4.27, nous modifions l’entité Customer pour définir trois requêtes statiques à l’aide d’annotations. Listing 4.27 : L’entité Customer avec des requêtes nommées @Entity @NamedQueries({ @NamedQuery(name = "findAll", query="select c from Customer c"), @NamedQuery(name = "findVincent", query="select c from Customer c „ where c.firstName = ’Vincent’"), @NamedQuery(name = "findWithParam", query="select c from Customer c where c.firstName = :fname") )} public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private Integer age; private String email; @OneToOne @JoinColumn(name = "address_fk") private Address address; // Constructeurs, getters, setters }
L’entité Customer définissant plusieurs requêtes nommées, nous utilisons l’annotation @NamedQueries, qui prend en paramètre un tableau de @NamedQuery. La première requête, nommée findAll, renvoie toutes les entités Customer de la base, sans aucune restriction (pas de clause WHERE). La requête findWithParam prend quant à elle un paramètre fname pour choisir les clients en fonction de leur prénom. Si l’entité Customer n’avait défini qu’une seule requête, nous aurions simplement utilisé une annotation @NamedQuery, comme dans l’exemple suivant : @Entity @NamedQuery(name = "findAll", query="select c from Customer c") public class Customer { ... }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 177
L’exécution de ces requêtes ressemble à celle des requêtes dynamiques : on appelle la méthode EntityManager.createNamedQuery() en lui passant le nom de la requête tel qu’il est défini dans les annotations. Cette méthode renvoie un objet Query qui peut servir à initialiser les paramètres, le nombre maximal de résultats, le mode de récupération, etc. Pour, par exemple, exécuter la requête findAll, on écrirait le code suivant : Query query = em.createNamedQuery("findAll"); List customers = query.getResultList();
Le fragment de code qui suit appelle la requête findWithParam en lui passant le paramètre fname et en limitant le nombre de résultats à 3 : Query query = em.createNamedQuery("findWithParam"); query.setParameter("fname", "Vincent"); query.setMaxResults(3); List customers = query.getResultList();
La plupart des méthodes de l’API Query renvoyant un objet Query, vous pouvez utiliser un raccourci élégant qui consiste à appeler les méthodes les unes après les autres (setParameter().setMaxResults(), etc.). Query query = em.createNamedQuery("findWithParam"). „ setParameter("fname", "Vincent").setMaxResults(3); List customers = query.getResultList();
Les requêtes nommées permettent d’organiser les définitions de requêtes et améliorent les performances de l’application. Cette organisation vient du fait qu’elles sont définies de façon statique sur les entités et généralement placées sur la classe entité qui correspond directement au résultat de la requête (ici, findAll renvoie des clients et doit donc être définie sur l’entité Customer). Cependant, la portée du nom de la requête est celle de l’unité de persistance et ce nom doit être unique dans cette portée, ce qui signifie qu’il ne peut exister qu’une seule requête findAll : ceci implique donc de nommer différemment cette requête si l’on devait, par exemple, en écrire une autre pour rechercher toutes les adresses. Une pratique courante consiste à préfixer le nom de la requête par celui de l’entité : on aurait ainsi une méthode Customer.findAll pour Customer et Address.findAll pour Address. Un autre problème est que le nom de la requête, qui est une chaîne, est modifiable et que vous risquez donc d’obtenir une exception indiquant que la requête n’existe pas si vous faites une erreur de frappe ou que vous refactorisiez le code. Pour limiter
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
178
Java EE 6 et GlassFish 3
ce risque, vous pouvez remplacer ce nom par une constante. Le Listing 4.28 montre comment refactoriser l’entité Customer. Listing 4.28 : L’entité Customer définit une requête nommée à l’aide d’une constante @Entity @NamedQuery(name = Customer.FIND_ALL, query="select c from Customer c") public class Customer { public static final String FIND_ALL = "Customer.findAll"; // Attributs, constructeurs, getters, setters }
La constante FIND_ALL identifie la requête findAll sans ambiguïté en préfixant son nom du nom de l’entité. C’est cette même constante qui est ensuite utilisée dans l’annotation @NamedQuery et que vous pouvez utiliser pour exécuter la requête : Query query = em.createNamedQuery(Customer.FIND_ALL); List customers = query.getResultList();
Requêtes natives
JPQL dispose d’une syntaxe riche permettant de gérer les entités sous n’importe quelle forme et de façon portable entre les différentes bases de données, mais JPA autorise également l’utilisation des fonctionnalités spécifiques d’un SGBDR via des requêtes natives. Celles-ci prennent en paramètre une instruction SQL (SELECT, UPDATE ou DELETE) et renvoient une instance de Query pour exécuter cette instruction. En revanche, les requêtes natives peuvent ne pas être portables d’une base de données à l’autre. Si le code n’est pas portable, pourquoi alors ne pas utiliser des appels JDBC ? La raison principale d’utiliser des requêtes JPA natives plutôt que des appels JDBC est que le résultat de la requête sera automatiquement converti en entités. Pour, par exemple, récupérer toutes les entités Customer de la base en utilisant SQL, vous devez appeler la méthode EntityManager.createNativeQuery(), qui prend en paramètre la requête SQL et la classe d’entité dans laquelle le résultat sera traduit : Query query = em.createNativeQuery("SELECT * FROM t_customer", Customer.class); List customers = query.getResultList();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 179
Comme vous pouvez le constater, la requête SQL est une chaîne qui peut être créée dynamiquement en cours d’exécution (exactement comme les requêtes JPQL dynamiques). Là aussi la requête pourrait être complexe et, ne la connaissant pas à l’avance, le fournisseur de persistance sera obligé de l’interpréter à chaque fois, ce qui aura des répercussions sur les performances de l’application. Toutefois, comme les requêtes nommées, les requêtes natives peuvent utiliser le mécanisme des annotations pour définir des requêtes SQL statiques. Ici, cette annotation s’appelle @NamedNativeQuery et peut être placée sur n’importe quelle entité (voir Listing 4.29) – comme avec JPQL, le nom de la requête doit être unique dans l’unité de persistance. Listing 4.29 : L’entité Customer définit une requête native nommée @Entity @NamedNativeQuery(name = "findAll", query="select * from t_customer") @Table(name = "t_customer") public class Customer { // Attributs, constructeurs, getters, setters }
Concurrence JPA peut servir à modifier des données persistantes et JPQL permet de récupérer des données répondant à certains critères. L’application qui les utilise peut s’exécuter dans un cluster de plusieurs nœuds, avoir plusieurs threads et une seule base de données : il est donc assez fréquent d’accéder aux entités de façon concurrente. Dans cette situation, l’application doit contrôler la synchronisation des données au moyen d’un mécanisme de verrouillage. Que votre programme soit simple ou complexe, il y a de grandes chances pour que vous soyez obligé d’utiliser des verrous à un endroit ou à un autre de votre code. Pour illustrer le problème de l’accès concurrent à une base de données, prenons l’exemple d’une application comprenant les deux méthodes de la Figure 4.3. L’une des méthodes recherche un livre par son identifiant et augmente son prix de 2 €. L’autre fait la même chose, mais augmente le prix de 5 €. Si les deux méthodes sont exécutées en même temps dans des transactions distinctes et qu’elles manipulent le même livre, vous ne pouvez donc pas prévoir le prix final. Dans notre exemple, son
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
180
Java EE 6 et GlassFish 3
prix initial était de 10 € : selon la transaction qui se termine en dernier, son prix final sera de 12 € ou de 15 €. tx1.begin()
tx2.begin()
// Le prix du livre est de 10€ Book book = em.find(Book.class, 12);
// Le prix du livre est de 10€ Book book = em.find(Book.class, 12);
book.raisePriceByT
book.raisePriceByFi
Euros();
tx1.comit(); // Le prix est maintenant de 12€
Euros();
tx2.comit(); // Le prix est maintenant de 15€
temps
Figure 4.3 Les transactions tx1 et tx2 modifient le prix d’un livre de façon concurrente.
Ce problème de concurrence, où le "gagnant" est celui qui valide la transaction en dernier, n’est pas spécifique à JPA. Cela fait bien longtemps que les SGBD ont dû résoudre ce problème et ont trouvé différentes solutions pour isoler les transactions les unes des autres. Un mécanisme classique consiste à verrouiller la ligne sur laquelle porte l’instruction SQL. JPA 2.0 dispose de deux types de verrouillages (JPA 1.0 ne proposait que le verrouillage optimiste) : ■■
Le verrouillage optimiste. Il repose sur la supposition que la plupart des transactions n’entreront pas en conflit les unes avec les autres, ce qui permet une concurrence aussi permissive que possible.
■■
Le verrouillage pessimiste. Il fait la supposition inverse, ce qui impose d’obtenir un verrou sur la ressource avant de la manipuler.
Prenons un exemple de la vie quotidienne pour illustrer ces concepts : la traversée d’une avenue. Dans une zone à faible trafic, vous pourriez traverser l’avenue sans regarder si des voitures arrivent (traversée optimiste) alors que, dans une zone à fort trafic, il ne faut certainement pas le faire (traversée pessimiste). JPA utilise différents mécanismes de verrouillage en fonction des niveaux de l’API. Les verrous optimistes et pessimistes peuvent être obtenus via les méthodes EntityManager.find() et EntityManager.refresh() (en plus de la méthode lock()), ainsi que par les requêtes JPQL : ceci signifie donc que le verrouillage peut s’effectuer au niveau du gestionnaire d’entités et au niveau Query avec les méthodes énumérées dans les Tableaux 4.4 et 4.5.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 181
Tableau 4.4 : Méthodes d’EntityManager pour verrouiller les entités
Méthode
Description
T find(Class entityClass, Object primaryKey, LockModeType lockMode)
Recherche une entité de la classe avec la clé indiquée et la verrouille selon le type du verrou.
void lock(Object entity, LockModeType lockMode)
Verrouille une instance d’entité contenue dans le contexte de persistance avec le type de verrou indiqué.
Rafraîchit l’état de l’instance à partir de la base de données en écrasant les éventuelles modifications apportées à l’entité et verrouille celle-ci selon le type de verrou indiqué.
Tableau 4.5 : Méthodes de Query pour verrouiller les requêtes JPQL
Méthode
Description
Query setLockMode(LockModeType lockMode)
Fixe le type de verrou utilisé pour l’exécution de la requête.
Toutes ces méthodes attendent un paramètre valeurs suivantes : ■■ OPTIMISTIC. Verrouillage
LockModeType
pouvant prendre les
optimiste.
■■ OPTIMISTIC_FORCE_INCREMENT.
Verrouillage optimiste et incrémentation de la colonne version de l’entité (voir la section "Gestion de version").
■■ PESSIMISTIC_READ. Verrouillage pessimiste sans avoir besoin de relire les données
à la fin de la transaction pour obtenir un verrou. ■■ PESSIMISTIC_WRITE. Verrouillage pessimiste et sérialisation entre les transactions
pour mettre à jour l’entité. ■■ PESSIMISTIC_FORCE_INCREMENT. Verrouillage
pessimiste et incrémentation de la colonne version de l’entité (voir la section "Gestion de version").
■■ NONE. Aucun
mécanisme de verrouillage n’est utilisé.
Vous pouvez utiliser ces paramètres à différents endroits en fonction de vos besoins. Vous pouvez lire puis verrouiller :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
182
Java EE 6 et GlassFish 3
Book book = em.find(Book.class, 12); // Verrouille pour augmenter le prix em.lock(book, LockModeType.PESSIMISTIC); book.raisePriceByTwoEuros();
Ou vous pouvez lire et verrouiller : Book book = em.find(Book.class, 12, LockModeType.PESSIMISTIC); // Le livre est déjà verrouillé : on augmente son prix book.raisePriceByTwoEuros();
La concurrence et le verrouillage sont les motivations essentielles de la gestion des versions. Gestion de version
Java utilise le système des versions : Java SE 5.0, Java SE 6.0, EJB 3.1, JAX-RS 1.0, etc. Lorsqu’une nouvelle version de JAX-RS apparaît, par exemple, son numéro de version est augmenté et vous mettez à jour votre environnement avec JAX-RS 1.1. JPA utilise exactement le même mécanisme lorsque l’on a besoin de versions d’entités. La première fois que vous rendez une entité persistante, elle prend le numéro de version 1. Si, plus tard, vous modifiez un attribut et que vous répercutiez cette modification dans la base de données, le numéro de version de l’entité passe à 2, etc. La version de l’entité évolue à chaque fois qu’elle est modifiée. Pour que ceci fonctionne, l’entité doit posséder un attribut annoté par @Version, lui permettant de stocker son numéro de version. Cet attribut est ensuite traduit par une colonne dans la base de données. Les types autorisés pour les numéros de version sont int, Integer, short, Short, long, Long ou Timestamp. Le Listing 4.30 montre comment ajouter un numéro de version à l’entité Book. Listing 4.30 : L’entité Book avec une annotation @Version @Entity public class Book {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 183
L’entité peut lire la valeur de sa version mais ne peut pas la modifier : seul le fournisseur de persistance peut initialiser ou modifier cette valeur lorsque l’objet est écrit ou modifié dans la base de données. Dans le Listing 4.31, par exemple, on rend une nouvelle entité Book persistante. Lorsque la transaction se termine, le fournisseur de persistance fixe son numéro de version à 1. Puis on modifie le prix du livre et, après l’écriture des données dans la base, le numéro de version est incrémenté et vaut donc 2. Listing 4.31 : Modification du prix d’un livre Book book = new Book("H2G2", 21f, "Best IT book", "123-456", 321, false); tx.begin(); em.persist(book); tx.commit(); assertEquals(1, book.getVersion()); tx.begin(); book.raisePriceByTwoEuros(); tx.commit(); assertEquals(2, book.getVersion());
L’attribut de version n’est pas obligatoire, mais il est conseillé lorsque l’entité est susceptible d’être modifiée en même temps par plusieurs processus ou plusieurs threads. La gestion de version est au cœur du verrouillage optimiste car elle offre une protection pour les modifications concurrentes épisodiques des entités. En fait, une entité est automatiquement gérée par verrouillage optimiste lorsqu’elle utilise l’annotation @Version. Verrouillage optimiste
Comme son nom l’indique, le verrouillage optimiste part du principe que les transactions sur la base de données n’entreront pas en conflit les unes avec les autres. En d’autres termes, on estime qu’il y a de fortes chances pour que la transaction qui modifie une entité soit la seule à modifier cette entité à cet instant. La décision de verrouiller l’entité est donc prise à la fin de la transaction, afin de garantir que les modifications apportées à l’entité seront cohérentes avec l’état courant de la base de données. Les transactions qui violeraient cette contrainte provoqueraient la levée d’une exception OptimisticLockException et seraient annulées.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
184
Java EE 6 et GlassFish 3
Comment lever une OptimisticLockException ? Soit en verrouillant explicitement l’entité (avec les méthodes lock() ou find()), soit en laissant le fournisseur de persistance contrôler l’attribut annoté par @Version. L’utilisation de cette annotation permet au gestionnaire d’entités d’effectuer un verrouillage optimiste simplement en comparant la valeur de l’attribut de version dans l’instance de l’entité avec la valeur de la colonne correspondante dans la base. Sans cette annotation, le gestionnaire d’entités ne peut pas réaliser de verrouillage optimiste. À la Figure 4.4, les transactions tx1 et tx2 obtiennent toutes les deux une instance de la même entité de Book. À ce moment précis, la version de l’entité est 1. La première transaction augmente le prix du livre de 2 € et valide cette modification : lorsque les données sont écrites dans la base, le fournisseur de persistance incrémente le numéro de version, qui passe donc à 2. Si la seconde transaction augmente le prix de 5 € et valide également cette modification, le gestionnaire d’entités de tx2 réalisera que le numéro de version dans la base est différent de celui de l’entité, ce qui signifie que la version a été modifiée par une autre transaction : une exception OptimisticLockException sera alors lancée. tx1.begin();
tx2.begin();
// Le prix du livre est de 10€ Book book = em.find(Book.class, 12); b t sio ) == 1
// Le prix du livre est de 10€ Book book = em.find(Book.class, 12); == 1 k t s
book.raisePriceByTwoEuros(); book.raisePriceByFiveEuros(); tx1.comit(); // Le prix est maintenant de 12€ b sio ( == 2 temps
tx2.comit(); d p
t êt k p
1
ll
ut 2
Figure 4.4 OptimisticLockException est lancée par la transaction tx2.
Le comportement par défaut de l’annotation @Version consiste à lancer l’exception OptimisticLockException lorsque les données sont écrites dans la base (lorsque la transaction est validée ou par un appel explicite à la méthode em.flush()), mais vous pouvez également contrôler l’endroit de placement du verrou optimiste en choisissant une stratégie "lire puis verrouiller" ou "lire et verrouiller". Le code de lire et verrouiller, par exemple, serait de la forme : Book book = em.find(Book.class, 12); // Verrouillage pour augmenter le prix em.lock(book, LockModeType.OPTIMISTIC); book.raisePriceByTwoDollars();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 4
Gestion des objets persistants 185
Avec le verrouillage optimiste, la valeur du paramètre LockModeType peut être OPTIMISTIC ou OPTIMISTIC_FORCE_INCREMENT (ou, respectivement, READ ou WRITE, mais ces valeurs sont dépréciées). La seule différence entre les deux est qu’OPTIMISTIC_ FORCE_INCREMENT forcera une mise à jour (incrémentation) de la colonne contenant la version de l’entité. Il est fortement conseillé d’utiliser le verrouillage optimiste pour toutes les entités auxquelles on est susceptible d’accéder de façon concurrente. Ne pas utiliser de verrou peut provoquer un état incohérent de l’entité, la perte de modifications et d’autres problèmes. Ce type de verrouillage donne de meilleures performances car il décharge la base de ce travail ; c’est une alternative au verrouillage pessimiste, qui, lui, exige un verrouillage de bas niveau de la base de données. Verrouillage pessimiste
Le verrouillage pessimiste part du principe opposé à celui du verrouillage optimiste puisqu’il consiste à verrouiller systématiquement l’entité avant de la manipuler. Ce mécanisme est donc très restrictif et dégrade les performances de façon significative puisqu’il implique que la base pose un verrou avec SELECT ... FOR UPDATE SQL lorsqu’elle lit les données. Généralement, les bases de données offrent un service de verrouillage pessimiste permettant au gestionnaire d’entités de verrouiller une ligne de la table pour empêcher un autre thread de modifier cette même ligne. C’est donc un mécanisme efficace pour garantir que deux clients ne modifieront pas la même ligne en même temps, mais il exige des vérifications de bas niveau qui pénalisent les performances. Les transactions qui violent cette contrainte provoquent la levée d’une exception PessimisticLockException et sont annulées. Le verrouillage optimiste convient bien lorsqu’il y a peu de contention entre les transactions mais, quand cette contention augmente, le verrouillage pessimiste peut se révéler préférable car le verrou sur la base est obtenu immédiatement, alors que les transactions optimistes échouent souvent plus tard. En temps de crise, par exemple, les marchés boursiers reçoivent d’énormes ordres de ventes. Si 100 millions de personnes veulent vendre leurs actions en même temps, le système doit utiliser un verrouillage pessimiste pour assurer la cohérence des données. Notez qu’actuellement le marché est plutôt pessimiste qu’optimiste, mais cela n’a rien à voir avec JPA. Le verrouillage pessimiste peut s’appliquer aux entités qui ne sont pas annotées par @Version.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
186
Java EE 6 et GlassFish 3
Résumé Dans ce chapitre, nous avons vu comment interroger les entités. Le gestionnaire d’entités est la pièce maîtresse de la persistance des entités : il peut créer, modifier, rechercher par identifiant, supprimer et synchroniser les entités avec la base de données en utilisant le contexte de persistance, qui se comporte comme un cache de premier niveau. JPA fournit également JPQL, un langage de requête très puissant et indépendant des SGBDR. Grâce à lui, vous pouvez récupérer les entités à l’aide d’une syntaxe claire disposant de clauses WHERE, ORDER BY ou GROUP BY. Lorsque vous accédez aux entités de façon concurrente, vous savez comment utiliser les numéros de version et quand utiliser le verrouillage optimiste ou le verrouillage pessimiste. Dans le prochain chapitre, nous en apprendrons plus sur le cycle de vie des entités et verrons comment y greffer du code à l’aide de méthodes de rappel ou d’écouteurs.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
5 Méthodes de rappel et écouteurs Au chapitre précédent, nous avons vu comment interroger les entités liées à une base de données. Nous savons maintenant comment rendre une entité persistante, la supprimer, la modifier et la retrouver à partir de son identifiant. Grâce à JPQL, nous pouvons récupérer une ou plusieurs entités en fonction de certains critères de recherche avec des requêtes dynamiques, statiques et natives. Toutes ces opérations sont réalisées par le gestionnaire d’entités – la composante essentielle qui manipule les entités et gère leur cycle de vie. Nous avons décrit ce cycle de vie en écrivant que les entités sont soit gérées par le gestionnaire d’entités (ce qui signifie qu’elles ont une identité de persistance et qu’elles sont synchronisées avec la base de données), soit détachées de la base de données et utilisées comme des POJO classiques. Mais le cycle de vie d’une entité est un peu plus riche. Surtout, JPA permet d’y greffer du code métier lorsque certains événements concernent l’entité : ce code est ensuite automatiquement appelé par le fournisseur de persistance à l’aide de méthodes de rappel. Vous pouvez considérer les méthodes de rappel et les écouteurs comme les triggers d’une base de données relationnelle. Un trigger exécute du code métier pour chaque ligne d’une table alors que les méthodes de rappel et les écouteurs sont appelés sur chaque instance d’une entité en réponse à un événement ou, plus précisément, avant et après la survenue d’un événement. Pour définir ces méthodes "Pre" et "Post", nous pouvons utiliser des annotations ou des descripteurs XML.
Cycle de vie d’une entité Maintenant que nous connaissons la plupart des mystères des entités, intéressonsnous à leur cycle de vie. Lorsqu’une entité est créée ou rendue persistante par le gestionnaire d’entités, celle-ci est dite gérée. Auparavant, elle n’était considérée par
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
188
Java EE 6 et GlassFish 3
la JVM que comme un simple POJO (elle était alors détachée) et pouvait être utilisée par l’application comme un objet normal. Dès qu’une entité devient gérée, le gestionnaire synchronise automatiquement la valeur de ses attributs avec la base de données sous-jacente. Pour mieux comprendre tout ceci, examinez la Figure 5.1, qui représente les états que peut prendre une entité Customer, ainsi que les transitions entre ces états. Figure 5.1 Customer cust = new Customer()
Cycle de vie d’une entité.
Customer cust = em.find() Requête JPQL
Supprimée par le ramasse-miettes
Existe en mémoire em.persist(cust)
em.clear() em.merge(cust) Sérialisée vers une autre couche Détachée
Supprimée de la base de données, mais toujours en mémoire
em.remove(cust) Gérée
Supprimée
em.merge(cust) em.refresh(cust) Modifiée avec les accesseurs Base de données
On crée une instance de l’entité Customer à l’aide de l’opérateur new. Dès lors, cet objet existe en mémoire bien que JPA ne le connaisse pas. Si l’on n’en fait rien, il devient hors de portée et finit par être supprimé par le ramasse-miettes, ce qui marque la fin de son cycle de vie. Nous pouvons aussi le rendre persistant à l’aide de la méthode EntityManager.persist(), auquel cas l’entité devient gérée et son état est synchronisé avec la base de données. Pendant qu’elle est dans cet état, nous pouvons modifier ses attributs en utilisant ses méthodes setters (customer. SetFirstName(), par exemple) ou rafraîchir son contenu par un appel à EntityManager.refresh(). Toutes ces modifications garderont l’entité synchronisée avec la base. Si l’on appelle la méthode EntityManager.contains(customer), celle-ci renverra true car customer appartient au contexte de persistance (il est géré). Un autre moyen de gérer une entité consiste à la charger à partir de la base de données à l’aide de la méthode EntityManager.find() ou d’une requête JPQL récupérant une liste d’entités qui seront alors toutes automatiquement gérées. Dans l’état géré, un appel à la méthode EntityManager.remove() supprime l’entité de la base de données et elle n’est plus gérée. Cependant, l’objet Java continue d’exister en mémoire, et il reste utilisable tant que le ramasse-miettes ne le supprime pas.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 5
Méthodes de rappel et écouteurs 189
Examinons maintenant l’état détaché. Nous avons vu au chapitre précédent qu’un appel explicite à EntityManager.clear() supprimait l’entité du contexte de persistance – elle devient alors détachée. Il y a un autre moyen, plus subtil, de détacher une entité : en la sérialisant. Bien que dans de nombreux exemples de ce livre les entités n’héritent d’aucune classe, elles doivent implémenter l’interface java. io.Serializable pour passer par un réseau afin d’être invoquées à distance ou pour traverser des couches afin d’être affichées dans une couche présentation – cette restriction est due non pas à JPA mais à Java. Une entité qui est sérialisée, qui passe par le réseau et est désérialisée est considérée comme un objet détaché : pour la réattacher, il faut appeler la méthode EntityManager.merge(). Les méthodes de rappel et les écouteurs permettent d’ajouter une logique métier qui s’exécutera lorsque certains événements du cycle de vie d’une entité surviennent, voire à chaque fois qu’un événement intervient dans le cycle de vie d’une entité.
Méthodes de rappel Le cycle de vie d’une entité se décompose en quatre parties : persistance, modification, suppression et chargement, qui correspondent aux opérations équivalentes sur la base de données. Chacune de ces parties est associée à un événement "Pré" et "Post" qui peut être intercepté par le gestionnaire d’entités pour appeler une méthode métier qui doit avoir été marquée par l’une des annotations du Tableau 5.1. Tableau 5.1 : Annotations des méthodes de rappel du cycle de vie
Annotation
Description
@PrePersist
La méthode sera appelée avant l’exécution d’EntityManager.persist().
@PostPersist
La méthode sera appelée après que l’entité sera devenue persistante. Si l’entité produit sa clé primaire (avec @GeneratedValue), sa valeur est accessible dans la méthode.
@PreUpdate
La méthode sera appelée avant une opération de modification de l’entité dans la base de données (appel des setters de l’entité ou de la méthode EntityManager.merge()).
@PostUpdate
La méthode sera appelée après une opération de modification de l’entité dans la base de données.
@PreRemove
La méthode sera appelée avant l’exécution d’EntityManager.remove().
@PostRemove
La méthode sera appelée après la suppression de l’entité.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
190
Java EE 6 et GlassFish 3
Annotation
Description
@PostLoad
La méthode sera appelée après le chargement de l’entité (par une requête JPQL, par un appel à EntityManager.find()) ou avant qu’elle soit rafraîchie à partir de la base de données. Il n’existe pas d’annotation @PreLoad car cela n’aurait aucun sens d’agir sur une entité qui n’a pas encore été construite.
La Figure 5.2 a ajouté ces annotations au diagramme d’états de la Figure 5.1. Figure 5.2 Supprimée par le ramasse-miettes
Cycle de vie d’une entité avec les annotations des méthodes de rappel.
findById ou JPQL
Existe en mémoire
@PostLoad @PrePersist
@PostRemove
@PostPersist @PreRemove Détachée @PostLoad après fusion @PreUpdate et @PostUpdate si l'entité a été modifiée
Gérée
Supprimée
@PreUpdate and @PostUpdate lorsque les accesseurs sont appelés @PostLoad après le rafraîchissement
Avant d’insérer une entité dans la base de données, le gestionnaire d’entités appelle la méthode annotée par @PrePersist. Si l’insertion ne provoque pas d’exception, l’entité est rendue persistante, son identifiant est créé, puis la méthode annotée par @PostPersist est appelée. Il en va de même pour les mises à jour (@PreUpdate, @ PostUpdate) et les suppressions (@PreRemove, @PostRemove). Lorsqu’une entité est chargée à partir de la base de données (via un appel à EntityManager.find() ou une requête JPQL), la méthode annotée par @PostLoad est appelée. Lorsque l’entité détachée a besoin d’être fusionnée, le gestionnaire d’entités doit d’abord vérifier si la version en mémoire est différente de celle de la base (@PostLoad) et modifier les données (@PreUpdate, @PostUpdate) si c’est le cas. Outre les attributs, les constructeurs, les getters et les setters, les entités peuvent contenir du code métier pour valider leur état ou calculer certains de leurs attributs. Comme le montre le Listing 5.1, ce code peut être placé dans des méthodes Java classiques invoquées par d’autres classes ou dans des méthodes de rappel (callbacks). Dans ce dernier cas, c’est le gestionnaire d’entités qui les appellera automatiquement en fonction de l’événement qui a été déclenché.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 5
Méthodes de rappel et écouteurs 191
Listing 5.1 : Entité Customer avec méthodes de rappel @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @Temporal(TemporalType.DATE) private Date dateOfBirth; @Transient private Integer age; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; @PrePersist @PreUpdate private void validate() { if (dateOfBirth.getTime() > new Date().getTime()) throw new IllegalArgumentException("Invalid date of birth"); if (!phoneNumber.startsWith("+")) throw new IllegalArgumentException("Invalid phone number"); } @PostLoad @PostPersist @PostUpdate public void calculateAge() { if (dateOfBirth == null) { age = null; return; } Calendar birth = new GregorianCalendar(); birth.setTime(dateOfBirth); Calendar now = new GregorianCalendar(); now.setTime(new Date()); int adjust = 0; if (now.get(DAY_OF_YEAR) - birth.get(DAY_OF_YEAR) < 0) { adjust = -1; } age = now.get(YEAR) - birth.get(YEAR) + adjust; } // Constructeurs, getters, setters }
Dans le Listing 5.1, l’entité Customer définit une méthode pour valider les données (elle vérifie les valeurs des attributs dateOfBirth et phoneNumber). Cette méthode étant annotée par @PrePersist et @PreUpdate, elle sera appelée avant l’insertion
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
192
Java EE 6 et GlassFish 3
ou la modification des données dans la base. Si ces données ne sont pas valides, la méthode lèvera une exception à l’exécution et l’insertion ou la modification sera annulée : ceci garantit que la base contiendra toujours des données valides. La méthode calculateAge() calcule l’âge du client. L’attribut age est transitoire et n’est donc pas écrit dans la base de données : lorsque l’entité est chargée, rendue persistante ou modifiée, cette méthode calcule l’âge à partir de la date de naissance et initialise l’attribut. Les méthodes de rappel doivent respecter les règles suivantes : ■■
Elles peuvent avoir un accès public, privé, protégé ou paquetage, mais elles ne peuvent pas être statiques ni finales. Dans le Listing 5.1, la méthode validate() est privée.
■■
Elles peuvent être marquées par plusieurs annotations du cycle de vie (la méthode validate() est annotée par @PrePersist et @PreUpdate). Cependant, une annotation de cycle de vie particulière ne peut apparaître qu’une seule fois dans une classe d’entité (il ne peut pas y avoir deux annotations @PrePersist dans la même entité, par exemple).
■■
Elles peuvent lancer des exceptions non contrôlées mais pas d’exceptions contrôlées. Le lancement d’une exception annule la transaction s’il y en a une en cours.
■■
Elles peuvent invoquer JNDI, JDBC, JMS et les EJB, mais aucune opération d’EntityManager ou de Query.
■■
Avec l’héritage, si une méthode est définie dans la superclasse, elle sera appelée avant la méthode de la classe fille. Si, par exemple, la classe Customer du Listing 5.1 héritait d’une classe Person fournissant une méthode @PrePersist, cette dernière serait appelée avant celle de Customer.
■■
Si une relation utilise la répercussion des événements, la méthode de rappel associée sera également appelée en cascade. Si un Customer contient une collection d’adresses et que la suppression d’un Customer soit répercutée sur Address, la suppression d’un client invoquera la méthode @PreRemove d’Address et celle de Customer.
Écouteurs (listeners)
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 5
Méthodes de rappel et écouteurs 193
Les méthodes de rappel d’une entité fonctionnent bien lorsque la logique métier n’est liée qu’à cette entité. Les écouteurs permettent d’extraire cette logique dans une classe séparée qui pourra être partagée par plusieurs entités. En réalité, un écouteur d’entité est simplement un POJO qui définit une ou plusieurs méthodes de rappel du cycle de vie. Pour enregistrer un écouteur, il suffit que l’entité utilise l’annotation @EntityListeners. Par rapport à l’exemple précédent, nous allons extraire les méthodes calculateAge() et validate() pour les placer respectivement dans deux classes écouteurs, AgeCalculationListener (voir Listing 5.2) et DataValidationListener (voir Listing 5.3). Listing 5.2 : Écouteur pour calculer l’âge d’un client public class AgeCalculationListener { @PostLoad @PostPersist @PostUpdate public void calculateAge(Customer customer) { if (customer.getDateOfBirth() == null) { customer.setAge(null); return; }
}
}
Calendar birth = new GregorianCalendar(); birth.setTime(customer.getDateOfBirth()); Calendar now = new GregorianCalendar(); now.setTime(new Date()); int adjust = 0; if (now.get(DAY_OF_YEAR) - birth.get(DAY_OF_YEAR) < 0) { adjust = -1; } customer.setAge(now.get(YEAR) - birth.get(YEAR) + adjust);
Listing 5.3 : Écouteur pour valider les attributs d’un client public class DataValidationListener { @PrePersist @PreUpdate private void validate(Customer customer) { if (dateOfBirth.getTime() > new Date().getTime()) throw new IllegalArgumentException("Invalid date of birth"); if (!phoneNumber.startsWith("+")) throw new IllegalArgumentException("Invalid phone number"); } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
194
Java EE 6 et GlassFish 3
Une classe écouteur ne doit obéir qu’à des règles simples. La première est qu’elle doit avoir un constructeur public sans paramètre. La seconde est que les signatures des méthodes de rappel sont légèrement différentes de celles du Listing 5.1. Lorsqu’elle est appelée sur un écouteur, une méthode de rappel doit en effet avoir accès à l’état de l’entité (le prénom et le nom du client, par exemple) : elle doit donc avoir un paramètre d’un type compatible avec celui de l’entité. Nous avons vu que, lorsqu’elle est définie dans l’entité, une méthode de rappel a la signature suivante, sans paramètre : void ();
Les méthodes de rappel définies dans un écouteur peuvent en revanche avoir deux types de signatures. Si une méthode doit servir à plusieurs entités, elle doit prendre un paramètre de type Object : void (Object uneEntité)
Si elle n’est destinée qu’à une seule entité ou à ses sous-classes, le paramètre peut être celui de l’entité : void (Customer customerOuSousClasses)
Pour indiquer que ces deux écouteurs seront prévenus des événements du cycle de vie de l’entité Customer, celle-ci doit le préciser à l’aide de l’annotation @EntityListeners (voir Listing 5.4). Cette annotation prend en paramètre une classe écouteur ou un tableau d’écouteurs. Lorsqu’il y a plusieurs écouteurs et qu’un événement du cycle de vie survient, le fournisseur de persistance parcourt chacun de ces écouteurs dans l’ordre où ils ont été indiqués et invoquera la méthode de rappel en lui passant une référence à l’entité concernée par l’événement. Puis il appellera les méthodes de rappel de l’entité elle-même (s’il y en a). Listing 5.4 : L’entité Customer définit deux écouteurs @EntityListeners({DataValidationListener.class, AgeCalculationListener.class}) @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @Temporal(TemporalType.DATE)
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 5
Méthodes de rappel et écouteurs 195
private Date dateOfBirth; @Transient private Integer age; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; // Constructeurs, getters, setters }
Ce code produit exactement le même résultat que l’exemple précédent (voir Listing 5.1). L’entité Customer utilise la méthode DataValidationListener.validate() pour valider ses données avant toute insertion ou mise à jour et la méthode AgeCalculationListener.calculateAge() pour calculer son âge. Les règles que doivent respecter les méthodes d’un écouteur sont les mêmes que celles suivies par les méthodes de rappel, mis à part quelques détails : ■■
Elles ne peuvent lancer que des exceptions non contrôlées. Ceci implique que les autres écouteurs et méthodes de rappel ne seront pas appelés et que l’éventuelle transaction sera annulée.
■■
Dans une hiérarchie de classes, si plusieurs entités définissent des écouteurs, ceux de la superclasse seront appelés avant ceux des sous-classes. Si une entité ne veut pas hériter des écouteurs de sa superclasse, elle peut explicitement les exclure à l’aide d’une annotation @ExcludeSuperclassListeners (ou son équivalent XML).
L’entité Customer du Listing 5.4 définissait deux écouteurs, mais il est également possible qu’un écouteur soit défini par plusieurs entités, ce qui peut se révéler utile lorsque l’écouteur fournit une logique générale dont les entités pourront profiter. Le Listing 5.5, par exemple, crée un écouteur de débogage affichant le nom des événements déclenchés. Listing 5.5 : Écouteur de débogage utilisable par n’importe quelle entité public class DebugListener { @PrePersist void prePersist(Object object) { System.out.println("prePersist"); } @PostPersist void postPersist(Object object) { System.out.println("postPersist"); }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Notez que chaque méthode prend un Object en paramètre, ce qui signifie que n’importe quel type d’entité peut utiliser cet écouteur en ajoutant la classe de DebugListener à son annotation @EntityListeners. Cependant, pour que toutes les entités d’une application utilisent cet écouteur, il faudrait ajouter manuellement cette annotation à chacune d’elles : pour éviter cela, JPA permet de définir des écouteurs par défaut qui couvrent toutes les entités d’une unité de persistance. Comme il n’existe pas d’annotation s’appliquant à la portée entière d’une unité de persistance, ces écouteurs par défaut ne peuvent être déclarés que dans un fichier d’association XML. Au Chapitre 3, nous avons vu comment utiliser les fichiers XML à la place des annotations. Il suffit de suivre ici les mêmes étapes pour définir DebugListener comme écouteur par défaut. Pour cela, vous devez créer et déployer avec l’application le fichier XML présenté dans le Listing 5.6. Listing 5.6 : Écouteur de débogage défini comme écouteur par défaut
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 5
Méthodes de rappel et écouteurs 197
Dans ce fichier, le marqueur sert à définir toutes les métadonnées qui n’ont pas d’équivalent avec les annotations. Le marqueur définit toutes les valeurs par défaut de l’unité de persistance et définit l’écouteur par défaut. Ce fichier doit être nommé persistence.xml et être déployé avec l’application. DebugListener sera alors automatiquement appelé par toutes les entités. Si l’on définit une liste d’écouteurs par défaut, chacun d’eux sera appelé dans l’ordre où il apparaît dans le fichier XML. Les écouteurs par défaut sont toujours invoqués avant ceux définis par l’annotation @EntityListeners. Pour qu’ils ne s’appliquent pas à une entité particulière, celle-ci doit le préciser avec l’annotation @Exclude DefaultListeners, comme dans le Listing 5.7. Listing 5.7 : L’entité Customer exclut les écouteurs par défaut @ExcludeDefaultListeners @Entity public class Customer { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String email; private String phoneNumber; @Temporal(TemporalType.DATE) private Date dateOfBirth; @Transient private Integer age; @Temporal(TemporalType.TIMESTAMP) private Date creationDate; // Constructeurs, getters, setters
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
198
Java EE 6 et GlassFish 3
Résumé Ce chapitre a décrit le cycle de vie d’une entité et expliqué comment le gestionnaire d’entités capture les événements pour appeler les méthodes de rappel. Celles-ci peuvent être définies sur une seule entité et marquées par plusieurs annotations (@PrePersist, @PostPersist, etc.). Les méthodes de rappel peuvent également être extraites dans des classes écouteurs pour être utilisées par plusieurs entités, voire toutes (en utilisant des écouteurs par défaut). Avec les méthodes de rappel, nous avons vu que les entités ne sont pas de simples objets anémiques qui ne contiendraient que des attributs, des getters et des setters : elles peuvent contenir une logique métier appelée par d’autres objets de l’application ou invoquée automatiquement par le gestionnaire d’entités au gré du cycle de vie de l’entité. Les autres composants de Java EE 6, comme les EJB, utilisent également ce type d’interception.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
6 Enterprise Java Beans Le chapitre précédent a montré comment implémenter des objets persistants avec JPA et comment les interroger avec JPQL. La couche de persistance utilise des objets qui encapsulent et associent leurs attributs à une base de données relationnelle grâce à des annotations. Le principe consiste à garder les entités aussi transparentes que possible et à ne pas les mélanger avec la logique métier. Les entités peuvent bien sûr posséder des méthodes pour valider leurs attributs, mais elles ne sont pas conçues pour représenter des tâches complexes, qui nécessitent souvent une interaction avec d’autres composants (autres objets persistants, services externes, etc.). La couche de persistance pas plus que l’interface utilisateur ne sont faites pour traiter du code métier, surtout quand il y a plusieurs interfaces (web, Swing, terminaux mobiles, etc.). Pour séparer la couche de persistance de la couche présentation, pour implémenter la logique métier, pour ajouter la gestion des transactions et la sécurité, les applications ont besoin d’une couche métier : avec Java EE, cette couche est implémentée par les EJB (Enterprise Java Beans). La décomposition en couches est importante pour la plupart des applications. En suivant une approche descendante, les chapitres précédents sur JPA ont modélisé les classes de domaine en définissant généralement des noms (Artist, CD, Book, Customer, etc.). Au-dessus de cette couche, la couche métier modélise les actions (ou verbes) de l’application (créer un livre, acheter un livre, afficher une commande, livrer un livre…). Souvent, cette couche interagit avec des services web externes (SOAP ou REST), envoie des messages asynchrones à d’autres systèmes (à l’aide de JMS) ou poste des e-mails ; elle orchestre différents composants allant des bases de données aux systèmes externes, sert de plaque tournante aux transactions et à la sécurité et constitue un point d’entrée pour toutes sortes de clients comme les interfaces web (servlets ou beans gérés par JSF), le traitement par lot ou les systèmes externes.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
200
Java EE 6 et GlassFish 3
Ce chapitre est une introduction aux EJB et les trois chapitres suivants vous donneront toutes les informations nécessaires pour construire la couche métier d’une application d’entreprise. Nous y expliquerons les différents types d’EJB et leurs cycles de vie ; nous décrirons également la notion de programmation orientée aspect (POA), ainsi que la gestion des transactions et de la sécurité.
Introduction aux EJB Les EJB sont des composants côté serveur qui encapsulent la logique métier et la prennent en charge ; ils s’occupent aussi de la sécurité. Les EJB savent également traiter les messages, l’ordonnancement, l’accès distant, les services web (SOAP et REST), l’injection de dépendances, le cycle de vie des composants, la programmation orientée aspect avec intercepteurs, etc. En outre, ils s’intègrent parfaitement avec les autres technologies de Java SE et Java EE – JDBC, JavaMail, JPA, JTA (Java Transaction API), JMS (Java Messaging Service), JAAS (Java Authentication and Authorization Service), JNDI (Java Naming and Directory Interface) et RMI (Remote Method Invocation). C’est la raison pour laquelle on les utilise pour construire les couches métier (voir Figure 6.1) au-dessus de la couche de persistance et comme point d’entrée pour les technologies de la couche présentation, comme JSF (JavaServer Faces). Figure 6.1 Architecture en couches.
Présentation
Logique métier
Persistance
Base de données
Les EJB utilisent un modèle de programmation très puissant qui allie simplicité d’utilisation et robustesse – il réduit la complexité tout en ajoutant la réutilisabilité et
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 201
l’adaptabilité aux applications essentielles pour l’entreprise. C’est sûrement actuellement le modèle de développement Java côté serveur le plus simple et, pourtant, tout ceci est facilement obtenu en annotant un objet Java ordinaire (un POJO) qui sera déployé dans un conteneur. Un conteneur EJB est un environnement d’exécution qui fournit des services comme la gestion des transactions, le contrôle de la concurrence, la gestion des pools et la sécurité, mais les serveurs d’applications lui ont ajouté d’autres fonctionnalités, comme la mise en cluster, la répartition de la charge et la reprise en cas de panne. Les développeurs EJB peuvent désormais se concentrer sur l’implémentation de la logique métier et laisser au conteneur le soin de s’occuper des détails techniques. Avec la version 3.1, les EJB peuvent, plus que jamais, être écrits une bonne fois pour toutes et être déployés sur n’importe quel conteneur respectant la spécification. Les API standard, les noms JNDI portables, les composants légers et la configuration par exception facilitent ce déploiement sur les implémentations open-source ou commerciales. La technologie sous-jacente ayant été créée il y a dix ans, les applications EJB bénéficient d’une base de code stable et de haute qualité, utilisée depuis longtemps par de nombreux environnements. Types d’EJB
Les applications d’entreprise pouvant être complexes, la plate-forme Java EE définit plusieurs types d’EJB. Les Chapitres 6 à 9 ne s’intéresseront qu’aux beans de session et au service timer : les premiers encapsulent la logique métier de haut niveau et forment donc la partie la plus importante de la technologie des EJB. Un bean de session peut avoir les caractéristiques suivantes : ■■
Sans état. Le bean de session ne contient aucun état conversationnel entre les méthodes et n’importe quel client peut utiliser n’importe quel instance.
■■
Avec état. Le bean de session contient l’état conversationnel qui doit être mémorisé entre les méthodes pour un utilisateur donné.
■■
Singleton. Un bean de session unique est partagé par les clients et autorise les accès concurrents.
Le service timer est la réponse standard de Java EE au problème de l’ordonnancement des tâches. Les applications d’entreprise qui dépendent de notifications temporelles l’utilisent pour modéliser les processus métier de type workflow. Les MDB (Message-Driven Beans) reçoivent des messages asynchrones à l’aide de JMS. Bien qu’ils ne fassent pas partie de la spécification EJB, nous les traiterons
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
202
Java EE 6 et GlassFish 3
au Chapitre 13 car ce modèle de composants sert essentiellement à intégrer des systèmes avec MOM (Message-Oriented Middleware). Les MDB délèguent généralement la logique métier aux beans de session. Les EJB peuvent également être utilisés comme points terminaux d’un service web. Les Chapitres 14 et 15 présenteront les services SOAP et REST, qui peuvent être soit de simples POJO déployés dans un conteneur web, soit des beans de session déployés dans un conteneur EJB. INFO Pour des raisons de compatibilité, la spécification EJB 3.1 mentionne encore les beans entités. Ce modèle de composants persistants a été élagué et est susceptible d’être supprimé de Java EE 7. JPA étant la technologie qui a été retenue pour associer et interroger les bases de données, nous ne présenterons pas les beans entités dans ce livre.
Anatomie d’un EJB
Les beans de session encapsulent la logique métier, sont transactionnels et reposent sur un conteneur qui gère un pool, la programmation multithreads, la sécurité, etc. Pour créer un composant aussi puissant, il suffit pourtant d’une seule classe Java et d’une seule annotation. Au chapitre suivant, nous verrons toutefois que les beans de session peuvent être plus complexes : ils peuvent utiliser différents types d’interfaces, d’annotations, de configuration XML et d’appels d’interception. Le Listing 6.1 montre la simplicité avec laquelle un conteneur peut savoir qu’une classe est un bean de session et qu’elle fournit tous les services d’entreprise. Listing 6.1 : Un EJB sans état simple @Stateless public class BookEJB { @PersistenceContext(unitName = "chapter06PU") private EntityManager em; public Book findBookById(Long id) { return em.find(Book.class, id); } public Book createBook(Book book) { em.persist(book); return book; } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 203
Les versions précédentes de J2EE exigeaient des développeurs qu’ils créent plusieurs artéfacts pour obtenir un bean de session : une interface locale ou distante (ou les deux), une interface "home" locale ou distante (ou les deux) et un descripteur de déploiement. Java EE 5 et EJB 3.0 ont considérablement simplifié ce modèle pour ne plus exiger qu’une seule classe et une ou plusieurs interfaces métier. EJB 3.1 va encore plus loin puisqu’il permet à un POJO annoté d’être un bean de session. Comme le montre le code du Listing 6.1, la classe n’implémente aucune interface et n’utilise pas non plus de configuration XML : l’annotation @Stateless suffit à transformer une classe Java en composant transactionnel et sécurisé. Puis, en utilisant le gestionnaire d’entités que nous avons présenté aux chapitres précédents, BookEJB crée et récupère des livres de la base de données de façon simple mais efficace. Nous verrons au chapitre suivant qu’il est également très simple de déclarer un bean à état ou un bean singleton. Cette simplicité s’applique aussi au code client. L’appel d’une méthode de BookEJB ne nécessite qu’une seule annotation, @EJB, pour obtenir une référence à l’aide d’une injection. Cette injection de dépendances permet à un conteneur (client, web ou EJB) d’injecter automatiquement une référence vers un EJB. Dans le Listing 6.2, par exemple, la classe Main obtient une référence à BookEJBRemote en annotant par @ EJB l’attribut statique et privé bookEJB. Si l’EJB est déployé dans un conteneur, Main doit accéder à cet EJB à distance : il suffit d’ajouter une interface distante à l’EJB pour qu’on puisse y accéder à distance. Listing 6.2 : Classe client invoquant l’EJB sans état public class Main { @EJB private static BookEJBRemote bookEJB; public static void main(String[] args) { Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Scifi book created by Douglas Adams"); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); bookEJB.createBook(book); }
Le Listing 6.2 montre l’une des différences qui existent entre une classe Java pure et un bean de session. Ici, la classe Main n’utilise pas le mot-clé new pour créer une instance de BookEJB : elle doit d’abord obtenir une référence à l’EJB – par injection
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
204
Java EE 6 et GlassFish 3
ou par une recherche JNDI – avant d’appeler l’une de ses méthodes. On utilise ce mécanisme parce que l’EJB s’exécute dans un environnement géré – il doit être déployé dans un conteneur (intégré ou non). Comme la plupart des composants de Java EE 6, les EJB ont besoin des métadonnées (exprimées sous forme d’annotation ou de XML) pour informer le conteneur des actions requises (démarcation des transactions) ou des services à injecter. Les EJB peuvent être déployés avec le descripteur de déploiement facultatif ejb-jar. xml, qui, s’il est présent, a priorité sur les annotations. Grâce à la configuration par exception, une simple annotation suffit généralement à transformer un POJO en EJB puisque le conteneur applique le comportement par défaut. Conteneur d’EJB
Comme on l’a mentionné précédemment, un EJB est un composant côté serveur qui doit s’exécuter dans un conteneur. Cet environnement d’exécution fournit les fonctionnalités essentielles, communes à de nombreuses applications d’entreprise : ■■
Communication distante. Sans écrire de code complexe, un client EJB (un autre EJB, une interface utilisateur, un processus non interactif, etc.) peut appeler des méthodes à distance via des protocoles standard.
■■
Injection de dépendances. Le conteneur peut injecter plusieurs ressources dans un EJB (destinations et fabriques JMS, sources de données, autres EJB, variables d’environnement, etc.).
■■
Gestion de l’état. Le conteneur gère l’état des beans à état de façon transparente. Vous pouvez ainsi gérer l’état d’un client particulier, comme si vous développiez une application classique.
■■
Pooling. Le conteneur crée pour les beans sans état et les MDB un pool d’instances qui peut être partagé par plusieurs clients. Une fois qu’il a été invoqué, un EJB n’est pas détruit mais retourne dans le pool pour être réutilisé.
■■
Cycle de vie. Le conteneur prend en charge le cycle de vie de chaque composant.
■■
Messages. Le conteneur permet aux MDB d’écouter les destinations et de consommer les messages sans qu’il soit nécessaire de trop se plonger dans les détails de JMS.
■■
Gestion des transactions. Avec la gestion déclarative des transactions, un EJB peut utiliser des annotations pour informer le conteneur de la politique de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 205
transaction qu’il doit utiliser. C’est le conteneur qui prend en charge la validation ou l’annulation des transactions. ■■
Sécurité. Les EJB peuvent préciser un contrôle d’accès au niveau de la classe ou des méthodes afin d’imposer une authentification de l’utilisateur et l’utilisation de rôles.
■■
Gestion de la concurrence. À part les singletons, tous les autres types d’EJB sont thread-safe par nature. Vous pouvez donc développer des applications parallèles sans vous soucier des problèmes liés aux threads.
■■
Intercepteurs transversaux. Les problèmes transversaux peuvent être placés dans des intercepteurs qui seront automatiquement appelés par le conteneur.
■■
Appels de méthodes asynchrones. Avec EJB 3.1, il est désormais possible d’avoir des appels asynchrones sans utiliser de messages.
Lorsque l’EJB est déployé, le conteneur s’occupe de toutes ces fonctionnalités, ce qui permet au développeur de se concentrer sur la logique métier tout en bénéficiant de ces services sans devoir ajouter le moindre code système. Les EJB sont des objets gérés. Lorsqu’un client appelle un EJB (comme dans le Listing 6.2), il travaille non pas directement avec une instance de cet EJB mais avec un proxy de cette instance. À chaque fois qu’un client invoque une méthode de l’EJB, cet appel est en réalité pris en charge par le proxy. Tout ceci est, bien entendu, transparent pour le client : de sa création à sa destruction, un EJB vit dans un conteneur. Dans une application Java EE, le conteneur EJB interagira généralement avec d’autres conteneurs : le conteneur de servlets (responsable de la gestion de l’exécution des servlets et des pages JSF), le conteneur client d’application (pour la gestion des applications autonomes), le gestionnaire de messages (pour l’envoi, la mise en attente et la réception des messages), le fournisseur de persistance, etc. Ces conteneurs s’exécutent tous dans un serveur d’applications (GlassFish, JBoss, Weblogic, etc.) dont l’implémentation est spécifique mais qui fournit le plus souvent des fonctionnalités de clustering, de montée en charge, de répartition de la charge, de reprise en cas de panne, d’administration, de cache, etc. Conteneur intégré
Dès le moment où ils sont créés, les EJB doivent s’exécuter dans un conteneur qui s’exécute lui-même dans une JVM séparée. Pensez à GlassFish, JBoss, Weblogic, etc.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
206
Java EE 6 et GlassFish 3
et vous vous rappellerez que le serveur d’applications doit d’abord être lancé avant que vous puissiez déployer et utiliser vos EJB. Pour un environnement en production, où le serveur tourne en permanence, c’est tout à fait souhaitable ; par contre, pour un environnement de développement où l’on a souvent besoin de déployer pour déboguer, par exemple, cela prend trop de temps. Un autre problème avec les serveurs qui s’exécutent dans un processus différent est que cela limite les possibilités de tests unitaires car ils ne peuvent s’exécuter simplement sans déployer l’EJB sur un serveur. Pour résoudre ces problèmes, certaines implémentations de serveurs d’applications étaient fournies avec des conteneurs intégrés, mais ceux-ci leur étaient spécifiques. Désormais, EJB 3.1 contient la spécification d’un conteneur intégré, ce qui assure la portabilité entre les différents serveurs. Le principe d’un conteneur intégré est de pouvoir exécuter des applications EJB dans un environnement Java SE afin de permettre aux clients de s’exécuter dans la même JVM. Ceci permet notamment de faciliter les tests et l’utilisation des EJB dans les applications classiques. L’API du conteneur intégré (définie dans javax. ejb.embeddable) fournit le même environnement géré que le conteneur d’exécution de Java EE et inclut les mêmes services : injection, accès à l’environnement d’un composant, gestion des transactions, etc. L’extrait de code suivant montre comment créer une instance d’un conteneur intégré, obtenir un contexte JNDI, rechercher un EJB et appeler l’une de ses méthodes : EJBContainer ec = EJBContainer.createEJBContainer(); Context ctx = ec.getContext(); BookEJB bookEJB = (BookEJB) ctx.lookup("java:global/BookEJB"); bookEJB.createBook(book);
Dans le prochain chapitre, nous verrons comment utiliser l’API de "bootstrap" pour lancer le conteneur et exécuter les EJB. Injection de dépendances et JNDI
Les EJB utilisent l’injection de dépendances pour accéder à différents types de ressources (autres EJB, destinations JMS, ressources d’environnement, etc.). Dans ce modèle, le conteneur pousse les données dans le bean. Comme le montre le Listing 6.2, un client s’injecte une dépendance à un EJB à l’aide de l’annotation @EJB : @EJB private static BookEJB bookEJB;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 207
L’injection a lieu lors du déploiement. Si les données risquent de ne pas être utilisées, le bean peut éviter le coût de l’injection en effectuant à la place une recherche JNDI. En ce cas, le code ne prend les données que s’il en a besoin au lieu d’accepter des données qui lui sont transmises et dont il n’aura peut-être pas besoin. JNDI est une API permettant d’accéder à différents types de services d’annuaires, elle permet au client de lier et de rechercher des objets par nom. JNDI est définie dans Java SE et est indépendante de l’implémentation sous-jacente, ce qui signifie que les objets peuvent être recherchés dans un annuaire LDAP (Lightweight Directory Access Protocol) ou dans un DNS (Domain Name System) à l’aide d’une API standard. L’alternative au code précédent consiste donc à utiliser un contexte JNDI et à y rechercher un EJB déployé portant le nom java:global/chapter06/BookEJB : Context ctx = new InitialContext(); BookEJB bookEJB = (BookEJB)ctx.lookup("java:global/chapter06/BookEJB");
JNDI existe depuis longtemps mais, bien que son API fût standardisée et portable entre les serveurs d’applications, ce n’était pas le cas des noms JNDI, qui restaient spécifiques aux plates-formes. Lorsqu’un EJB était déployé dans GlassFish ou JBoss, son nom dans le service d’annuaire était différent et donc non portable : un client devait rechercher un EJB avec un certain nom sous GlassFish et un autre sous JBoss... EJB 3.1 a standardisé les noms JNDI afin qu’ils soient désormais portables. Dans l’exemple précédent, le nom java:global/chapter06/BookEJB respecte cette nouvelle convention de nommage : java:global[/]// „ [!]
Le chapitre suivant montrera comment utiliser ce nom pour rechercher des EJB. Méthodes de rappel et intercepteurs
Le cycle de vie de tous les types d’EJB (sans et avec état, singleton et MDB) est géré par le conteneur. Un EJB peut ainsi avoir des méthodes annotées (@PostConstruct, @ PreDestroy, etc.) ressemblant aux méthodes de rappel utilisées par les entités et qui seront automatiquement appelées par le conteneur au cours des différentes étapes de son cycle de vie. Ces méthodes peuvent initialiser l’état du bean, rechercher des ressources avec JNDI ou libérer les connexions aux bases de données.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
208
Java EE 6 et GlassFish 3
Pour les problèmes transversaux, les développeurs peuvent utiliser des intercepteurs qui reposent sur le modèle de la programmation par aspect, dans lequel l’appel d’une méthode est automatiquement enrichi de fonctionnalités supplémentaires. Les cycles de vie des EJB, les méthodes de rappel et les intercepteurs seront étudiés au Chapitre 8 (le Chapitre 5 a présenté le cycle de vie des entités). Assemblage
Comme la plupart des composants Java EE (servlets, pages JSF, services web, etc.), les EJB doivent être assemblés avant d’être déployés dans un conteneur d’exécution. Dans la même archive, on trouve généralement la classe bean métier, ses interfaces, intercepteurs, les éventuelles superclasses ou superinterfaces, les exceptions, les classes utilitaires et, éventuellement, un descripteur de déploiement (ejb-jar.xml). Lorsque tous ces artéfacts sont assemblés dans un fichier jar, on peut les déployer directement dans un conteneur. Une autre possibilité consiste à intégrer le fichier jar dans un fichier ear (entreprise archive) et à déployer ce dernier. Un fichier ear sert à assembler un ou plusieurs modules (des EJB ou des applications web) en une archive unique afin que leur déploiement sur un serveur d’applications soit simultané et cohérent. Comme le montre la Figure 6.2, pour déployer une application web on peut assembler les EJB et les entités dans des fichiers jar séparés, les servlets dans un fichier war et tout regrouper dans un fichier ear. Il suffit ensuite de déployer ce fichier sur le serveur d’applications pour pouvoir manipuler les entités à partir de la servlet en utilisant les EJB. Figure 6.2 Assemblage des EJB.
BookApplication.war session/BookEJB.class entity/Book.class servlet.BookServlet META INF/ejb jar.xml META INF/persistence.xml WEB INF/web.xml
Depuis EJB 3.1, les EJB peuvent également être assemblés directement dans un module web (un fichier war). À droite de la Figure 6.2, la servlet, l’EJB et l’entité sont tous assemblés dans le même fichier war, avec tous les descripteurs de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 209
déploiement. Vous remarquerez que le descripteur de déploiement est stocké dans META-INF/ejb-jar.xml dans le module EJB et dans WEB-INF/web-jar.xml dans le module web.
Tour d’horizon de la spécification EJB La spécification EJB 1.0 remonte à 1998 et EJB 3.1 est apparue en 2009 avec Java EE 6. Au cours de ces dix années, la spécification a beaucoup évolué tout en conservant ses bases solides. Des composants lourds aux POJO annotés en passant par les beans entités et par JPA, les EJB se sont réinventés afin de mieux correspondre aux besoins des développeurs et des architectures modernes. Plus que jamais, la spécification EJB 3.1 permet d’éviter la dépendance vis-à-vis des éditeurs en fournissant des fonctionnalités qui, auparavant, n’étaient pas standard (les noms JNDI ou les conteneurs intégrés, par exemple). Elle est donc bien plus portable que par le passé. Historique
Peu après la création du langage Java, l’industrie a ressenti le besoin de disposer d’une technologie permettant de satisfaire les besoins des applications à grande échelle et qui intégrerait RMI et JTA. L’idée d’un framework de composants métier distribué et transactionnel fit donc son chemin et, en réponse, IBM commença à développer ce qui allait ensuite devenir EJB. EJB 1.0 reconnaissait les beans de session avec et sans état et disposait d’un support optionnel des beans entités. Le modèle de programmation utilisait des interfaces "home" et distantes en plus du bean session lui-même ; les EJB étaient accessibles via une interface qui offrait un accès distant avec des paramètres passés par valeur. EJB 1.1 ajouta le support des beans entités et introduisit les descripteurs de déploiement XML pour stocker les métadonnées (qui étaient ensuite sérialisées en binaire dans un fichier). Cette version gérait mieux l’assemblage et le déploiement des applications grâce à l’introduction des rôles. En 2001, EJB 2.0 fut la première version à être standardisée par le JCP (sous le nom de JSR 19). Elle résolvait le problème du surcoût du passage des paramètres par valeur en introduisant les interfaces locales. Les clients s’exécutant dans le conteneur accédaient aux EJB par leur interface locale (en utilisant des paramètres passés
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
210
Java EE 6 et GlassFish 3
par référence) et ceux qui s’exécutaient dans un autre conteneur utilisaient l’interface distante. Cette version a également introduit les MDB, et les beans entités ont reçu le support des relations et d’un langage de requêtes (EJB QL). Deux ans plus tard, EJB 2.1 (JSR 153) a ajouté le support des services web, permettant ainsi aux beans de session d’être invoqués par SOAP/HTTP. Un service timer fut également créé pour pouvoir appeler les EJB à des instants précis ou à des intervalles donnés. Trois ans se sont écoulés entre EJB 2.1 et EJB 3.0, ce qui a permis au groupe d’experts de remodéliser entièrement la conception. En 2006, la spécification EJB 3.0 (JSR 220) amorça une rupture avec les versions précédentes en s’attachant à la simplicité d’utilisation grâce à des EJB ressemblant plus à des POJO. Les beans entités furent remplacés par une toute nouvelle spécification (JPA) et les beans de session n’eurent plus besoin d’interfaces "home" ou spécifiques. L’injection des dépendances, les intercepteurs et les méthodes de rappel du cycle de vie firent leur apparition. En 2009, la spécification EJB 3.1 (JSR 318) fut intégrée à Java EE 6 ; elle poursuit dans la voie de la version précédente en simplifiant encore le modèle de programmation et en lui ajoutant de nouvelles fonctionnalités. Nouveautés d’EJB 3.1
La spécification EJB 3.1 (JSR 318) a apporté plusieurs modifications : JPA ne fait désormais plus partie de la spécification EJB et évolue dans une JSR distincte (JSR 317). La spécification est maintenant organisée en deux documents différents : ■■
"EJB Core Contracts and Requirements" est le document principal qui spécifie les EJB.
■■
"Interceptor Requirements" est le document qui spécifie les intercepteurs.
Il faut garder à l’esprit que la spécification doit supporter le modèle de composant EJB 2.x, ce qui signifie que ses 600 pages doivent tenir compte des interfaces "home", des beans entités, d’EJB QL, etc. Pour simplifier l’adoption future de la spécification, le groupe d’experts Java EE 6 a rassemblé une liste de fonctionnalités éventuellement amenées à disparaître : aucune n’a été supprimée d’EJB 3.1, mais la prochaine version en retiendra et en supprimera certaines : ■■
beans entités 2.x ;
■■
vue cliente d’un bean entité 2.x ;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 211
■■
EJB QL (langage de requête pour la persistance gérée par un conteneur) ;
■■
services web JAX-RPC ;
■■
vue cliente d’un service web JAX-RPC.
La spécification EJB 3.1 ajoute les fonctionnalités et les simplifications suivantes : ■■
Vue sans interface. On peut accéder aux beans de session avec une vue locale sans passer par une interface métier locale.
■■
Déploiement war. Il est désormais possible d’assembler et de déployer directement les composants EJB dans un fichier war.
■■
Conteneur intégré. Une nouvelle API embeddable permet d’exécuter les composants EJB dans un environnement Java SE (pour les tests unitaires, les traitements non interactifs, etc.).
■■
Singleton. Ce nouveau type de composant facilite l’accès à l’état partagé.
■■
Service timer plus élaboré. Cette fonctionnalité permet de créer automatiquement des expressions temporelles.
■■
Asynchronisme. Les appels asynchrones sont désormais possibles sans MDB.
■■
EJB Lite. Définit un sous-ensemble de fonctionnalités utilisables dans les profils Java EE (le profil web, par exemple).
■■
Noms JNDI portables. La syntaxe de recherche des composants EJB est désormais standard.
EJB Lite
Les Enterprise Java Beans sont le modèle de composant prédominant de Java EE 6 car c’est la méthode la plus simple pour effectuer des traitements métiers transactionnels et sécurisés. Cependant, EJB 3.1 continue de définir les beans entités, les interfaces "home", EJB QL, etc., ce qui signifie qu’un nouvel éditeur qui implémenterait la spécification EJB 3.1 devrait également implémenter les beans entités. Les développeurs débutant avec les EJB seraient donc submergés par de nombreuses technologies dont ils n’ont finalement pas besoin. Pour toutes ces raisons, la spécification définit EJB Lite, un sous-ensemble minimal de l’API EJB. Ce sous-ensemble comprend un choix réduit mais efficace des fonctionnalités des EJB adaptées à l’écriture d’une logique métier portable, transactionnelle et
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
212
Java EE 6 et GlassFish 3
sécurisée. Toute application EJB Lite peut être déployée sur n’importe quel produit JAVA EE implémentant EJB 3.1. Le Tableau 6.1 énumère ses composantes. Tableau 6.1 : Comparaison entre EJB Lite et EJB complète
Fonctionnalité
EJB Lite
EJB 3.1 complète
Beans de session beans (avec et sans état, singleton)
Oui
Oui
MDB
Non
Oui
Beans entités 1.x/2.x
Non
Oui (élagable)
Vue sans interface
Oui
Oui
Interface locale
Oui
Oui
Interface distante
Non
Oui
Interfaces 2.x
Non
Oui (élagable)
Services web JAX-WS
Non
Oui
Services web JAX-RS
Non
Oui
Services web JAX-RPC
Non
Oui (élagable)
Timer service
Non
Oui
Asynchronous calls
Non
Oui
Interceptors
Oui
Oui
Interopérabilité RMI/IIOP
Non
Oui
Support des transactions
Oui
Oui
Sécurité
Oui
Oui
API Embeddable
Oui
Oui
Implémentation de référence GlassFish est un projet de serveur d’applications open-source conduit par Sun Microsystems pour la plate-forme Java EE. Lancé en 2005, il est devenu l’implémentation de référence de Java EE 5 en 2006. Aujourd’hui, GlassFish v3 est l’implémentation de référence d’EJB 3.1. Ce produit est construit de façon modulaire (il repose sur le runtime OSGi Felix d’Apache), ce qui lui permet de démarrer très rapidement, et il utilise différents conteneurs d’’applications (Java EE 6, bien sûr, mais également Ruby, PHP, etc.).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 213
Dans ce livre, nous utiliserons GlassFish comme serveur d’applications pour déployer et exécuter les EJB, les pages JSF, les services web SOAP et REST et les MDB JMS.
Récapitulatif Dans la section "Récapitulatif" du Chapitre 2, nous avons vu le développement complet d’une entité Book (présentée dans le Listing 2.3) qui était associée à une base de données Derby. Puis le Listing 2.4 a présenté une classe Main utilisant le gestionnaire d’entités pour rendre un livre persistant et récupérer tous les livres de la base (en utilisant des démarcations explicites de transactions tx.begin() et tx.commit()). Nous allons ici reprendre ce cas d’utilisation, mais en remplaçant la classe Main du Chapitre 2 par un bean de session sans état (BookEJB). Par nature, les EJB sont transactionnels : BookEJB prendra donc en charge les opérations CRUD (Create, Read, Update, Delete) de l’entité Book. BookEJB et Book seront ensuite assemblés et déployés dans GlassFish. L’EJB a besoin d’une interface distante car une application cliente externe (la classe Main) appellera à distance les méthodes de l’EJB (voir Figure 6.3) en se servant d’un conteneur client d’application. Figure 6.3 Récapitulatif.
Main +main(args : String []) : void
BookEJBRemote +findBooks() : List +findBookByld(id : Long) : Book +createBook(book : Book) : Book +deleteBook(book : Book) : void +updateBook(book : Book) : Book
BookEJB em : EntityManager +findBooks() : List +findBookByld(id : Long) : Book +createBook(book : Book) : Book +deleteBook(book : Book) : void +updateBook(book : Book) : Book
Book id : Long jdbc/chapter06DS title : String chapter06DB price : Float description : String isbn : String nbOfPage : Integer illustrations : Boolean
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
214
Java EE 6 et GlassFish 3
Pour utiliser les transactions, le bean de session sans état doit accéder à la base en passant par une source de données (jdbc/chapter06DS) qui devra être créée dans GlassFish et être liée à la base chapter06DB. La structure de répertoire du projet respecte les conventions de Maven ; les classes et les fichiers seront donc placés dans les répertoires suivants : ■■ src/main/java,
pour l’entité
Book, BookEJB,
l’interface
BookEJBRemote
et la
classe Main ; ■■ src/main/resources :,
pour le fichier persistence.xml, qui contient l’unité de persistance pour le SGBDR Derby ;
■■ src/test/java,
pour la classe des tests unitaires BookTest ;
■■ src/test/resources,
pour le fichier persistence.xml, utilisé pour la base de données intégrée Derby servant aux cas de tests ;
■■ pom.xml, le fichier Maven décrivant le projet et ses dépendances vis-à-vis d’autres
modules et composants externes. L’entité Book
Le Listing 6.3 décrivant la même entité Book que celle du Chapitre 2 (voir Listing 2.3), nous n’y reviendrons pas. Notez toutefois que son fichier doit se trouver dans le répertoire src/main/java. Listing 6.3 : Entité Book avec une requête nommée @Entity @NamedQuery(name = "findAllBooks", query = "SELECT b FROM Book b") public class Book { @Id @GeneratedValue private Long id; @Column(nullable = false) private String title; private Float price; @Column(length = 2000) private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 215
Le bean de session sans état BookEJB
est un bean de session sans état qui sert de façade et gère les opérations CRUD de l’entité Book. Le Listing 6.4 montre que cette classe doit être annotée par @ javax.ejb.Stateless et qu’elle implémente l’interface BookEJBRemote décrite dans le Listing 6.5. Par injection de dépendances, l’EJB obtient une référence à un gestionnaire d’entités qui est ensuite employé pour chacune des méthodes suivantes :
BookEJB
■■ findBooks utilise la requête nommée findAllBooks définie dans l’entité Book pour
récupérer toutes les instances de Book dans la base de données. ■■ findBookById invoque EntityManager.find() pour retrouver un livre dans la base
à partir de son identifiant. ■■ createBook
rend persistante l’instance de Book qui lui est passée en paramètre.
utilise la méthode merge() pour attacher au gestionnaire d’entités l’objet Book détaché qui lui est passé en paramètre. Cet objet est alors synchronisé avec la base de données.
■■ updateBook
est une méthode qui réattache l’objet qui lui est passé en paramètre au gestionnaire d’entités, puis le supprime.
■■ deleteBook
Listing 6.4 : Bean de session sans état servant de façade aux opérations CRUD @Stateless public class BookEJB implements BookEJBRemote { @PersistenceContext(unitName = "chapter06PU") private EntityManager em; public List findBooks() { Query query = em.createNamedQuery("findAllBooks"); return query.getResultList(); } public Book findBookById(Long id) { return em.find(Book.class, id); } public Book createBook(Book book) { em.persist(book); return book; } public void deleteBook(Book book) { em.remove(em.merge(book));
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
216
Java EE 6 et GlassFish 3
} public Book updateBook(Book book) { return em.merge(book); } }
Les principales différences entre la classe Main du Chapitre 2 (voir Listing 2.4) et celle du Listing 6.4 est qu’une instance d’EntityManager est directement injectée dans le bean de session : on n’utilise plus une EntityManagerFactory pour créer le gestionnaire. Le conteneur EJB gérant le cycle de vie de l’EntityManager, il en injecte une instance puis la ferme lorsque l’EJB est supprimé. En outre, les appels JPA ne sont plus encadrés par tx.begin() et tx.commit() car les méthodes des beans de session sont implicitement transactionnelles. Ce comportement par défaut sera décrit au Chapitre 9. Le BookEJB doit implémenter une interface distante puisqu’il est invoqué à distance par la classe Main. Comme le montre le Listing 6.5, la seule différence entre une interface Java normale et une interface distante est la présence de l’annotation @Remote. Listing 6.5 : Interface distante @Remote public interface BookEJBRemote { public public public public public
List findBooks(); Book findBookById(Long id); Book createBook(Book book); void deleteBook(Book book); Book updateBook(Book book);
}
Unité de persistance pour le BookEJB
Au Chapitre 2, les transactions étaient gérées par l’application (transaction-type ="RESOURCE_LOCAL") et l’unité de persistance (voir Listing 2.5) devait donc définir le pilote et l’URL JDBC, ainsi que l’utilisateur et son mot de passe afin d’établir une connexion à la base de données Derby. Dans un environnement géré par un conteneur comme celui des EJB, les transactions sont en revanche gérées par le conteneur, non par l’application ; c’est la raison pour laquelle le type de transaction de l’unité de persistance doit valoir JTA (voir Listing 6.6).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 217
Listing 6.6 : Unité de persistance utilisant la source de données chapter06DS org.eclipse.persistence.jpa.PersistenceProvider jdbc/chapter06DS com.apress.javaee6.chapter06.Book
Dans le Listing 6.4, on injecte dans le BookEJB une référence à un EntityManager associé à l’unité de persistance chapter06PU. Celle-ci (définie dans le Listing 6.6) doit définir la source de données à laquelle se connecter (jdbc/chapter06DS) sans préciser d’autres informations d’accès (URL, pilote JDBC, etc.) car elles sont contenues dans la source de données qui sera créée plus tard dans GlassFish. La classe Main
La classe Main (voir Listing 6.7) déclare une instance de l’interface BookEJBRemote et la décore avec l’annotation @EJB pour qu’une référence puisse être injectée – n’oubliez pas que cette classe Main est exécutée dans le conteneur client d’application et que l’injection est donc possible. La méthode main() commence par créer une nouvelle instance de Book, initialise ses attributs et utilise la méthode createBook() de l’EJB pour la rendre persistante. Puis elle modifie le titre, met à jour le livre dans la base et le supprime. Ce code n’ayant pas de contexte de persistance, l’entité Book est un objet détaché manipulé comme une classe Java normale, sans intervention de JPA. C’est l’EJB qui détient le contexte de persistance et utilise le gestionnaire d’entités pour accéder à la base de données. Listing 6.7 : Classe Main utilisant le BookEJB public class Main { @EJB private static BookEJBRemote bookEJB; public static void main(String[] args) { Book book = new Book();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
218
Java EE 6 et GlassFish 3
book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Scifi book created by Douglas Adams"); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); bookEJB.createBook(book); book.setTitle("H2G2"); bookEJB.updateBook(book); bookEJB.deleteBook(book); }
Compilation et assemblage avec Maven
Nous pouvons maintenant utiliser Maven pour compiler l’entité Book, le BookEJB, l’interface BookEJBRemote et la classe Main, puis assembler le résultat dans un fichier jar avec l’unité de persistance. Maven utilise un fichier pom.xml (voir Listing 6.8) pour décrire le projet et les dépendances externes. Ici, on a besoin de l’API JPA (javax.persistence) et de l’API EJB (javax.ejb). Les classes seront compilées et assemblées (jar) dans un fichier jar nommé chapter06- 1.0.jar. Comme le montre le Listing 6.8, l’élément maven-compiler-plugin indique à Maven que l’on utilise Java SE 6. Listing 6.8 : Fichier pom.xml utilisé par Maven pour construire l’application 4.0.0 com.apress.javaee6 chapter06 jar 1.0 chapter06 org.eclipse.persistence javax.persistence 1.1.0 org.glassfish javax.ejb 3.0
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
le fichier jar produit contiendra un fichier permettant d’ajouter des métadonnées à la structure même du jar. Pour que le jar soit exécutable, on ajoute la classe Main à l’élément Main-Class. META-INF\MANIFEST.MF
Vous remarquerez que ce code contient la dépendance glassfish-embedded-all, qui est utilisée par la classe de test (test) pour invoquer le conteneur intégré et lancer l’EJB. Pour compiler et assembler les classes, tapez la commande suivante dans une fenêtre de commandes : mvn package
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
220
Java EE 6 et GlassFish 3
Le message BUILD SUCCESSFUL devrait s’afficher pour vous informer du succès de l’opération. Si vous vérifiez le contenu du répertoire target, vous constaterez que Maven a créé le fichier chapter06-1.0.jar. Déploiement sur GlassFish
Maintenant que le bean de session BookEJB a été assemblé dans une archive jar, nous pouvons le déployer sur le serveur d’applications GlassFish (après nous être assurés que GlassFish et Derby s’exécutent). La source de données jdbc/chapter06DS nécessaire à l’unité de persistance doit être créée à l’aide de la console d’administration de GlassFish ou à partir de la ligne de commandes, l’utilisation de cette dernière étant la plus rapide et la plus simple à reproduire. Avant de créer une source de données, nous avons besoin d’un pool de connexions. GlassFish définit un ensemble de pools prêts à l’emploi, mais nous pouvons créer le nôtre à l’aide de la commande suivante : asadmin create-jdbc-connection-pool „ --datasourceclassname=org.apache.derby.jdbc.ClientDataSource „ --restype=javax.sql.DataSource „ --property portNumber=1527:password=APP:user=APP: „ serverName=localhost:databaseName=chapter06DB: „ connectionAttributes=;create\=true Chapter06Pool
Cette commande crée le pool Chapter06Pool en utilisant une source de données Derby et un ensemble de propriétés définissant la connexion à la base : son nom (chapter06DB), le serveur (localhost), le port (1527), un utilisateur (APP) et un mot de passe (APP). Si l’on teste maintenant cette source de données, Derby créera automatiquement la base (car l’on a précisé connectionAttributes=;create\ =true). La commande suivante permet de tester la source de données : asadmin ping-connection-pool Chapter06Pool
Après l’exécution de cette commande, le répertoire chapter06DB devrait apparaître sur le disque dur à l’endroit où Derby stocke les données. La base et le pool de connexions étant créés, nous devons maintenant déclarer la source de données jdbc/ chapter06DS et la lier à ce pool : asadmin create-jdbc-resource --connectionpoolid Chapter06Pool „ jdbc/chapter06DS
La commande suivante énumère toutes les sources de données hébergées par GlassFish : asadmin list-jdbc-resources
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 221
L’utilitaire asadmin permet de déployer l’application sur GlassFish. Après exécution, la commande suivante affichera un message nous informant du résultat du déploiement : asadmin deploy --force=true target\chapter06-1.0.jar
Maintenant que l’EJB est déployé sur GlassFish avec l’entité et l’unité de persistance, que Derby s’exécute et que la source de données a été créée, il est temps de lancer la classe Main. Exécution de la classe Main avec Derby
La classe Main (voir Listing 6.7) est une application autonome qui s’exécute à l’extérieur du conteneur GlassFish, mais elle utilise l’annotation @EJB, qui a besoin d’un conteneur pour injecter une référence à l’interface BookEJBRemote. La classe Main doit s’exécuter dans un conteneur client d’application (ACC) ; nous aurions pu utiliser une recherche JNDI au lieu d’un ACC, mais ce dernier peut comprendre un fichier jar pour lui donner accès aux ressources du serveur d’applications. Pour exécuter l’ACC, il suffit d’utiliser le programme appclient fourni avec GlassFish en lui passant le fichier jar en paramètre : appclient -client chapter06-1.0.jar
N’oubliez pas que le fichier chapter06-1.0.jar est exécutable puisque nous avons ajouté un élément Main-Class au fichier MANIFEST.MF. Avec la commande précédente, l’ACC exécute la classe Main et injecte une référence à l’interface BookEJB Remote, qui, à son tour, crée, modifie et supprime l’entité Book. La classe BookEJBTest
Pour les équipes de développement modernes, la classe Main ne suffit pas – il faut appliquer des tests unitaires aux classes. Avec la version précédente des EJB, tester unitairement BookEJB n’était pas chose facile car il fallait utiliser des fonctionnalités spécifiques de certains serveurs d’applications ou bricoler le code. Désormais, grâce au nouveau conteneur intégré, un EJB devient une classe testable comme une autre car elle peut s’exécuter dans un environnement Java SE. La seule exigence consiste à ajouter un fichier jar spécifique au classpath, comme on l’a fait dans le fichier pom. xml du Listing 6.8 avec la dépendance glassfish-embedded-all.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
222
Java EE 6 et GlassFish 3
Au Chapitre 2, nous avons déjà présenté tous les artéfacts requis par les tests unitaires avec une base de données intégrée. Pour tester unitairement l’EJB, nous avons besoin de la base Derby intégrée, d’une unité de persistance différente et du conteneur d’EJB intégré. Il suffit ensuite d’écrire une classe de test JUnit (voir Listing 6.9) pour initialiser l’EJBContainer (EJBContainer.createEJBContainer()), lancer quelques tests (createBook()) et fermer le conteneur (ec.close()). Listing 6.9 : Classe JUnit pour tester l’EJB avec le conteneur intégré public class BookEJBTest { private static EJBContainer ec; private static Context ctx; @BeforeClass public static void initContainer() throws Exception { ec = EJBContainer.createEJBContainer(); ctx = ec.getContext(); } @AfterClass public static void closeContainer() throws Exception { ec.close(); } @Test public void createBook() throws Exception { // Création d’une instance de Book Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction comedy book"); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); // Recherche de l’EJB BookEJBRemote bookEJB = (BookEJBRemote) „ ctx.lookup("java:global/chapter06/BookEJBRemote"); // Rend le livre persistant dans la base book = bookEJB.createBook(book); assertNotNull("ID should not be null", book.getId()); // Récupère tous les livres de la base List books = bookEJB.findBooks(); assertNotNull(books); } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 6
Enterprise Java Beans 223
La méthode de test createBook() crée une instance de Book, recherche l’interface distante en utilisant JNDI pour rendre le livre persistant et récupère la liste de tous les livres stockés dans la base. Assurez-vous que la classe BookEJBTest soit dans le répertoire src/test/java de Maven, puis faites la commande suivante : mvn test
BookEJBTest
s’exécute et Maven devrait vous informer que le test s’est bien passé :
Résumé Ce chapitre a présenté EJB 3.1. À partir des versions 2.x, la spécification EJB a évolué pour passer d’un modèle lourd – dans lequel il fallait assembler les interfaces home et distante/locale avec une grande quantité de fichiers XML – à une simple classe Java sans interface et avec une seule annotation. La fonctionnalité sous-jacente est pourtant toujours la même : fournir une logique métier transactionnelle et sécurisée. EJB 3.1 permet de simplifier encore plus le modèle de programmation (vue sans interface, déploiement war), l’enrichit (conteneur intégré, singletons, service timer, appels asynchrones) et améliore sa portabilité entre les serveurs d’applications (noms JNDI standardisés). La simplification la plus importante est probablement la création d’EJB Lite, un sous-ensemble de l’API EJB qui offre une version d’EJB plus simple mais néanmoins efficace pouvant être utilisée dans le profil web de Java EE. Le conteneur EJB intégré, de son côté, facilite la mise en place et la portabilité des tests unitaires. EJB 3.1 est donc le digne successeur d’EJB 3.0. Le Chapitre 7 s’intéressera aux beans de session avec et sans état, aux singletons et au service timer. Le Chapitre 8 présentera les méthodes de rappel et les intercepteurs. Le Chapitre 9 sera consacré aux transactions et à la sécurité.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
7 Beans de session et service timer Les Chapitres 2 à 5 se sont intéressés aux objets persistants qui utilisent les entités JPA. Ces entités encapsulent les données, l’association avec le modèle relationnel et, parfois, la logique de validation. Nous allons maintenant présenter le développement d’une couche métier qui gère ces objets persistants avec les beans de session qui prennent en charge les tâches complexes nécessitant des interactions avec d’autres composants (entités, services web, messages, etc.). Cette séparation logique entre entités et beans de session respecte le paradigme de "séparation des problèmes" selon lequel une application est divisée en plusieurs composants dont les opérations se recouvrent le moins possible. Dans ce chapitre, nous présenterons les trois types de beans de session : sans état, avec état et singleton. Les premiers sont les plus adaptables des trois car ils ne mémorisent aucune information et effectuent toute la logique métier dans un seul appel de méthode. Les seconds gèrent un état conversationnel avec un seul client. Les beans de session singletons (une seule instance par application) ont été ajoutés dans la spécification EJB 3.1. La dernière section du chapitre sera consacrée à l’utilisation du service timer pour planifier les tâches. Les beans pilotés par messages (MDB), qui font également partie de la spécification EJB, seront présentés au Chapitre 13 avec JMS (Java Message Service). Comme nous le verrons aux Chapitres 14 et 15, un bean de session sans état peut être transformé en service web SOAP ou REST. Ou, plus exactement, ces services web peuvent profiter de certaines fonctionnalités d’EJB comme les transactions, la sécurité, les intercepteurs, etc.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
226
Java EE 6 et GlassFish 3
Beans de session Les beans de session sont parfaits pour implémenter la logique métier, les processus et le workflow mais, avant de les utiliser, vous devez choisir le type qui convient : ■■
Sans état. Ne mémorisent aucun état conversationnel pour l’application. Ils servent à gérer les tâches qui peuvent s’effectuer à l’aide d’un seul appel de méthode.
■■
Avec état. Mémorisent l’état et sont associés à un client précis. Ils servent à gérer les tâches qui demandent plusieurs étapes.
■■
Singletons. Implémentent le patron de conception Singleton. Le conteneur s’assurera qu’il n’en existe qu’une seule instance pour toute l’application.
Bien que ces trois types de beans de session aient des fonctionnalités spécifiques, ils en ont aussi beaucoup en commun et, surtout, ils utilisent tous le même modèle de programmation. Comme nous le verrons plus tard, un bean de session peut avoir une interface locale ou distante, ou aucune interface. Les beans de session sont des composants gérés par un conteneur et doivent donc être assemblés dans une archive (un fichier jar, war ou ear) et déployés dans le conteneur. Ce dernier est responsable de la gestion de leur cycle de vie (qui sera étudié au chapitre suivant), des transactions, des intercepteurs et de bien d’autres choses encore. La Figure 7.1 montre un schéma très synthétique des beans de session et du service timer dans un conteneur EJB. Figure 7.1 Beans de session et service timer dans un conteneur EJB.
Conteneur EJB
Bean de session
Service timer
Beans sans état
Les beans sans état sont les beans de session les plus connus dans les applications Java EE. Ils sont simples, puissants, efficaces et répondent aux besoins fréquents des tâches métiers. "Sans état" signifie simplement qu’une tâche doit se réaliser par un seul appel de méthode.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 227
À titre d’exemple, revenons aux racines de la programmation orientée objet, où un objet encapsule son état et son comportement. Pour rendre un livre persistant dans une base de données en utilisant un seul objet, vous devez réaliser les opérations suivantes : créer une instance book de Book, initialiser ses attributs et appeler une méthode afin qu’il se stocke lui-même dans la base de données (book.persistTo Database()). Dans le code suivant, vous pouvez constater que l’objet book est appelé plusieurs fois entre la première et la dernière ligne et qu’il mémorise son état : Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction by Douglas Adams."); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); book.persistToDatabase();
Les beans sans état sont la solution idéale lorsque l’on doit implémenter une tâche qui peut se réaliser en un seul appel de méthode. Si l’on reprend le code précédent et que l’on y ajoute un composant sans état, il faut donc créer un objet Book, initialiser ses attributs, puis utiliser un composant sans état pour invoquer une méthode qui stockera le livre en un seul appel. L’état est donc géré par Book, non par le composant sans état. Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction by Douglas Adams."); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); statelessComponent.persistToDatabase(book);
Les beans de session sans état sont également les beans les plus efficaces car ils peuvent être placés dans un pool pour y être partagés par plusieurs clients – le conteneur conserve en mémoire un certain nombre d’instances (un pool) de chaque EJB sans état et les partage entre les clients. Ces beans ne mémorisant pas l’état des clients, toutes leurs instances sont donc équivalentes. Lorsqu’un client appelle une méthode d’un bean sans état, le conteneur choisit une instance du pool et l’affecte au client ; lorsque ce dernier en a fini, l’instance retourne dans le pool pour y être réutilisée. Comme le montre la Figure 7.2, il suffit donc d’un petit nombre de beans pour gérer plusieurs clients (le conteneur ne garantit pas qu’il fournira toujours la même instance du bean pour un client donné).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
228
Java EE 6 et GlassFish 3
Figure 7.2
Conteneur EJB
Clients accédant à des beans sans état placés dans un pool.
Client 1
Pool
Client 2
...
Client n
Instance 1
Instance 2
Le Listing 7.1 montre qu’un EJB sans état ressemble à une simple classe Java avec uniquement une annotation @Stateless. Il peut utiliser n’importe quel service du conteneur dans lequel il se trouve, notamment l’injection de dépendances. L’annotation @PersistenceContext sert à injecter une référence de gestionnaire d’entités. Le contexte de persistance des beans de session sans état étant transactionnel, toutes les méthodes appelées sur cet EJB (createBook(), createCD(), etc.) le seront également (nous y reviendrons plus en détail au Chapitre 9). Vous remarquerez que toutes les méthodes reçoivent les paramètres nécessaires au traitement de la logique métier en un seul appel : createBook(), par exemple, prend un objet Book en paramètre et le rend persistant sans avoir besoin d’aucune autre information. Listing 7.1 : Bean de session sans état ItemEJB @Stateless public class ItemEJB { @PersistenceContext(unitName = "chapter07PU") private EntityManager em; public List findBooks() { Query query = em.createNamedQuery("findAllBooks"); return query.getResultList(); } public List findCDs() { Query query = em.createNamedQuery("findAllCDs"); return query.getResultList(); } public Book createBook(Book book) { em.persist(book); return book; }
}
public CD createCD(CD cd) { em.persist(cd); return cd; }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 229
Les beans de session sans état offrent souvent plusieurs méthodes métiers étroitement liées. Le bean ItemEJB du Listing 7.1, par exemple, définit des méthodes qui concernent les articles vendus par l’application CD-BookStore : vous y trouverez donc les opérations de création, de modification ou de recherche de livres et de CD, ainsi que d’autres traitements métiers apparentés. Grâce à l’annotation @javax.ejb.Stateless, le POJO ItemEJB devient un bean de session sans état – elle transforme donc une simple classe Java en composant pour conteneur. Le Listing 7.2 contient la spécification de cette annotation. Listing 7.2 : API de l’annotation @Stateless @Target({TYPE}) @Retention(RUNTIME) public @interface Stateless { String name() default ""; String mappedName() default ""; String description() default ""; }
Le paramètre name précise le nom du bean, qui est, par défaut, celui de la classe (ItemEJB dans l’exemple du Listing 7.1). Ce paramètre peut être utilisé pour rechercher un EJB particulier avec JNDI, par exemple. description est une chaîne permettant de décrire l’EJB et mappedName est le nom JNDI global affecté par le conteneur – ce dernier est spécifique à l’éditeur et n’est donc pas portable. mappedName n’a aucun rapport avec le nom JNDI global et portable que nous avons évoqué au chapitre précédent et que nous décrirons en détail dans la section "Accès JNDI global", plus loin dans ce chapitre. Les beans de session sans état peuvent supporter un grand nombre de clients en minimisant les ressources nécessaires : c’est la raison pour laquelle les applications qui les utilisent sont plus adaptables. Les beans de session avec état, au contraire, ne sont liés qu’à un et un seul client. Beans avec état
Les beans sans état fournissent des méthodes métiers aux clients mais n’entretiennent pas d’état conversationnel avec eux. Les beans de session avec état, par contre, préservent cet état : ils permettent donc d’implémenter les tâches qui nécessitent plusieurs étapes, chacune tenant compte de l’état de l’étape précédente. Prenons comme exemple le panier virtuel d’un site de commerce en ligne : un client se connecte (sa session débute), choisit un premier livre et l’ajoute à son panier, puis
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
230
Java EE 6 et GlassFish 3
choisit un second livre et l’ajoute également. Puis le client valide la commande, la paye et se déconnecte (la session se termine). Ici, le panier virtuel conserve l’état – les livres choisis – pendant tout le temps de la session. Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction by Douglas Adams."); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); statefullComponent.addBookToShoppingCart(book); book.setTitle("The Robots of Dawn"); book.setPrice(18.25F); book.setDescription("Isaac Asimov’s Robot Series"); book.setIsbn("0-553-29949-2"); book.setNbOfPage(276); book.setIllustrations(false); statefullComponent.addBookToShoppingCart(book); statefullComponent.checkOutShoppingCart();
Le code précédent montre bien comment fonctionne un bean de session avec état. Il crée deux livres et les ajoute au panier virtuel d’un composant avec état. À la fin, la méthode checkOutShoppingCart() se fie a l’état mémorisé pour commander les deux livres. Quand un client invoque un bean avec état sur le serveur, le conteneur EJB doit fournir la même instance à chaque appel de méthode – ce bean ne peut pas être réutilisé par un autre client. La Figure 7.3 montre la relation 1–1 qui s’établit entre l’instance et le client ; du point de vue du développeur, aucun code supplémentaire n’est nécessaire car cette relation est gérée automatiquement par le conteneur. Figure 7.3 Clients accédant à des beans avec état.
Conteneur EJB
Client 1
Instance 1
Client 2
Instance 2
Cette relation 1–1 a évidemment un prix : si l’on a 1 million de clients, ceci signifie que l’on aura 1 million de beans en mémoire. Pour réduire cette occupation, les beans doivent donc être supprimés temporairement de la mémoire entre deux requêtes – cette technique est appelée passivation et activation. La passivation consiste à supprimer une instance de la mémoire et à la sauvegarder dans un emplacement
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 231
persistant (un fichier sur disque, une base de données, etc.) : elle permet de libérer la mémoire et les ressources. L’activation est le processus inverse : elle restaure l’état et l’applique à une instance. Ces deux opérations sont réalisées par le conteneur : le développeur n’a pas à s’en occuper – comme nous le verrons au prochain chapitre, il doit simplement se charger de la libération des ressources (connexion à une base de données ou à une fabrique JMS, etc.) avant que la passivation n’ait lieu. Le Listing 7.3 applique l’exemple du panier virtuel à un bean avec état. Un client se connecte au site web, parcourt le catalogue des articles et ajoute deux livres au panier (à l’aide de la méthode addItem()). L’attribut cartItems stocke le contenu du panier. Supposons que le client décide alors d’aller chercher un café : pendant ce temps, le conteneur peut passiver l’instance pour libérer la mémoire, ce qui entraîne la sauvegarde du contenu du panier dans une zone de stockage permanente. Quelques minutes plus tard, le client revient et veut connaître le montant total de son panier (avec la méthode getTotal()) avant de passer commande. Le conteneur active donc l’EJB pour restaurer les données dans le panier et le client peut alors commander (méthode checkout()) ses livres. Lorsqu’il se déconnecte, sa session se termine et le conteneur libère la mémoire en supprimant définitivement l’instance du bean en mémoire. Listing 7.3 : Bean de session avec état @Stateful @StatefulTimeout(20000) public class ShoppingCartEJB { private List cartItems = new ArrayList(); public void addItem(Item item) { if (!cartItems.contains(item)) cartItems.add(item); } public void removeItem(Item item) { if (cartItems.contains(item)) cartItems.remove(item); } public Float getTotal() { if (cartItems == null || cartItems.isEmpty()) return 0f;
}
Float total = 0f; for (Item cartItem : cartItems) { total += (cartItem.getPrice()); } return total;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
232
Java EE 6 et GlassFish 3
@Remove public void checkout() { // Code métier cartItems.clear(); } @Remove public void empty() { cartItems.clear(); } }
Ce que nous venons de voir pour le panier virtuel représente l’utilisation classique des beans avec état, dans laquelle le conteneur gère automatiquement l’état conversationnel. Ici, la seule annotation nécessaire est @javax.ejb.Stateful, qui utilise les mêmes paramètres que @Stateless (voir Listing 7.2). Nous avons également utilisé les annotations facultatives @javax.ejb.StatefulTimeout et @javax.ejb.Remove. @Remove décore les méthodes checkout() et empty() : leur appel provoquera désormais la suppression définitive de l’instance de la mémoire. @StatefulTimeout met en place un délai d’expiration en millisecondes – si le bean ne reçoit aucune demande du client au bout de ce délai, il sera supprimé par le conteneur. Il est également possible de se passer de ces annotations en se fiant au fait que le conteneur supprime automatiquement une instance lorsqu’une session client se termine ou expire, mais s’assurer que le bean est détruit au moment adéquat permet de réduire l’occupation mémoire, ce qui peut se révéler essentiel pour les applications à haute concurrence. Singletons
Un bean singleton est simplement un bean de session qui n’est instancié qu’une seule fois par application. C’est donc une implémentation du fameux patron de conception du Gang of Four, décrit dans l’ouvrage Design Patterns: Elements of Reusable Object-Oriented Software, d’Erich Gamma, Richard Helm, Ralph Johnson et John M. Vlissides (Addison-Wesley, 1995). Il garantit qu’une seule instance d’une classe existera dans l’application et fournit un point d’accès global vers cette classe. Les objets singletons sont nécessaires dans toutes les situations où l’on n’a besoin que d’un seul exemplaire d’un objet – pour décrire une souris, un gestionnaire de fenêtres, un spooler d’impression, un système de fichiers, etc. Un autre cas d’utilisation des singletons est la création d’un cache unique pour toute l’application afin d’y stocker des objets. Dans un environnement géré par
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 233
l’application, vous devez modifier le code de la classe du cache afin de la transformer en singleton (voir Listing 7.4). Pour cela, il faut d’abord rendre son constructeur privé pour empêcher la création d’une nouvelle instance. La méthode publique et statique getInstance() se chargera alors de renvoyer la seule instance possible de la classe. Pour ajouter un objet au cache en utilisant le singleton, une classe cliente devra alors réaliser l’appel suivant : CacheSingleton.getInstance().addToCache(myObject);
Le mot-clé synchronized permet d’empêcher toute interférence lors de l’accès à cette méthode par plusieurs threads. Listing 7.4 : Classe Java respectant le patron de conception Singleton public class CacheSingleton { private static CacheSingleton instance = new CacheSingleton(); private Map cache = new HashMap(); private CacheSingleton() { } public static synchronized CacheSingleton getInstance() { return instance; } public void addToCache(Long id, Object object) { if (!cache.containsKey(id)) cache.put(id, object); } public void removeFromCache(Long id) { if (cache.containsKey(id)) cache.remove(id); } public Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
EJB 3.1 introduit les beans de session singletons qui respectent le patron de conception Singleton : une fois instancié, le conteneur garantit qu’il n’y aura qu’une seule instance du singleton pour toute la durée de l’application. Comme le montre la Figure 7.4, une instance est partagée par plusieurs clients. Les beans singletons mémorisent leur état entre les appels des clients.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
234
Java EE 6 et GlassFish 3
Figure 7.4
Conteneur EJB
Clients accédant à un bean singleton.
Client 1
Instance unique
Client 2
INFO Les singletons ne sont pas compatibles avec les clusters. Un cluster est un groupe de conteneurs fonctionnant de concert (ils partagent les mêmes ressources, les mêmes EJB, etc.). Lorsqu’il y a plusieurs conteneurs répartis en cluster sur des machines différentes, chaque conteneur aura donc sa propre instance du singleton.
Il n’y a pas grand-chose à faire pour transformer le code du Listing 7.4 en bean de session singleton (voir Listing 7.5). En fait, il suffit d’annoter la classe avec @ Singleton et de ne pas s’occuper du constructeur privé ou de la méthode statique getInstance() : le conteneur s’assurera qu’une seule instance est créée. L’annotation @javax.ejb.Singleton a la même API que celle de l’annotation @Stateless décrite dans le Listing 7.2. Listing 7.5 : Bean de session singleton @Singleton public class CacheEJB { private Map cache = new HashMap(); public void addToCache(Long id, Object object) { if (!cache.containsKey(id)) cache.put(id, object); } public void removeFromCache(Long id) { if (cache.containsKey(id)) cache.remove(id); } public Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 235
Comme vous pouvez le constater, les beans de session sans état, avec état et singletons sont très simples à écrire puisqu’il suffit d’une seule annotation. Les singletons, toutefois, ont plus de possibilités : ils peuvent être initialisés au lancement de l’application, chaînés ensemble, et il est possible de personnaliser leurs accès concurrents. Initialisation
Lorsqu’une classe client veut appeler une méthode d’un bean singleton, le conteneur s’assure de créer l’instance ou d’utiliser celle qui existe déjà. Parfois, cependant, l’initialisation d’un singleton peut être assez longue : CacheEJB peut, par exemple, devoir accéder à une base de données pour charger un millier d’objets. En ce cas, le premier appel au bean prendra du temps et le premier client devra attendre la fin de son initialisation. Pour éviter ce temps de latence, vous pouvez demander au conteneur d’initialiser un bean singleton dès le démarrage de l’application en ajoutant l’annotation @Startup à la déclaration du bean : @Singleton @Startup public class CacheEJB { // ... }
Chaînage de singletons
Dans certains cas, l’ordre explicite des initialisations peut avoir une importance lorsque l’on a plusieurs beans singletons. Supposons que le bean CacheEJB ait besoin de stocker des données provenant d’un autre bean singleton (un CountryCodeEJB renvoyant tous les codes ISO des pays, par exemple) : ce dernier doit donc être initialisé avant le CacheEJB. L’annotation @javax.ejb.DependsOn est justement prévue pour exprimer les dépendances entre les singletons : @Singleton public class CountryCodeEJB { ... } @DependsOn("CountryCodeEJB") @Singleton public class CacheEJB { ... }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
236
Java EE 6 et GlassFish 3
prend en paramètre une ou plusieurs chaînes désignant chacune le nom d’un bean singleton dont dépend le singleton annoté. Le code suivant, par exemple, montre que CacheDB dépend de l’initialisation de CountryCodeEJB et ZipCodeEJB – @DependsOn("CountryCodeEJB", "ZipCodeEJB") demande au conteneur de garantir que les singletons CountryCodeEJB et ZipCodeEJB seront initialisés avant CacheEJB. @DependsOn
@Singleton public class CountryCodeEJB { ... } @Singleton public class ZipCodeEJB { ... } @DependsOn("CountryCodeEJB", "ZipCodeEJB") @Startup @Singleton public class CacheEJB { ... }
Comme vous pouvez le constater dans le code précédent, il vous est même possible de combiner ces dépendances avec une initialisation lors du démarrage de l’application : CacheEJB étant initialisé dès le lancement (car il est annoté par @Startup), CountryCodeEJB et ZipCodeEJB le seront également, mais avant lui. Concurrence
Un singleton n’ayant qu’une seule instance partagée par plusieurs clients, les accès concurrents peuvent être contrôlés de trois façons différentes par l’annotation @ConcurrencyManagement : ■■
Concurrence gérée par le conteneur (Container-Managed Concurrency ou CMC). Le conteneur contrôle les accès concurrents en utilisant les métadonnées (annotation ou l’équivalent en XML).
■■
Concurrence gérée par le bean (Bean-Managed Concurrency ou BMC). Le conteneur autorise tous les accès concurrents et délègue la responsabilité de la synchronisation de ces accès au bean lui-même.
■■
Concurrence interdite. Si un client appelle une méthode métier qui est en cours d’utilisation par un autre client, l’exception ConcurrentAccessException est levée.
En l’absence d’indication explicite, la gestion par défaut est CMC. Un bean singleton peut utiliser CMC ou BMC, mais pas les deux.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 237
Concurrence gérée par le conteneur
Avec CMC, la valeur par défaut, le conteneur est responsable du contrôle des accès concurrents à l’instance du bean singleton. Vous pouvez alors vous servir de l’annotation @Lock pour préciser le type de verrouillage : ■■ @Lock(LockType.WRITE).
Une méthode annotée par un verrou WRITE (exclusif) n’autorisera aucun autre appel concurrent tant qu’elle est en cours d’exécution. Si un client C1 appelle une méthode avec un verrou exclusif, le client C2 ne pourra pas l’appeler tant que l’appel de C1 ne s’est pas terminé.
■■ @Lock(LockType.READ).
Une méthode annotée par un verrou READ (partagé) autorisera un nombre quelconque d’appels concurrents. Deux clients C1 et C2 pourront appeler simultanément une méthode avec un verrou partagé.
L’annotation @Lock peut être associée à la classe, aux méthodes ou aux deux. Dans le premier cas, cela revient à l’associer à toutes les méthodes. En l’absence d’indication, le type de verrouillage par défaut est WRITE. Dans le code du Listing 7.6, le bean CacheEJB utilise un verrou READ, ce qui implique que toutes ses méthodes auront un verrou partagé, sauf getFromCache(), qui l’a redéfini à WRITE. Listing 7.6 : Bean de session Singleton avec CMC @Singleton @Lock(LockType.READ) public class CacheEJB { private Map cache = new HashMap(); public void addToCache(Long id, Object object) { if (!cache.containsKey(id)) cache.put(id, object); } public void removeFromCache(Long id) { if (cache.containsKey(id)) cache.remove(id); } @AccessTimeout(2000) @Lock(LockType.WRITE) public Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
238
Java EE 6 et GlassFish 3
Vous remarquerez que la méthode getFromCache() utilise également une annotation @AccessTimeout. Celle-ci permet de limiter le temps pendant lequel un accès concurrent sera bloqué : si le verrou n’a pas pu être obtenu dans ce délai, la requête sera rejetée. Ici, si un appel à getFromCache() est bloqué pendant plus de 2 secondes, l’appelant recevra l’exception ConcurrentAccessTimeoutException. Concurrence gérée par le bean
Avec BMC, le conteneur autorise tous les accès à l’instance du bean singleton. C’est donc le développeur qui doit protéger l’état contre les erreurs de synchronisation dues aux accès concurrents. Pour ce faire, il peut utiliser les primitives de synchronisation de Java, comme synchronized et volatile. Dans le Listing 7.7, le bean CacheEJB utilise BMC (@ConcurrencyManagement(BEAN)) et protège les accès à la méthode addToCache() à l’aide du mot-clé synchronized. Listing 7.7 : Bean de session singleton avec BMC @Singleton @ConcurrencyManagement(ConcurrencyManagementType.BEAN) public class CacheEJB { private Map cache = new HashMap(); public synchronized void addToCache(Long id, Object object) { if (!cache.containsKey(id)) cache.put(id, object); } public void removeFromCache(Long id) { if (cache.containsKey(id)) cache.remove(id); } public synchronized Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
Concurrence interdite
Les accès concurrents peuvent également être interdits sur une méthode ou sur l’ensemble du bean : en ce cas, un client appelant une méthode en cours d’utilisation
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 239
par un autre client recevra l’exception ConcurrentAccessException. Ceci peut avoir des conséquences sur les performances puisque les clients devront gérer l’exception, réessayeront d’accéder au bean, etc. Dans le Listing 7.8, le bean CacheEJB interdit la concurrence sur la méthode addToCache() ; les deux autres méthodes utilisent le verrouillage par défaut défini au niveau de la classe : CMC avec @LockREAD). Listing 7.8 : Bean de session singleton interdisant la concurrence @Singleton @Lock(LockType.READ) public class CacheEJB { private Map cache = new HashMap(); @ConcurrencyManagement(ConcurrencyManagementType.CONCURRENCY_NOT_ALLOWED) public void addToCache(Long id, Object object) { if (!cache.containsKey(id)) cache.put(id, object); } public void removeFromCache(Long id) { if (cache.containsKey(id)) cache.remove(id); } @AccessTimeout(2000) @Lock(LockType.WRITE) public Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
Modèle des beans de session
Pour l’instant, les exemples de beans de session que nous avons présentés utilisaient le modèle de programmation le plus simple : un POJO annoté sans interface. En fonction de vos besoins, les beans peuvent vous offrir un modèle bien plus riche vous permettant de réaliser des appels distants, l’injection de dépendances ou des appels asynchrones.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
240
Java EE 6 et GlassFish 3
Interfaces et classe bean
Les beans de session que nous avons étudiés n’étaient composés que d’une seule classe. En réalité, ils peuvent inclure les éléments suivants : ■■
Interfaces métiers. Ces interfaces contiennent les déclarations des méthodes métiers visibles par les clients et implémentées par la classe bean. Un bean de session peut avoir des interfaces locales, distantes, ou aucune interface (une vue sans interface avec uniquement un accès local).
■■
Une classe bean. Cette classe contient les implémentations des méthodes métiers et peut implémenter aucune ou plusieurs interfaces métiers. En fonction du type de bean, elle doit être annotée par @Stateless, @Stateful ou @Singleton.
Comme le montre la Figure 7.5, une application cliente peut accéder à un bean de session par l’une de ses interfaces (locale ou distante) ou directement en invoquant la classe elle-même. Figure 7.5 Les beans de session peuvent avoir différents types d’interfaces.
Client
Local Distant
Bean de session
pas d'interface
Vues distantes, locales et sans interface
Selon d’où un client invoque un bean de session, la classe de ce dernier devra implémenter des interfaces locales ou distantes, voire aucune interface. Si, dans votre architecture, les clients se trouvent à l’extérieur de l’instance JVM du conteneur d’EJB, ils devront utiliser une interface distante. Comme le montre la Figure 7.6, ceci s’applique également aux clients qui s’exécutent dans une JVM séparée (un client riche, par exemple), dans un conteneur client d’application (ACC) ou dans un conteneur web ou EJB externe. Dans ces situations, les clients devront invoquer les méthodes des beans de session via RMI (Remote Method Invocation). Les appels locaux, en revanche, ne peuvent être utilisés que lorsque le bean et le client s’exécutent dans la même JVM – un EJB invoquant un autre EJB ou un composant web (servlet, JSF) tournant dans un conteneur web de la même JVM, par exemple. Une application peut également utiliser des appels distants et locaux sur le même bean de session.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Figure 7.6 Beans de session appelés par plusieurs clients.
Un bean de session peut implémenter plusieurs interfaces ou aucune. Une interface métier est une interface classique de Java qui n’hérite d’aucune interface EJB spécifique. Comme toute interface Java, les interfaces métiers énumèrent les méthodes qui seront disponibles pour l’application cliente. Elles peuvent utiliser les annotations suivantes : ■■ @Remote indique une interface métier distante. Les paramètres des méthodes sont
passés par valeur et doivent être sérialisables pour être pris en compte par le protocole RMI. indique une interface métier locale. Les paramètres des méthodes sont passés par référence du client au bean.
■■ @Local
Une interface donnée ne peut pas utiliser plus d’une de ces annotations. Les beans de session que nous avons vu jusqu’à présent n’avaient pas d’interface – la vue sans interface est une variante de la vue locale qui expose localement toutes les méthodes métiers publiques de la classe bean sans nécessiter l’emploi d’une interface métier. Le Listing 7.9 présente une interface locale (ItemLocal) et une interface distante (ItemRemote) implémentées par le bean de session sans état ItemEJB. Dans cet exemple, les clients pourront appeler localement ou à distance la méthode findCDs() puisqu’elle est définie dans ces deux interfaces. La méthode createCd(), par contre, ne pourra être appelée que par RMI.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
242
Java EE 6 et GlassFish 3
Listing 7.9 : Bean de session sans état implémentant une interface distante et locale @Local public interface ItemLocal { List findBooks(); List findCDs(); } @Remote public interface ItemRemote { List findBooks(); List findCDs(); Book createBook(Book book); CD createCD(CD cd); } @Stateless public class ItemEJB implements ItemLocal, ItemRemote { ... }
Dans le code du Listing 7.9, vous pourriez également préciser la nature de l’interface dans la classe du bean. En ce cas, il faudrait inclure le nom de l’interface dans les annotations @Local et @Remote comme le montre le Listing 7.10. Cette approche est tout particulièrement adaptée lorsque l’on dispose d’interfaces existantes et que l’on souhaite les utiliser avec le bean de session. Listing 7.10 : Une classe bean définissant une interface locale et une interface distante public interface ItemLocal { List findBooks(); List findCDs(); } public interface ItemRemote { List findBooks(); List findCDs(); Book createBook(Book book); CD createCD(CD cd); } @Stateless @Remote (ItemRemote) @Local (ItemLocal) public class ItemEJB implements ItemLocal, ItemRemote { ... }
Interfaces de services web
Outre les appels distants par RMI, les beans sans état peuvent également être appelés à distance comme services web SOAP ou REST. Ceux-ci faisant l’objet des
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 243
Chapitres 14 et 15, nous ne les citons ici que parce qu’ils font partie des différentes façons d’appeler un bean de session sans état, simplement en implémentant des interfaces annotées différentes. Le Listing 7.11 présente un bean sans état avec une interface locale, un service web SOAP (@WebService) et un service web REST (@Path). Listing 7.11 : Bean de session sans état implémentant une interface de services web @Local public interface ItemLocal { List findBooks(); List findCDs(); } @WebService public interface ItemWeb { List findBooks(); List findCDs(); Book createBook(Book book); CD createCD(CD cd); } @Path(/items) public interface ItemRest { List findBooks(); } @Stateless public class ItemEJB implements ItemLocal, ItemWeb, ItemRest { ... }
Classes bean
Un bean de session sans état est une classe Java classique qui implémente une logique métier. Pour qu’elle devienne une classe bean de session, elle doit satisfaire les obligations suivantes : ■■
Elle doit être annotée par @Stateless, @Stateful, @Singleton ou leurs équivalents XML dans un descripteur de déploiement.
■■
Elle doit implémenter les méthodes de ses éventuelles interfaces.
■■
Elle doit être publique et ni finale ni abstraite.
■■
Elle doit fournir un constructeur public sans paramètre qui servira à créer les instances.
■■
Elle ne doit pas définir de méthode finalize().
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
244
Java EE 6 et GlassFish 3
■■
Les noms des méthodes métiers ne doivent pas commencer par ejb et ne peuvent être ni finals ni statiques.
■■
Le paramètre et la valeur de retour d’une méthode distante doivent être d’un type reconnu par RMI.
Vue cliente
Maintenant que nous avons vu des exemples de beans de session et de leurs différentes interfaces, nous pouvons étudier la façon dont le client les appelle. Le client d’un bean de session peut être n’importe quel type de composant : un POJO, une interface graphique (Swing), une servlet, un bean géré par JSF, un service web (SOAP ou REST) ou un autre EJB (déployé dans le même conteneur ou dans un autre). Pour appeler une méthode d’un bean de session, un client n’instancie pas directement le bean avec l’opérateur new. Pourtant, il a besoin d’une référence à ce bean (ou à l’une de ses interfaces) : il peut en obtenir une via l’injection de dépendances (avec l’annotation @EJB) ou par une recherche JNDI. Sauf mention contraire, un client invoque un bean de façon synchrone mais, comme nous le verrons plus loin, EJB 3.1 autorise maintenant les appels de méthodes asynchrones. @EJB
Java EE utilise plusieurs annotations pour injecter des références de ressources (@Resource), de gestionnaires d’entités (@PersistenceContext), de services web (@WebServiceRef), etc. L’annotation @javax.ejb.EJB, en revanche, est spécialement conçue pour injecter des références de beans de session dans du code client. L’injection de dépendances n’est possible que dans des environnements gérés, comme les conteneurs EJB, les conteneurs web et les conteneurs clients d’application. Reprenons nos premiers exemples dans lesquels les beans de session n’avaient pas d’interface. Pour qu’un client invoque une vue de bean sans interface, il doit obtenir une référence à la classe elle-même. Dans le code suivant, par exemple, le client obtient une référence à la classe ItemEJB en utilisant l’annotation @EJB : @Stateless public class ItemEJB { ... } // Code client @EJB ItemEJB itemEJB;
Si le bean de session implémente plusieurs interfaces, par contre, le client devra indiquer celle qu’il veut référencer. Dans le code qui suit, le bean ItemEJB implémente
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 245
deux interfaces et le client peut invoquer cet EJB via son interface locale ou distante, mais il ne peut plus l’invoquer directement. @Stateless @Remote (ItemRemote) @Local (ItemLocal) public class ItemEJB implements ItemLocal, ItemRemote { ... } // Code client @EJB ItemEJB itemEJB; // Impossible @EJB ItemLocal itemEJBLocal; @EJB ItemRemote itemEJBRemote;
Si le bean expose au moins une interface, il doit préciser qu’il propose également une vue sans interface en utilisant l’annotation @LocalBean. Comme vous pouvez le constater dans le code suivant, le client peut maintenent appeler le bean via son interface locale, distante, ou directement via sa classe. @Stateless @Remote (ItemRemote) @Local (ItemLocal) @LocalBean public class ItemEJB implements ItemLocal, ItemRemote { ... } // Code client @EJB ItemEJB itemEJB; @EJB ItemLocal itemEJBLocal; @EJB ItemRemote itemEJBRemote;
Si l’injection n’est pas possible (lorsque le composant n’est pas géré par le conteneur), vous pouvez utiliser JNDI pour rechercher les beans de session à partir de leur nom JNDI portable. Accès JNDI global
Les beans de session peuvent également être recherchés par JNDI, qui est surtout utilisée pour les accès distants lorsqu’un client non géré par un conteneur ne peut pas utiliser l’injection de dépendances. Mais JNDI peut également être utilisée par des clients locaux, même si l’injection de dépendances produit un code plus clair. Pour rechercher des beans de session, une application cliente doit faire communiquer l’API JNDI avec un service d’annuaire. Un bean de session déployé dans un conteneur est automatiquement lié à un nom JNDI. Avant Java EE 6, ce nom n’était pas standardisé, ce qui impliquait qu’un bean déployé dans des conteneurs différents (GlassFish, JBoss, WebLogic, etc.) portait
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
246
Java EE 6 et GlassFish 3
des noms différents. La spécification Java EE 6 a corrigé ce problème en définissant des noms JNDI portables ayant la syntaxe suivante : java:global[/]// „ [!]
Les différentes parties d’un nom JNDI ont les significations suivantes : est facultative car cette partie ne s’applique que si le bean est assemblé dans un fichier ear. En ce cas, est, par défaut, le nom du fichier ear (sans l’extension .ear). ■■ est le nom du module dans lequel a été assemblé le bean de session. Ce module peut être un module EJB dans un fichier jar autonome ou un module web dans un fichier war. Par défaut, est le nom du fichier archive, sans son extension. ■■ est le nom du bean de session. ■■ est le nom pleinement qualifié de chaque interface métier qui a été définie. Dans le cas des vues sans interface, ce nom est le nom pleinement qualifié de la classe du bean. Pour illustrer cette convention, prenons l’exemple du bean ItemEJB. ItemEJB est le et est assemblé dans l’archive cdbookstore.jar (le ). L’EJB a une interface distante et une vue sans interface (signalée par l’annotation @LocalBean). Lorsqu’il sera déployé, le conteneur créera donc les noms JNDI suivants : ■■
package com.apress.javaee6; @Stateless @LocalBean @Remote (ItemRemote) public class ItemEJB implements ItemRemote { ... } // noms JNDI java:global/cdbookstore/ItemEJB!com.apress.javaee6.ItemEJB java:global/cdbookstore/ItemEJB!com.apress.javaee6.ItemRemote
Outre cette convention, si le bean n’expose qu’une seule interface cliente (ou n’a qu’une vue sans interface), le conteneur enregistre une entrée JNDI pour cette vue avec la syntaxe suivante : java:global[/]//
Le code qui suit représente le bean ItemEJB avec seulement une vue sans interface. Le nom JNDI est alors uniquement composé du nom du module (cdbookstore) et de celui du bean.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 247
package com.apress.javaee6; @Stateless public class ItemEJB { ... } // Nom JNDI } java:global/cdbookstore/ItemEJB
Contexte de session
Les beans de session sont des composants métiers résidant dans un conteneur. Généralement, ils n’accèdent pas au conteneur et n’utilisent pas directement ses services (transactions, sécurité, injection de dépendances, etc.), qui sont prévus pour être gérés de façon transparente par le conteneur pour le compte du bean. Parfois, cependant, le bean a besoin d’utiliser explicitement les services du conteneur (pour annuler explicitement une transaction, par exemple) : en ce cas, il doit passer par l’interface javax.ejb.SessionContext, qui donne accès au contexte d’exécution qui lui a été fourni. SessionContext hérite de l’interface javax.ejb.EJBContext ; une partie des méthodes de son API est décrite dans le Tableau 7.1. Tableau 7.1 : Une partie des méthodes de l’interface SessionContext
Méthode
Description
getCallerPrincipal
Renvoie le java.security.Principal associé à l’appel.
getRollbackOnly
Teste si la transaction courante a été marquée pour annulation.
getTimerService
Renvoie l’interface javax.ejb.TimerService. Cette méthode ne peut être utilisée que par les beans sans état et singletons. Les beans avec état ne peuvent pas utiliser les services timer.
getUserTransaction
Renvoie l’interface javax.transaction.UserTransaction permettant de délimiter les transactions. Cette méthode ne peut être utilisée que par les beans de session avec des transactions gérées par les beans (BMT).
isCallerInRole
Teste si l’appelant a fourni un rôle de sécurité précis.
setRollbackOnly
Autorise le bean à marquer la transaction pour annulation. Cette méthode ne peut être utilisée que par les beans avec BMT.
wasCancelCalled
Teste si un client a appelé la méthode cancel() sur l’objet Future client correspondant à la méthode métier asynchrone en cours d’exécution.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
248
Java EE 6 et GlassFish 3
Un bean de session peut avoir accès à son contexte d’environnement en injectant une référence SessionContext à l’aide d’une annotation @Resource. @Stateless public class ItemEJB { @Resource private SessionContext context; ... public Book createBook(Book book) { ... if (cantFindAuthor()) context.setRollbackOnly(); } }
Descripteur de déploiement
Les composants Java EE 6 utilisent une configuration par exception, ce qui signifie que le conteneur, le fournisseur de persistance ou le serveur de message appliqueront un ensemble de services par défaut à ces composants. Si l’on souhaite disposer d’un comportement particulier, il faut explicitement utiliser une annotation ou son équivalent XML : c’est ce que nous avons déjà fait avec les entités JPA pour personnaliser les associations. Ce principe s’applique également aux beans de session : une seule annotation (@Stateless, @Stateful, etc.) suffit pour que le conteneur applique certains services (transaction, cycle de vie, sécurité, intercepteurs, concurrence, asynchronisme, etc.) mais, si vous voulez les modifier, d’autres annotations (ou leurs équivalents XML) sont à votre disposition. Les annotations et les descripteurs de déploiement XML permettent en effet d’attacher des informations supplémentaires à une classe, une interface, une méthode ou une variable. Un descripteur de déploiement XML est une alternative aux annotations, ce qui signifie que toute annotation a un marqueur XML équivalent. Lorsque les deux mécanismes sont utilisés, la configuration décrite dans le descripteur de déploiement a priorité sur les annotations. Nous ne rentrerons pas ici dans les détails de la structure d’un descripteur de déploiement XML (stocké dans un fichier nommé ejbjar.xml) car il est facultatif et peut être très verbeux. Le Listing 7.12 montre à quoi pourrait ressembler le fichier ejb-jar.xml d’ItemEJB (voir Listing 7.9). Il définit la classe bean, l’interface locale et distante, son type (Stateless) et indique qu’il utilise des transactions gérées par le conteneur (CMT). L’élément définit les entrées de l’environnement du bean de session. Nous y reviendrons dans la section "Contexte de nommage de l’environnement".
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 249
Listing 7.12 : Le fichier ejb-jar.xml ItemEJB com.apress.javaee6.ItemEJB com.apress.javaee6.ItemLocal com.apress.javaee6.ItemLocal Stateless Container aBookTitle java.lang.String Beginning Java EE 6
Si le bean de session est déployé dans un fichier jar, le descripteur de déploiement doit être stocké dans le fichier META-INF/ejb-jar.xml. S’il est déployé dans un fichier war, il doit être stocké dans le fichier WEB-INF/web.xml. Injection de dépendances
Nous avons déjà évoqué l’injection de dépendances, et vous la rencontrerez encore plusieurs fois dans les prochains chapitres. Il s’agit d’un mécanisme simple mais puissant utilisé par Java EE 6 pour injecter des références de ressources dans des attributs : au lieu que l’application recherche les ressources dans JNDI, celles-ci sont injectées par le conteneur. Les conteneurs peuvent injecter différents types de ressources dans les beans de session à l’aide de plusieurs annotations (ou descripteurs de déploiement) : injecte dans la variable annotée une référence de la vue locale, distante ou sans interface de l’EJB.
■■ @EJB
et @PersistenceUnit expriment, respectivement, une dépendance sur un EntityManager et sur une EntityManagerFactory.
■■ @PersistenceContext ■■ @WebServiceRef
injecte une référence à un service web.
injecte plusieurs ressources, comme les sources de données JDBC, les contextes de session, les transactions utilisateur, les fabriques de connexion JMS et les destinations, les entrées d’environnement, le service timer, etc.
■■ @Resource
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
250
Java EE 6 et GlassFish 3
L’extrait de code du Listing 7.13 montre un extrait de bean de session sans état utilisant plusieurs annotations pour injecter différentes ressources dans les attributs. Vous remarquerez que ces annotations peuvent porter sur les variables d’instance ainsi que sur les méthodes setter. Listing 7.13 : Un bean sans état utilisant l’injection @Stateless public class ItemEJB { @PersistenceContext(unitName = "chapter07PU") private EntityManager em; @EJB private CustomerEJB customerEJB; @WebServiceRef private ArtistWebService artistWebService; private SessionContext context;
Les paramètres des applications d’entreprise peuvent varier d’un déploiement à l’autre (en fonction du pays, de la version de l’application, etc.). Dans l’application CDBookStore, par exemple, ItemConverterEJB (voir Listing 7.14) convertit le prix d’un article dans la monnaie du pays dans lequel l’application a été déployée (en appliquant un taux de change par rapport au dollar). Si ce bean sans état est déployé en Europe, le prix de l’article doit être multiplié par 0,8 et le nom de la monnaie doit être l’euro. Listing 7.14 : Bean de session sans état convertissant des prix en euros @Stateless public class ItemConverterEJB { public Item convertPrice(Item item) { item.setPrice(item.getPrice() * 0.80); item.setCurrency("Euros"); return item; } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 251
Comme vous l’aurez compris, coder en dur ces paramètres implique de modifier le code, de le recompiler et de redéployer le composant pour chaque pays ayant une monnaie différente. Une autre possibilité consisterait à utiliser une base de données à chaque appel de la méthode convertPrice(), mais cela gaspillerait des ressources. En réalité, on veut simplement stocker ces paramètres à un endroit où ils pourront être modifiés lors du déploiement : le descripteur de déploiement est donc un emplacement de choix. Avec EJB 3.1, le descripteur de déploiement (ejb-jar.xml) est facultatif, mais son utilisation est justifiée lorsque l’on a des paramètres liés à l’environnement. Ces entrées peuvent en effet être placées dans le fichier et être accessibles via l’injection de dépendances (ou par JNDI). Elles peuvent être de type String, Character, Byte, Short, Integer, Long, Boolean, Double et Float. Le Listing 7.15, par exemple, montre que le fichier ejb-jar.xml d’ItemConverterEJB définit deux entrées : currencyEntry, de type String et de valeur Euros, et changeRateEntry, de type Float et de valeur 0.80. Listing 7.15 : Entrées d’environnement d’ItemConverterEJB dans ejb-jar.xml ItemConverterEJB com.apress.javaee6.ItemConverterEJB currencyEntry java.lang.String Euros changeRateEntry java.lang.Float 0.80
Maintenant que les paramètres de l’application ont été externalisés dans le descripteur de déploiement, ItemConverterEJB peut utiliser l’injection de dépendances pour obtenir leurs valeurs. Dans le Listing 7.16, @Resource(name = "currencyEntry") injecte la valeur de l’entrée currencyEntry dans l’attribut currency ; si les types de l’entrée et de l’attribut ne sont pas compatibles, le conteneur lève une exception.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
252
Java EE 6 et GlassFish 3
Listing 7.16 : Un ItemConverterEJB utilisant des entrées d’environnement @Stateless public class ItemConverterEJB { @Resource(name = "currencyEntry") private String currency; @Resource(name = "changeRateEntry") private Float changeRate; public Item convertPrice(Item item) { item.setPrice(item.getPrice() * changeRate); item.setCurrency(currency); return item; } }
Appels asynchrones
Par défaut, les appels des beans de session via des vues distantes, locales et sans interface sont synchrones : un client qui appelle une méthode reste bloqué pendant la durée de cet appel. Un traitement asynchrone est donc souvent nécessaire lorsque l’application doit exécuter une opération qui dure longtemps. L’impression d’une commande, par exemple, peut prendre beaucoup de temps si des dizaines de documents sont déjà dans la file d’attente de l’imprimante, mais un client qui appelle une méthode d’impression d’un document souhaite simplement déclencher un processus qui imprimera ce document, puis continuer son traitement. Avant EJB 3.1, les traitements asynchrones pouvaient être pris en charge par JMS et les MDB (voir Chapitre 13). Il fallait créer des objets administrés (fabriques et destinations JMS), utiliser l’API JMS de bas niveau afin d’envoyer un message à un destinataire, puis développer un MDB pour consommer et traiter le message. Cela fonctionnait mais se révélait assez lourd dans la plupart des cas car il fallait mettre en œuvre un système MOM pour simplement appeler une méthode de façon asynchrone. Depuis EJB 3.1, on peut appeler de façon asynchrone une méthode de bean de session en l’annotant simplement avec @javax.ejb.Asynchronous. Le Listing 7.17 montre le bean OrderEJB, qui dispose d’une méthode pour envoyer un e-mail à un client et d’une autre pour imprimer la commande. Ces deux méthodes durant longtemps, elles sont toutes les deux annotées par @Asynchronous.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 253
Listing 7.17 : Le bean OrderEJB déclare deux méthodes asynchrones @Stateless public class OrderEJB { @Asynchronous private void sendEmailOrderComplete(Order order, Customer customer) { // Envoie un mail }
Lorsqu’un client appelle printOrder() ou sendEmailOrderComplete(), le conteneur lui redonne immédiatement le contrôle et continue le traitement de cet appel dans un thread séparé. Comme vous pouvez le voir dans le Listing 7.17, le type du résultat de ces deux méthodes est void, mais une méthode asynchrone peut également renvoyer un objet de type java.util.concurrent.Future, où V représente la valeur du résultat. Les objets Future permettent d’obtenir le résultat d’une méthode qui s’exécute dans un thread distinct : le client peut alors utiliser l’API de Future pour obtenir ce résultat ou annuler l’appel. Le Listing 7.18 montre un exemple de méthode renvoyant un objet Future : sendOrderToWorkflow() utilise un workflow pour traiter un objet Order. Supposons qu’elle appelle plusieurs composants d’entreprise (messages, services web, etc.) et que chaque étape renvoie un code d’état (un entier) : lorsque le client appelle de façon asynchrone la méthode sendOrderToWorkflow(), il s’attend donc à recevoir le code d’état du workflow, qu’il peut récupérer par un appel à la méthode Future. get(). Si, pour une raison ou pour une autre, il souhaite annuler l’appel, il peut utiliser Future.cancel(), auquel cas le conteneur tentera d’annuler l’appel asynchrone s’il n’a pas encore démarré. Notez que la méthode sendOrderToWorkflow() appelle SessionContext.wasCancelCalled() pour tester si le client a demandé ou non l’annulation de l’appel. Le résultat est de type javax.ejb.AsyncResult, qui est une implémentation pratique de Future. En fait, AsyncResult est utilisée pour passer la valeur du résultat au conteneur au lieu de la passer directement à l’appelant. Listing 7.18 : Méthode asynchrone renvoyant un Future @Stateless @Asynchronous public class OrderEJB {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
254
Java EE 6 et GlassFish 3
@Resource SessionContext ctx; private void sendEmailOrderComplete(Order order, Customer customer) { // Envoie un mail } private void printOrder(Order order) { // Imprime une commande } private Future sendOrderToWorkflow(Order order) { Integer status = 0; // Traitement status = 1; if (ctx.wasCancelCalled()) { return new AsyncResult(2);
}
}
// Traitement return new AsyncResult(status);
Dans le Listing 7.18, vous remarquerez que l’annotation @Asynchronous est appliquée au niveau de la classe, ce qui implique que toutes les méthodes définies sont asynchrones. Pour récupérer la valeur du résultat d’un appel à sendOrderToWorkflow(), le client devra appeler Future.get() : Future status = orderEJB.sendOrderToWorkflow (order); Integer statusValue = status.get();
Conteneurs intégrés
L’avantage des beans de session est qu’ils sont gérés par un conteneur : ce dernier s’occupe de tous les services (transaction, cycle de vie, asynchronisme, intercepteurs, etc.), ce qui permet au développeur de se concentrer sur le code métier. L’inconvénient est qu’il faut toujours exécuter les EJB dans un conteneur, même pour les tester. Pour résoudre ce problème, on finit généralement par bricoler le code métier afin de pouvoir le tester : on ajoute des interfaces distantes alors que l’EJB n’a besoin que d’un accès local, on crée une façade TestEJB distante qui délègue les appels aux véritables EJB ou l’on utilise des fonctionnalités spécifiques à un éditeur – d’une façon ou d’une autre, on doit exécuter un conteneur avec les EJB déployés. Ce problème a été résolu par EJB 3.1 avec la création d’un conteneur EJB intégrable. EJB 3.1 ajoute en effet une API standardisée pour exécuter les EJB dans un environnement Java SE. Celle-ci (l’API javax.ejb.embeddable) permet à un client
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 255
d’instancier un conteneur EJB qui s’exécutera dans sa propre JVM. Ce conteneur fournit un environnement géré disposant des mêmes services de base que ceux d’un conteneur Java EE, mais il ne fonctionne qu’avec l’API EJB Lite (pas de MDB, pas de beans entités 2.x, etc.). Le Listing 7.19 montre une classe JUnit qui utilise l’API de "bootstrap" pour lancer le conteneur (la classe abstraite javax.ejb.embeddable.EJBContainer), rechercher un EJB et appeler ses méthodes. Listing 7.19 : Classe de test utilisant un conteneur intégré public class ItemEJBTest { private static EJBContainer ec; private static Context ctx; @BeforeClass public static void initContainer() throws Exception { ec = EJBContainer.createEJBContainer(); ctx = ec.getContext(); } @AfterClass public static void closeContainer() throws Exception { ec.close(); } @Test public void createBook() throws Exception { // Création d’un livre Book book = new Book(); book.setTitle("The Hitchhiker’s Guide to the Galaxy"); book.setPrice(12.5F); book.setDescription("Science fiction comedy book"); book.setIsbn("1-84023-742-2"); book.setNbOfPage(354); book.setIllustrations(false); // Recherche de l’EJB ItemEJB bookEJB = (ItemEJB) „ ctx.lookup("java:global/chapter07/ItemEJB"); // Stockage du livre dans la base de données book = itemEJB.createBook(book); assertNotNull("ID should not be null", book.getId()); // Récupère tous les livres de la base List books = itemEJB.findBooks(); assertNotNull(books); } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
256
Java EE 6 et GlassFish 3
Comme vous pouvez le constater dans la méthode initContainer() du Listing 7.19, EJBContainer dispose d’une méthode fabrique (createEJBContainer()) permettant de créer une instance de conteneur. Par défaut, le conteneur intégré recherche les EJB à initialiser dans le classpath du client. Lorsque le conteneur a été initialisé, la méthode récupère son contexte (EJBContainer.getContext(), qui renvoie un javax.naming.Context) pour rechercher le bean ItemEJB (en utilisant la syntaxe des noms portables JNDI). Notez qu’ItemEJB (voir Listing 7.1) est un bean de session sans état qui expose ses méthodes via une vue sans interface. Il utilise l’injection, les transactions gérées par le conteneur et une entité JPA Book. Le conteneur intégré s’occupe d’injecter un gestionnaire d’entités et de valider ou d’annuler les transactions. La méthode closeContainer() appelle simplement EJBContainer.close() pour fermer l’instance du conteneur intégré. Nous nous sommes servis d’une classe de test dans cet exemple pour vous montrer comment utiliser un conteneur EJB intégré, mais les EJB peuvent désormais être employés dans n’importe quel environnement Java SE : des classes de tests aux applications Swing en passant par une simple classe Java avec une méthode public static void main().
Le service timer Certaines applications Java EE ont besoin de planifier des tâches pour être averties à des instants donnés. L’application CD-BookStore, par exemple, veut envoyer tous les ans un e-mail d’anniversaire à ses clients, afficher les statistiques mensuelles des ventes, produire des rapports toutes les nuits sur l’état du stock et rafraîchir un cache toutes les 30 secondes. Pour ce faire, EJB 2.1 a introduit un service timer car les clients ne pouvaient pas utiliser directement l’API Thread, mais il était moins riche que d’autres outils ou certains frameworks (l’utilitaire cron d’Unix, Quartz, etc.). Il a fallu attendre EJB 3.1 pour voir apparaître une amélioration considérable de ce service, qui s’est inspiré de cron et d’autres outils reconnus. Désormais, il peut répondre à la plupart des besoins de planification. Le service timer EJB est un service conteneur qui permet aux EJB de s’enregistrer pour être rappelés. Les notifications peuvent être planifiées pour intervenir à une date ou une heure données, après un certain délai ou à intervalles réguliers.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 257
Le conteneur mémorise tous les timers et appelle la méthode d’instance appropriée lorsqu’un timer a expiré. La Figure 7.7 montre les deux étapes de l’utilisation de ce service. L’EJB doit d’abord créer un timer (automatiquement ou explicitement) et s’enregistrer pour être rappelé, puis le service timer déclenche la méthode d’instance enregistrée de l’EJB. Conteneur EJB Enregistrement automatique ou explicite
Service timer
Prévient le bean à l'expiration du timer
Bean de session
Figure 7.7 Interaction entre le service timer et un bean de session.
Les timers sont destinés aux processus métiers longs et ils sont donc persistants par défaut, ce qui signifie qu’ils survivent aux arrêts du serveur : lorsqu’il redémarre, les timers s’exécutent comme s’il ne s’était rien passé. Selon vos besoins, vous pouvez également demander des timers non persistants. INFO Le service de timer peut enregistrer des beans sans état et singletons ainsi que des MDB, mais pas de beans avec état. Ces derniers ne doivent donc pas utiliser l’API de planification.
Les timers peuvent être créés automatiquement par le conteneur au moment du déploiement si le bean comprend des méthodes annotées par @Schedule. Ils peuvent également être créés explicitement par programme et doivent fournir une méthode de rappel annotée par @Timeout. Expressions calendaires
Le service timer utilise une syntaxe calendaire inspirée de celle du programme cron d’Unix. Cette syntaxe est utilisée pour la création des timers par programme (avec la classe ScheduleExpression) ou pour les créations automatiques (via l’annotation @Schedule ou le descripteur de déploiement). Le Tableau 7.2 énumère les attributs de création des expressions calendaires.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
258
Java EE 6 et GlassFish 3
Tableau 7.2 : Attributs d’une expression calendaire
Valeur par défaut
Attribut
Description
Valeurs possibles
second
Une ou plusieurs secondes dans une minute
[0,59]
0
minute
Une ou plusieurs minutes dans une heure
[0,59]
0
hour
Une ou plusieurs heures dans une journée
[0,23]
0
dayOfMonth
Un ou plusieurs jours dans un mois
[1,31] ou {"1st", "2nd", "3rd", . . . ,"30th", "31st"} ou {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} ou "Last" (le dernier jour du mois) ou -x (x
[0,7] ou {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} – "0" et "7"
*
signifient Dimanche year
Une année particulière
Une année sur quatre chiffres
timezone
Une zone horaire particulière
Liste des zones horaires fournies par la base de données zoneinfo (ou tz)
*
Chaque attribut d’une expression calendaire (second, minute, hour, etc.) permet d’exprimer les valeurs sous différentes formes. Vous pouvez, par exemple, avoir une liste de jours ou un intervalle entre deux années. Le Tableau 7.3 présente les différentes formes que peut prendre un attribut.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 259
Tableau 7.3 : Forme d’une expression calendaire
Forme
Description
Exemple
Valeur simple
Une seule valeur possible
year = "2009" month= "May"
Joker
Toutes les valeurs possibles d’un attribut
second = "*" dayOfWeek = "*"
Liste
Deux valeurs ou plus séparées par une virgule
year = "2008,2012,2016" dayOfWeek = "Sat,Sun" minute = "0-10,30,40"
Intervalle
Intervalle de valeurs séparées par un tiret
second="1-10" dayOfWeek = "Mon-Fri"
Incréments
Un point de départ et un intervalle séparés par une barre de fraction
minute = "*/15" second = "30/10"
Cette syntaxe devrait sembler familière à ceux qui connaissent cron, mais elle est bien plus simple. Comme le montre le Tableau 7.4, elle permet d’exprimer quasiment n’importe quel type d’expression calendaire. Tableau 7.4 : Exemples d’expressions calendaires
Trois jours avant le dernier jour de chaque mois à 13 h 00
hour="13", dayOfMonth="-3"
Toutes les deux heures à partir de midi le second mardi de chaque mois
hour="12/2", dayOfMonth="2nd Tue"
Toutes les 14 minutes de 1 h 00 et 2 h 00
minute = "*/14", hour="1,2"
Toutes les 14 minutes de 1 h 00 et 2 h 00
minute = "0,14,28,42,56", hour = "1,2"
Toutes les 10 secondes à partir de la 30e seconde
second = "30/10"
Toutes les 10 secondes à partir de la 30e seconde
second = "30,40,50"
Création automatique d’un timer
Le conteneur peut créer automatiquement les timers au moment du déploiement en utilisant les métadonnées. Il crée un timer pour chaque méthode annotée par @javax. ejb.Schedule ou @Schedules (ou leur équivalent XML dans le descripteur de déploiement ejb-jar.xml). Par défaut, chaque annotation @Schedule correspond à un seul timer persistant, mais il est également possible de définir des timers non persistants. Le Listing 7.20 montre un bean StatisticsEJB qui définit plusieurs méthodes : statisticsItemsSold() crée un timer qui appellera la méthode le premier jour de chaque mois à 05 h 30 ; generateReport() crée deux timers (avec @Schedules) : l’un pour chaque jour à 02 h 00, l’autre pour chaque mercredi à 14 h 00 ; refreshCache() crée un timer non persistant qui rafraîchit le cache toutes les 10 minutes. Listing 7.20 : Le bean StatisticsEJB enregistre quatre timers @Stateless public class StatisticsEJB { @Schedule(dayOfMonth = "1", hour = "5", minute = "30")
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Pour créer un timer par programme, l’EJB doit accéder à l’interface javax. ejb.TimerService en utilisant soit l’injection de dépendances, soit l’EJBContext (EJBContext.getTimerService()), soit une recherche JNDI. L’API TimerService définit plusieurs méthodes permettant de créer quatre sortes de timers : crée un timer reposant sur des dates, des intervalles ou des durées. Ces méthodes n’utilisent pas les expressions calendaires.
■■ createTimer
crée un timer simple-action qui expire à un instant donné ou après une certaine durée. Le conteneur supprime le timer après l’appel à la méthode de rappel.
■■ createSingleActionTimer
crée un timer intervalle dont la première expiration intervient à un instant donné et les suivantes, après les intervalles indiqués.
■■ createIntervalTimer
■■ createCalendarTimer crée un timer utilisant les expressions calendaires à l’aide
de la classe ScheduleExpression. La classe ScheduleExpression permet de créer des expressions calendaires par programme. Ses méthodes sont liées aux attributs du Tableau 7.2 et permettent de programmer tous les exemples du Tableau 7.4. Voici quelques exemples : new ScheduleExpression().dayOfMonth("Mon").month("Jan"); new ScheduleExpression().second("10,30,50").minute("*/5"). „ hour("10-14"); new ScheduleExpression().dayOfWeek("1,5"). „ timezone("Europe/Lisbon");
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
262
Java EE 6 et GlassFish 3
Toutes les méthodes de TimerService (createSingleActionTimer, createCalendarTimer, etc.) renvoient un objet Timer contenant des informations sur le timer créé (date de création, persistant ou non, etc.). Timer permet également à l’EJB d’annuler le timer avant son expiration. Lorsque le timer expire, le conteneur appelle la méthode annotée par @Timeout correspondante du bean en lui passant l’objet Timer. Un bean ne peut pas posséder plus d’une méthode @Timeout. Lorsque CustomerEJB (voir Listing 7.21) ajoute un nouveau client au système (avec la méthode createCustomer()), il crée également un timer calendaire reposant sur la date de naissance de ce client : chaque année, le conteneur pourra ainsi déclencher un bean pour créer et envoyer un courrier électronique afin de souhaiter l’anniversaire du client. Pour ce faire, le bean sans état doit d’abord injecter une référence au service timer (avec @Resource). La méthode createCustomer() stocke le client dans la base de données et utilise le jour et le mois de sa naissance pour créer un objet ScheduleExpression qui sert ensuite à créer un timer calendaire avec TimerConfig – l’appel à new TimerConfig(customer, true) configure un timer persistant (indiqué par son paramètre true) qui passe l’objet customer représentant le client. Listing 7.21 : Le bean CustomerEJB crée explicitement un timer @Stateless public class CustomerEJB { @Resource TimerService timerService; @PersistenceContext(unitName = "chapter07PU") private EntityManager em; public void createCustomer(Customer customer) { em.persist(customer); ScheduleExpression birthDay = new ScheduleExpression(). „ dayOfMonth(customer.getBirthDay()). „ month(customer.getBirthMonth()); timerService.createCalendarTimer(birthDay, new TimerConfig(customer, true)); } @Timeout public void sendBirthdayEmail(Timer timer) { Customer customer = (Customer) timer.getInfo(); // ... } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 7
Beans de session et service timer 263
Une fois le timer créé, le conteneur invoquera tous les ans la méthode @Timeout (sendBirthdayEmail()) en lui passant l’objet Timer. Le timer ayant été sérialisé avec l’objet customer, la méthode peut y accéder en appelant getinfo().
Résumé Ce chapitre a été consacré aux beans de session et au service timer (les MDB seront présentés au Chapitre 13, les services web SOAP, au Chapitre 14 et les services web REST, au Chapitre 15). Les beans de session sont des composants gérés par un conteneur qui permettent de développer des couches métiers. Il existe trois types de beans de session : sans état, avec état et singletons. Les beans sans état s’adaptent facilement car ils ne mémorisent aucune information, sont placés dans un pool et traitent les tâches qui peuvent être réalisées par un seul appel de méthode. Les beans avec état sont en relation 1–1 avec un client et peuvent être temporairement ôtés de la mémoire grâce aux mécanismes de passivation et d’activation. Les singletons n’ont qu’une seule instance partagée par plusieurs clients et peuvent être initialisés au lancement de l’application, chaînés ensemble ; en outre, leurs accès concurrents peuvent s’adapter en fonction des besoins. Malgré ces différences, tous les beans de session partagent le même modèle de programmation. Ils peuvent avoir une vue locale, distante ou sans interface, utiliser des annotations ou être déployés avec un descripteur de déploiement. Les beans de session peuvent utiliser l’injection de dépendances pour obtenir des références à plusieurs ressources (sources de données JDBC, contexte persistant, entrées d’environnement, etc.) ; ils peuvent également se servir de leur contexte d’environnement (l’objet SessionContext). Depuis EJB 3.1, vous pouvez appeler des méthodes de façon asynchrone, rechercher les EJB à l’aide de noms JNDI portables ou utiliser un conteneur EJB intégré dans l’environnement Java SE. EJB 3.1 a également amélioré le service timer, qui peut désormais rivaliser avec les autres outils de planification. Le chapitre suivant présente le cycle de vie des beans de session et explique comment interagir avec les annotations de rappel. Les intercepteurs, qui permettent de mettre en œuvre la programmation orientée aspect (POA) avec les beans de session, y sont également détaillés.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
8 Méthodes de rappel et intercepteurs Le chapitre précédent a montré que les beans sont des composants gérés par un conteneur. Ils résident dans un conteneur EJB qui encapsule le code métier avec plusieurs services (injection de dépendances, gestion des transactions, de la sécurité, etc.). La gestion du cycle de vie et l’interception font également partie de ces services. Le cycle de vie signifie qu’un bean de session passe par un ensemble d’états bien précis, qui dépendent du type de bean (sans état, avec état ou singleton). À chaque phase de ce cycle. le conteneur peut invoquer les méthodes qui ont été annotées comme méthodes de rappel. Vous pouvez utiliser ces annotations pour initialiser les ressources de vos beans de session ou pour les libérer avant leur destruction. Les intercepteurs permettent d’ajouter des traitements transverses à vos beans. Lorsqu’un client appelle une méthode d’un bean de session, le conteneur peut intercepter l’appel et traiter la logique métier avant que la méthode du bean ne soit invoquée. Ce chapitre présente les différents cycles de vie des beans de session, ainsi que les annotations de rappel que vous pouvez utiliser pour traiter la logique métier au cours des différentes phases. Nous verrons également comment intercepter les appels de méthodes et les encapsuler par notre propre code.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
266
Java EE 6 et GlassFish 3
Cycles de vie des beans de session Comme nous l’avons vu au chapitre précédent, un client ne crée pas une instance d’un bean de session à l’aide de l’opérateur new : il obtient une référence à ce bean via l’injection de dépendances ou par une recherche JNDI. C’est le conteneur qui crée l’instance et qui la détruit, ce qui signifie que ni le client ni le bean ne sont responsables du moment où l’instance est créée, où les dépendances sont injectées et où l’instance est supprimée. La responsabilité de la gestion du cycle de vie du bean incombe au conteneur. Tous les beans de session passent par deux phases évidentes de leur cycle de vie : leur création et leur destruction. En outre, les beans avec état passent par les phases de passivation et d’activation que nous avons décrites au chapitre précédent. Beans sans état et singletons
Les beans sans état et singletons partagent la caractéristique de ne pas mémoriser l’état conversationnel avec leur client et d’autoriser leur accès par n’importe quel client – les beans sans état le font en série, instance par instance, alors que les singletons fournissent un accès concurrent à une seule instance. Tous les deux partagent le cycle de vie suivant, représenté par la Figure 8.1 : 1. Le cycle de vie commence lorsqu’un client demande une référence au bean (par injection de dépendances ou par une recherche JNDI). Le conteneur crée alors une nouvelle instance de bean de session. 2. Si cette nouvelle instance utilise l’injection de dépendances via des annotations (@Resource, @EJB, @PersistenceContext, etc.) ou des descripteurs de déploiement, le conteneur injecte toutes les ressources nécessaires. 3. Si l’instance contient une méthode annotée par l’appelle.
@PostConstruct,
le conteneur
4. L’instance traite l’appel du client et reste prête pour traiter les appels suivants. Les beans sans état restent prêts jusqu’à ce que le conteneur libère de la place dans le pool, les singletons restent prêts jusqu’à la terminaison du conteneur. 5. Le conteneur n’a plus besoin de l’instance. Si celle-ci contient une méthode annotée par @PreDestroy, il l’appelle et met fin à l’instance.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Figure 8.1 Cycle de vie des beans sans état et singletons.
Méthodes de rappel et intercepteurs 267
N'existe pas 1. Nouvelle instance 2. Injection de dépendances 3. @PostConstruct
5. @PreDestroy
Prêt
4. Appel de méthode
Bien qu’ils partagent le même cycle de vie, les beans sans état et singletons ne sont pas créés et détruits de la même façon. Lorsqu’un bean de session sans état est déployé, le conteneur en crée plusieurs instances et les place dans un pool. Quand un client appelle une méthode de ce bean, le conteneur choisit une instance dans le pool, lui délègue l’appel de méthode et la replace dans le pool. Lorsque le conteneur n’a plus besoin de l’instance (parce que, par exemple, il veut réduire le nombre d’instances du pool), il la supprime. INFO GlassFish permet de paramétrer le pool des EJB. Vous pouvez ainsi fixer une taille (nombre initial, minimal et maximal de beans dans le pool), le nombre de beans à supprimer du pool lorsque son temps d’inactivité a expiré et le nombre de millisecondes du délai d’expiration du pool.
La création des beans singletons varie selon qu’ils ont été instanciés dès le démarrage (@Startup) ou non, ou qu’ils dépendent (@DependsOn) d’un autre singleton déjà créé : dans ce cas, une instance sera créée au moment du déploiement ; sinon le conteneur créera l’instance lorsqu’un client appellera une méthode métier. Comme les singletons durent tout le temps de l’application, leur instance n’est détruite que lorsque le conteneur se termine. Beans avec état
Du point de vue du programme, les beans de session avec état ne sont pas très différents des beans sans état ou singletons : seules leurs métadonnées changent (@Stateful au lieu de @Stateless ou @Singleton). La véritable différence réside dans le fait que les beans avec état mémorisent l’état conversationnel avec leurs
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
268
Java EE 6 et GlassFish 3
clients et qu’ils ont donc un cycle de vie légèrement différent. Le conteneur produit une instance et ne l’affecte qu’à un seul client. Ensuite, chaque requête de ce client sera transmise à la même instance. Selon ce principe et en fonction de l’application, il peut finalement s’établir une relation 1–1 entre un client et un bean avec état (un millier de clients simultanés peuvent produire un millier de beans avec état). Si un client n’invoque pas son instance de bean au cours d’une période suffisamment longue, le conteneur doit le supprimer avant que la JVM ne soit à court de mémoire, préserver l’état de cette instance dans une zone de stockage permanente, puis la rappeler lorsque son état redevient nécessaire. Pour ce faire, le conteneur utilise le mécanisme de passivation et activation. Comme on l’a expliqué au Chapitre 7, la passivation consiste à sérialiser l’instance du bean sur un support de stockage permanent (fichier sur disque, base de données, etc.) au lieu de la maintenir en mémoire. L’activation, qui est l’opération opposée, a lieu lorsque l’instance est redemandée par le client. Le conteneur désérialise alors le bean et le replace en mémoire. Ceci signifie donc que les attributs du bean doivent être sérialisables (donc être d’un type Java primitif ou qui implémente l’interface java.io.Serializable). Le cycle de vie d’un bean avec état passe donc par les étapes suivantes, qui sont représentées par la Figure 8.2 : 1. Le cycle de vie démarre lorsqu’un client demande une référence au bean (soit par injection de dépendances, soit par une recherche JNDI) : le conteneur crée alors une nouvelle instance du bean de session et la stocke en mémoire. 2. Si la nouvelle instance utilise l’injection de dépendances via des annotations (@Resource, @EJB, @PersistenceContext, etc.) ou des descripteurs de déploiement, le conteneur injecte les ressources nécessaires. 3. Si l’instance contient une méthode annotée par l’appelle.
@PostConstruct,
le conteneur
4. Le bean exécute l’appel demandé et reste en mémoire en attente d’autres requêtes du client. 5. Si le client reste inactif pendant un certain temps, le conteneur appelle la méthode annotée par @PrePassivate, s’il y en a une, et stocke le bean sur un support de stockage permanent. 6. Si le client appelle un bean qui a été passivé, le conteneur le replace en mémoire et appelle la méthode annotée par @PostActivate, s’il y en a une. 7. Si le client n’invoque pas une instance passivée avant la fin du délai d’expiration de la session, le conteneur supprime cette instance.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Méthodes de rappel et intercepteurs 269
8. Alternative à l’étape 7 : si le client appelle une méthode annotée par @Remove, le conteneur invoque alors la méthode annotée par @PreDestroy, s’il y en a une, et met fin au cycle de vie de l’instance. Figure 8.2
7. Expiration du délai
N'existe pas
Cycle de vie d’un bean avec état.
1. Nouvelle instance 2. Injection de dépendances 3. @PostConstruct
8. @Remove et @Predestroy 6. PostActivate Prêt
Passivé
5. @PrePassivate
4. Appel de méthode
Dans certains cas, un bean avec état contient des ressources ouvertes comme des sockets ou des connexions de bases de données. Un conteneur ne pouvant garder ces ressources ouvertes pour chaque bean, vous devez fermer et rouvrir ces ressources avant et après la passivation : c’est là que les méthodes de rappel interviennent. Méthodes de rappel
Comme nous venons de le voir, le cycle de vie de chaque bean de session est géré par son conteneur. Ce dernier permet de greffer du code métier aux différentes phases de ce cycle : les passages d’un état à l’autre sont alors interceptés par le conteneur, qui appellera les méthodes annotées par l’une des annotations du Tableau 8.1. Tableau 8.1 : Annotations de rappel du cycle de vie
Annotation
Description
@PostConstruct
Indique la méthode à appeler immédiatement après la création de l’instance et l’injection de dépendances par le conteneur. Cette méthode sert le plus souvent à réaliser les initialisations.
@PreDestroy
Indique la méthode à appeler immédiatement avant la suppression de l’instance par le conteneur. Cette méthode sert le plus souvent à libérer les ressources utilisées par le bean. Dans le cas des beans avec état, cette méthode est appelée après la fin de l’exécution de la méthode annotée par @Remove.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
270
Java EE 6 et GlassFish 3
Annotation
Description
@PrePassivate
Indique la méthode à appeler avant que le conteneur passive l’instance. Elle donne généralement l’occasion au bean de se préparer à la sérialisation et de libérer les ressources qui ne peuvent pas être sérialisées (connexions à une base de données, serveur de message, socket réseau, etc.).
@PostActivate
Indique la méthode à appeler immédiatement après la réactivation de l’instance par le conteneur. Elle lui permet de réinitialiser les ressources qu’il a fermées au cours de la passivation.
INFO Les annotations @PrePassivate et @PostActivate sont définies dans le paquetage javax. ejb et font partie de la spécification EJB 3.1 (JSR 318). @PostConstruct et @PreDestroy font partie de la spécification Common Annotations 1.0 (JSR 250) et proviennent du paquetage javax.annotation (comme @Resource et les annotations concernant la sécurité, que nous
présenterons au chapitre suivant).
Une méthode de rappel doit avoir la signature suivante : void ();
et respecter les règles suivantes : ■■
Elle ne doit pas prendre de paramètres et doit renvoyer void.
■■
Elle ne doit pas lancer d’exception contrôlée, mais elle peut déclencher une exception runtime : dans ce cas, si une transaction est en cours, celle-ci sera annulée (voir chapitre suivant).
■■
Elle peut avoir un accès public, private, mais ne peut être ni static ni final.
■■
Elle peut être annotée par plusieurs annotations (la méthode init() du Listing 8.2, par exemple, est annotée par @PostConstruct et @PostActivate). Cependant, il ne peut y avoir qu’une seule annotation du même type dans le bean (il ne peut pas exister deux annotations @PostConstruct dans le même bean de session, par exemple).
■■
Elle peut accéder aux entrées d’environnement du bean (voir la section "Contexte de nommage de l’environnement" du Chapitre 7).
protected
ou de niveau paquetage,
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Méthodes de rappel et intercepteurs 271
Ces méthodes de rappel servent généralement à allouer et/ou à libérer les ressources du bean. Le Listing 8.1, par exemple, montre que le bean singleton CacheEJB utilise une annotation @PostConstruct pour initialiser son cache : immédiatement après la création de l’unique instance de ce bean, le conteneur invoquera donc la méthode initCache(). Listing 8.1 : Singleton initialisant son cache avec l’annotation @PostConstruct @Singleton public class CacheEJB { private Map cache = new HashMap(); @PostConstruct private void initCache() { // Initialise le cache } public Object getFromCache(Long id) { if (cache.containsKey(id)) return cache.get(id); else return null; } }
Le Listing 8.2 présente un extrait de code pour un bean avec état. Le conteneur gère l’état conversationnel, qui peut contenir des ressources importantes comme une connexion à une base de données. L’ouverture d’une telle connexion étant coûteuse, elle devrait être partagée par tous les appels, mais libérée lorsque le bean est inactif (ou passivé). Après la création de l’instance du bean, le conteneur injecte la référence d’une source de données dans l’attribut ds. Il pourra ensuite appeler la méthode annotée par @PostConstruct (init()), qui crée une connexion vers une base de données. Si le conteneur passive l’instance, la méthode close() (annotée par @PrePassivate) sera d’abord invoquée afin de fermer la connexion JDBC, qui ne sert plus pendant la passivation. Lorsque le client appelle une méthode métier du bean, le conteneur l’active et appelle à nouveau la méthode init() (car elle est également annotée par @PostActivate). Si le client invoque la méthode checkout() (annotée par @Remove), le conteneur supprime l’instance après avoir appelé à nouveau la méthode close() (car elle est aussi annotée par @PreDestroy).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
272
Java EE 6 et GlassFish 3
Listing 8.2 : Bean avec état, initialisant et libérant des ressources @Stateful public class ShoppingCartEJB { @Resource private DataSource ds; private Connection connection; private List cartItems = new ArrayList(); @PostConstruct @PostActivate private void init() { connection = ds.getConnection(); } @PreDestroy @PrePassivate private void close() { connection.close(); } // ... @Remove public void checkout() { cartItems.clear(); } }
Pour plus de lisibilité, la gestion des exceptions SQL a été omise dans les méthodes de rappel.
Intercepteurs Avant de présenter les intercepteurs, passons un peu de temps à évoquer la programmation orientée aspect (POA). La POA est un paradigme de programmation qui sépare les traitements transverses (ceux qui apparaissent partout dans l’application) du code métier. La plupart des applications contiennent du code qui se répète dans tous les composants. Il peut s’agir de traitements techniques (enregistrer l’entrée et la sortie de chaque méthode, la durée d’un appel de méthode, les statistiques d’utilisation d’une méthode, etc.) ou de traitements métiers (effectuer des vérifications supplémentaires si un client achète pour plus de 10 000 € d’articles, envoyer une demande de réapprovisionnement lorsque l’inventaire est trop bas, etc.). Avec la POA, ces traitements peuvent s’appliquer automatiquement à toute l’application ou uniquement à un sous-ensemble de celle-ci.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Méthodes de rappel et intercepteurs 273
Les EJB permettent d’utiliser la POA en fournissant la possibilité d’intercepter les appels de méthodes à l’aide d’intercepteurs, qui seront automatiquement déclenchés par le conteneur lorsqu’une méthode EJB est invoquée. Comme le montre la Figure 8.3, les intercepteurs peuvent être chaînés et sont appelés avant et/ou après l’exécution d’une méthode. INFO Les intercepteurs s’appliquent aux beans de session et aux beans pilotés par messages (MDB). Aux Chapitres 14 et 15, nous verrons qu’un service web SOAP ou REST peut également être implémenté comme un EJB (en ajoutant l’annotation @Stateless). En ce cas, ces services web peuvent également utiliser des intercepteurs.
La Figure 8.3 montre que plusieurs intercepteurs sont appelés entre le client et l’EJB. En fait, vous pouvez considérer qu’un conteneur EJB est lui-même une chaîne d’intercepteurs : lorsque vous développez un bean de session, vous vous concentrez sur le code métier mais, en coulisse, le conteneur intercepte les appels de méthodes effectués par le client et applique différents services (gestion du cycle de vie, transactions, sécurité, etc.). Grâce aux intercepteurs, vous pouvez ajouter vos propres traitements transverses et les appliquer au code métier de façon transparente. Conteneur EJB Client
Intercepteur 1
Intercepteur n
Bean de session
Figure 8.3 Conteneur interceptant un appel et invoquant un intercepteur.
Il existe trois types d’intercepteurs (que nous décrirons dans la section suivante) : ■■
intercepteurs autour des appels ;
■■
intercepteurs des méthodes métiers ;
■■
intercepteurs des méthodes de rappel du cycle de vie.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
274
Java EE 6 et GlassFish 3
INFO La spécification EJB 3.1 (JSR 318) est composée de deux documents : la spécification EJB centrale et le document énonçant les besoins des intercepteurs. Ce dernier définit le fonctionnement des intercepteurs et la façon de les utiliser. La raison de cette séparation est liée au fait que, dans de prochaines versions, les intercepteurs pourraient être traités indépendamment des EJB.
Intercepteurs autour des appels
Le moyen le plus simple de définir un intercepteur consiste à ajouter une annotation @javax.interceptor.AroundInvoke (ou l’élément de déploiement ) dans le bean lui-même, comme dans le Listing 8.3. CustomerEJB annote la méthode logMethod(), qui sert à enregistrer un message lorsque l’on entre dans une méthode et un autre lorsqu’on en sort. Lorsque cet EJB est déployé, tous les appels aux méthodes createCustomer() ou findCustomerById() seront interceptés et le code de logMethod() s’appliquera. Notez que la portée de cet intercepteur est limitée au bean. Les intercepteurs autour des appels n’interviennent que dans la même transaction et dans le même contexte de sécurité que la méthode pour laquelle ils s’interposent. Listing 8.3 : CustomerEJB utilise un intercepteur @Stateless public class CustomerEJB { @PersistenceContext(unitName = "chapter08PU") private EntityManager em; private Logger logger = Logger.getLogger("com.apress.javaee6"); public void createCustomer(Customer customer) { em.persist(customer); } public Customer findCustomerById(Long id) { return em.find(Customer.class, id); } @AroundInvoke private Object logMethod(InvocationContext ic) throws Exception {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Une méthode intercepteur autour des appels doit respecter les règles suivantes : ■■
Elle peut être public, private, protected ou avoir un accès paquetage, mais ne peut pas être static ou final.
■■
Elle doit avoir un paramètre javax.interceptor.InvocationContext et renvoyer un Object, qui est le résultat de l’appel de la méthode cible (si cette méthode renvoie void, cet objet vaudra null).
■■
Elle peut lever une exception contrôlée.
L’objet InvocationContext permet aux intercepteurs de contrôler le comportement de la chaîne des appels. Lorsque plusieurs intercepteurs sont chaînés, c’est la même instance d’InvocationContext qui est passée à chacun d’eux, ce qui peut impliquer un traitement de ces données contextuelles par les autres intercepteurs. Le Tableau 8.2 décrit l’API d’InvocationContext. Tableau 8.2 : Définition de l’interface InvocationContext
Méthode
Description
getContextData
Permet de passer des valeurs entre les mêmes méthodes intercepteurs dans la même instance d’InvocationContext à l’aide d’une Map.
getMethod
Renvoie la méthode du bean pour laquelle l’intercepteur a été invoqué.
getParameters
Renvoie les paramètres qui seront utilisés pour invoquer la méthode métier.
getTarget
Renvoie l’instance du bean à laquelle appartient la méthode interceptée.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
276
Java EE 6 et GlassFish 3
Méthode
Description
getTimer
Renvoie le timer associé à une méthode @Timeout.
proceed
Appelle la méthode intercepteur suivante de la chaîne. Renvoie le résultat de la méthode suivante. Si une méthode est de type void, proceed renvoie null.
setParameters
Modifie la valeur des paramètres utilisés pour l’appel de la méthode cible. Si les types et le nombre de paramètres ne correspondent pas à la signature de la méthode, l’exception IllegalArgumentException est levée.
Pour expliquer le fonctionnement du code du Listing 8.3, examinons le diagramme de séquence de la Figure 8.4 pour voir ce qui se passe lorsqu’un client invoque la méthode createCustomer(). Tout d’abord, le conteneur intercepte cet appel et, au lieu d’exécuter directement createCustomer(), appelle la méthode logMethod(). Celle-ci utilise l’interface InvocationContext pour obtenir le nom du bean (ic. getTarget()) et de la méthode (ic.getMethod()) appelés afin de produire un message (logger.entering()). Puis logMethod() appelle la méthode InvocationContext.proceed(), qui lui indique qu’elle doit passer à l’intercepteur suivant ou appeler la méthode métier du bean. Cet appel est très important car, sans lui, la chaîne des intercepteurs serait rompue et la méthode métier ne serait pas appelée. Enfin, la méthode createCustomer() est finalement exécutée – lorsqu’elle se termine, l’intercepteur termine son exécution en enregistrant un message (logger. exiting()). La même séquence se serait produite si un client avait appelé la méthode findCustomerById(). Figure 8.4 Chaînage de différents intercepteurs.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Méthodes de rappel et intercepteurs 277
Intercepteurs de méthode
Le Listing 8.3 définit un intercepteur qui n’est disponible que pour CustomerEJB mais, la plupart du temps, on souhaite isoler un traitement transverse dans une classe distincte et demander au conteneur d’intercepter les appels de méthodes de plusieurs beans de session. L’enregistrement de journaux est un exemple typique de situation dans laquelle on veut enregistrer les entrées et les sorties de toutes les méthodes de tous les EJB. Pour disposer de ce type d’intercepteur, on doit créer une classe distincte et informer le conteneur de l’appliquer à un bean précis ou à une méthode de bean particulière. Le Listing 8.4 isole la méthode logMethod() du Listing 8.3 dans une classe à part, LoggingInterceptor, qui est un simple POJO disposant d’une méthode annotée par @AroundInvoke. Listing 8.4 : Classe intercepteur enregistrant l’entrée et la sortie d’une méthode public class LoggingInterceptor { private Logger logger = Logger.getLogger("com.apress.javaee6");
peut maintenant être utilisée de façon transparente par n’importe quel EJB souhaitant disposer d’un intercepteur. Pour ce faire, le bean doit informer le conteneur avec l’annotation @javax.interceptor.Interceptors. Dans le Listing 8.5, cette annotation est placée sur la méthode createCustomer(), ce qui signifie que tout appel à cette méthode sera intercepté par le conteneur qui invoquera la classe .LoggingInterceptor (pour enregistrer un message signalant l’entrée et la sortie de la méthode). LoggingInterceptor
Listing 8.5 : CustomerEJB utilise un intercepteur sur une méthode @Stateless public class CustomerEJB { @PersistenceContext(unitName = "chapter08PU")
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
public Customer findCustomerById(Long id) { return em.find(Customer.class, id); }
Dans le Listing 8.5, @Interceptors n’est attachée qu’à la méthode createCustomer(), ce qui signifie que le conteneur n’interceptera pas un appel à findCustomerById(). Si vous voulez que ces deux méthodes soient interceptées, vous pouvez placer l’annotation @Interceptors sur chacune de ces méthodes ou sur le bean lui-même (dans ce dernier cas, l’intercepteur sera déclenché pour toutes les méthodes du bean) : @Stateless @Interceptors(LoggingInterceptor.class) public class CustomerEJB { public void createCustomer(Customer customer) { ... } public Customer findCustomerById(Long id) { ... } }
Si vous voulez que toutes les méthodes, sauf une, soient interceptées, utilisez l’annotation javax.interceptor.ExcludeClassInterceptors pour exclure la méthode concernée. Dans le code suivant, l’appel à updateCustomer() ne sera pas intercepté alors que les appels à toutes les autres méthodes le seront : @Stateless @Interceptors(LoggingInterceptor.class) public class CustomerEJB { public void createCustomer(Customer customer) { ... } public Customer findCustomerById(Long id) { ... } public void removeCustomer(Customer customer) { ... } @ExcludeClassInterceptors public Customer updateCustomer(Customer customer) { ... } }
Intercepteur du cycle de vie
Dans la première partie de ce chapitre, nous avons vu comment gérer les méthodes de rappel dans un EJB. Avec une annotation de rappel, vous pouvez demander au conteneur d’appeler une méthode lors d’une phase précise du cycle de vie (@PostConstruct, @PrePassivate, @PostActivate et @PreDestroy). Si vous souhaitez, par exemple, ajouter une entrée dans un journal à chaque fois qu’une instance d’un bean
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Méthodes de rappel et intercepteurs 279
est créée, il suffit de placer l’annotation @PostConstruct sur une méthode du bean et d’ajouter un peu de code pour enregistrer l’entrée dans le journal. Mais comment faire pour capturer les événements du cycle de vie entre plusieurs types de beans ? Les intercepteurs du cycle de vie permettent d’isoler du code dans une classe et de l’invoquer lorsque l’un de ces événements se déclenche. Les intercepteurs du cycle de vie ressemblent à ce que nous venons de voir dans le Listing 8.4, sauf que les méthodes utilisent des annotations de rappel au lieu de @AroundInvoke. Le Listing 8.6 présente une classe ProfileInterceptor avec deux méthodes : logMethod(), qui sera appelée après la construction d’une instance et profile(), qui sera invoquée avant la destruction d’une instance. Listing 8.6 : Intercepteur du cycle de vie définissant deux méthodes public class ProfileInterceptor { private Logger logger = Logger.getLogger("com.apress.javaee6"); @PostConstruct public void logMethod(InvocationContext ic) { logger.entering(ic.getTarget().toString(), ic.getMethod().getName()); try { return ic.proceed(); } finally { logger.exiting(ic.getTarget().toString(), ic.getMethod().getName()); } } @PreDestroy public void profile(InvocationContext ic) { long initTime = System.currentTimeMillis(); try { return ic.proceed(); } finally { long diffTime = System.currentTimeMillis() - initTime; logger.fine(ic.getMethod() + " took " + diffTime + " millis"); } } }
Comme vous pouvez le voir dans le Listing 8.6, les méthodes intercepteurs du cycle de vie prennent en paramètre un objet InvocationContext, renvoient void au lieu d’Object (car, comme on l’a expliqué dans la section "Méthodes de rappel", les méthodes de rappel du cycle de vie renvoient void) et ne peuvent pas lancer d’exceptions contrôlées.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
280
Java EE 6 et GlassFish 3
Pour appliquer l’intercepteur du Listing 8.6, le bean de session doit utiliser l’annotation @Interceptors : dans le Listing 8.7, CustomerEJB précise qu’il s’agit de la classe ProfileInterceptor. Dès lors, quand l’EJB sera instancié par le conteneur, la méthode logMethod() de l’intercepteur sera invoquée avant la méthode init(). Les appels aux méthodes createCustomer() ou findCustomerById() ne seront en revanche pas interceptés, mais la méthode profile() de l’intercepteur sera appelée avant que le CustomerEJB soit détruit par le conteneur. Listing 8.7 : CustomerEJB utilisant un intercepteur de rappel @Stateless @Interceptors(ProfileInterceptor.class) public class CustomerEJB { @PersistenceContext(unitName = "chapter08PU") private EntityManager em; @PostConstruct public void init() { // ... } public void createCustomer(Customer customer) { em.persist(customer); }
}
public Customer findCustomerById(Long id) { return em.find(Customer.class, id); }
Les méthodes de rappel du cycle de vie et les méthodes @AroundInvoke peuvent être définies dans la même classe intercepteur. Chaînage et exclusion d’intercepteurs
Nous venons de voir comment intercepter les appels dans un seul bean (avec @AroundInvoke) et entre plusieurs beans (avec @Interceptors). EJB 3.1 permet également de chaîner plusieurs intercepteurs et de définir des intercepteurs par défaut qui s’appliqueront à tous les beans de session. En fait, il est possible d’attacher plusieurs intercepteurs avec l’annotation @Interceptors en lui passant en paramètre une liste d’intercepteurs séparés par des virgules. En ce cas, l’ordre dans lequel ils seront invoqués est déterminé par leur ordre d’apparition dans cette liste. Le code du Listing 8.8, par exemple, utilise @Interceptors à la fois au niveau du bean et au niveau des méthodes.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 8
Méthodes de rappel et intercepteurs 281
Listing 8.8 : CustomerEJB utilisant un intercepteur de rappel @Stateless @Interceptors(I1.class, I2.class) public class CustomerEJB { public void createCustomer(Customer customer) { ... } @Interceptors(I3.class, I4.class) public Customer findCustomerById(Long id) { ... } public void removeCustomer(Customer customer) { ... } @ExcludeClassInterceptors public Customer updateCustomer(Customer customer) { ... } }
Aucun intercepteur ne sera invoqué lorsqu’un client appelle la méthode updateCustomer() (car elle est annotée par @ExcludeClassInterceptors). Lorsque createCustomer() est appelée, l’intercepteur I1 s’exécutera, suivi de l’intercepteur I2. Lorsque findCustomerById() est appelée, les intercepteurs I1, I2, I3 et I4 seront exécutés dans cet ordre. Outre les intercepteurs au niveau des méthodes et des classes, EJB 3.1 permet de créer des intercepteurs par défaut, qui seront utilisés pour toutes les méthodes de tous les EJB d’une application. Aucune annotation n’ayant la portée d’une application, ces intercepteurs doivent être définis dans le descripteur de déploiement (ejbjar.xml). Voici, par exemple, la partie XML à ajouter à ce fichier pour appliquer par défaut l’intercepteur ProfileInterceptor à tous les EJB : * com.apress.javaee6.ProfileInterceptor
Le caractère joker * dans l’élément signifie que tous les EJB appliqueront l’intercepteur défini dans l’élément . Si vous déployez le bean CustomerEJB du Listing 8.7 avec cet intercepteur par défaut, le ProfileInterceptor sera invoqué avant tous les autres intercepteurs. Si plusieurs types d’intercepteurs sont définis pour un même bean de session, le conteneur les applique dans l’ordre décroissant des portées : le premier sera donc l’intercepteur par défaut et le dernier, l’intercepteur de méthode. Les règles qui gouvernent ces appels sont décrites à la Figure 8.5. Pour désactiver les intercepteurs par défaut pour un EJB spécifique, il suffit d’appliquer l’annotation @javax. interceptor.ExcludeDefaultInterceptors sur la classe ou sur les méthodes, comme le montre le Listing 8.9.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
282
Java EE 6 et GlassFish 3
Conteneur EJB Client
Intercepteur par défaut
Intercepteur de classe
Intercepteur de méthode
Bean de session
Figure 8.5 Chaînage des différents types d’intercepteurs.
Listing 8.9 : EJB excluant les intercepteurs par défaut @Stateless @ExcludeDefaultInterceptors @Interceptors(LoggingInterceptor.class) public class CustomerEJB { public void createCustomer(Customer customer) { ... } public Customer findCustomerById(Long id) { ... } public void removeCustomer(Customer customer) { ... } @ExcludeClassInterceptors public Customer updateCustomer(Customer customer) { ... } }
Résumé Dans ce chapitre, nous avons vu que les beans de session sans état et singletons partagent le même cycle de vie et que celui des beans avec état est légèrement différent. En effet, ces derniers mémorisent l’état conversationnel avec leur client et doivent temporairement sérialiser cet état sur un support de stockage permanent (passivation). Nous avons également vu que les annotations de rappel permettent d’ajouter de la logique métier aux beans, qui s’exécutera avant ou après la survenue d’un événement (@PostConstruct, @PreDestroy, etc.). Les intercepteurs sont des mécanismes permettant de mettre en œuvre la POA avec les EJB car ils permettent au conteneur d’invoquer des traitements transverses sur l’application. Ils sont simples à utiliser, puissants et peuvent être chaînés pour appliquer plusieurs traitements à la suite. Il est également possible de définir des intercepteurs par défaut qui s’appliqueront à toutes les méthodes de tous les beans d’une application. Un conteneur EJB peut lui-même être considéré comme une chaîne d’intercepteurs : les appels de méthodes sont interceptés par le conteneur, qui applique alors plusieurs services comme la gestion des transactions et de la sécurité. Le chapitre suivant est consacré à ces deux services.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
9 Transactions et sécurité La gestion des transactions et de la sécurité est un problème important pour les entreprises car elle permet aux applications de disposer de données cohérentes et de sécuriser les accès à ces données. Ces deux services sont des traitements de bas niveau dont ne devraient pas se soucier ceux qui développent du code métier. Ils sont offerts par les EJB de façon très simple : soit par programmation à un haut niveau d’abstraction, soit de façon déclarative en utilisant les métadonnées. L’essentiel du travail d’une application d’entreprise consiste à gérer des données : à les stocker (généralement dans une base de données), à les récupérer, à les traiter, etc. Ces traitements sont souvent réalisés simultanément par plusieurs applications qui tentent d’accéder aux mêmes données. Les SGBDR disposent de mécanismes de bas niveau pour synchroniser les accès concurrents – le verrouillage pessimiste, par exemple – et utilisent les transactions pour garantir la cohérence des données. Les EJB utilisent tous ces mécanismes. La sécurisation des données est également un point important. La couche métier doit agir comme un pare-feu et autoriser certaines opérations à certains groupes d’utilisateurs tout en interdisant l’accès à d’autres (les utilisateurs et les employés peuvent lire les données, mais seuls les employés sont autorisés à les stocker, par exemple). La première partie de ce chapitre est consacrée à la gestion des transactions avec EJB 3.1. Nous présenterons les transactions en général, puis les différents types de transactions reconnus par les EJB. La seconde partie du chapitre s’intéressera à la sécurité.
Transactions Les données sont cruciales et elles doivent être correctes, quelles que soient les opérations effectuées et le nombre d’applications qui y accèdent. Une transaction
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
284
Java EE 6 et GlassFish 3
sert à garantir que les données resteront dans un état cohérent. Elle représente un groupe logique d’opérations qui doivent être exécutées de façon atomique – elle forme donc ce que l’on appelle une unité de traitement. Les opérations qui la constituent peuvent impliquer le stockage de données dans une ou plusieurs bases, l’envoi de messages ou l’appel de services web. Les sociétés utilisent quotidiennement les transactions pour les applications bancaires ou de commerce en ligne, ainsi que pour les interactions B2B (business-to-business) avec leurs partenaires. Ces opérations métiers indivisibles s’exécutent en séquence ou en parallèle pendant une durée relativement courte. Pour qu’une transaction réussisse, toutes ses opérations doivent réussir (on dit alors que la transaction est validée – committed). Il suffit que l’une des opérations échoue pour que la transaction échoue également (la transaction est annulée – rolled back). Les transactions doivent garantir un certain niveau de fiabilité et de robustesse et respecter les propriétés ACID. ACID
ACID est un acronyme des quatre propriétés qui définissent une transaction fiable : atomicité, cohérence, isolement et durée (voir Tableau 9.1). Pour expliquer chacune d’elles, prenons l’exemple classique d’un transfert bancaire dans lequel on débite un compte épargne pour créditer un compte courant. Tableau 9.1 : Propriétés ACID
Propriété
Description
Atomicité
Une transaction est composée d’une ou de plusieurs opérations regroupées dans une unité de traitement. À la fin de la transaction, soit toutes les opérations se sont déroulées correctement (transaction validée – commit), soit il s’est passé un problème inattendu, auquel cas aucune ne sera réalisée (transaction annulée – rollback).
Cohérence
À la fin d’une transaction, les données sont dans un état cohérent.
Isolement
L’état intermédiaire d’une transaction n’est pas visible aux applications externes.
Durée
Lorsqu’une transaction est validée, les modifications apportées aux données sont visibles aux autres applications.
On peut imaginer que le transfert d’un compte vers un autre représente une suite d’accès à la base de données : le compte épargne est débité à l’aide d’une instruction update de SQL, le compte courant est crédité par une autre instruction update et un
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 285
enregistrement est ajouté dans une autre table afin de garder la trace de ce transfert. Ces opérations doivent s’effectuer dans la même unité de traitement (atomicité) car il ne faut pas que le débit ait lieu et qu’il n’y ait pas de crédit correspondant. Du point de vue d’une application externe interrogeant les comptes, les deux opérations ne seront visibles que lorsqu’elles se seront toutes les deux correctement réalisées (isolement). Lorsque la transaction est validée ou annulée, la cohérence des données est assurée par les contraintes d’intégrité de la base de données (clés primaires, relations ou champs). Lorsque le transfert est terminé, il est possible d’accéder aux données par les autres applications (durée). Transactions locales
Pour que les transactions fonctionnent et respectent les propriétés ACID, plusieurs composants doivent être mis en place. Commençons par l’exemple le plus simple qui soit d’une application effectuant plusieurs modifications sur une ressource unique (une base de données, par exemple). Lorsqu’une seule ressource transactionnelle est nécessaire, il suffit d’utiliser une transaction locale – on peut utiliser des transactions distribuées à la JTA, mais ce n’est pas strictement nécessaire. La Figure 9.1 représente l’interaction entre une application et une ressource via un gestionnaire de transactions et un gestionnaire de ressources. Figure 9.1 Transaction n’impliquant qu’une seule ressource.
Application JTA Gestionnaire de transactions JTA Gestionnaire de ressources
Ressource
Les composants présentés à la Figure 9.1 permettent d’abstraire de l’application l’essentiel du traitement spécifique à une transaction : ■■
Le gestionnaire de transactions est le composant central de la gestion des opérations transactionnelles. Il crée les transactions pour le compte de l’application, informe le gestionnaire de ressources qu’il participe à une transaction (opération
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
286
Java EE 6 et GlassFish 3
de recrutement) et conduit la validation ou l’annulation de cette transaction sur ce gestionnaire de ressources. ■■ Le gestionnaire de ressources s’occupe de gérer les ressources et de les enregistrer auprès du gestionnaire de transactions. Un pilote de SGBDR, une ressource JMS ou un connecteur Java sont des gestionnaires de ressources. ■■ La ressource est le support de stockage persistant sur lequel on lit ou écrit (une base de données, etc.). L’application n’est pas responsable du respect des propriétés ACID : elle se borne simplement à décider s’il faut valider ou annuler la transaction, et c’est le gestionnaire de transactions qui prépare toutes les ressources pour que tout se passe bien. Avec Java EE, ces composants gèrent les transactions via JTA (Java Transaction API), qui est décrite par la JSR 907. JTA définit un ensemble d’interfaces permettant à l’application de délimiter des frontières de transactions, ainsi que des API pour fonctionner avec le gestionnaire de transactions. Ces interfaces sont définies dans le paquetage javax.transaction ; le Tableau 9.2 décrit les principales. Tableau 9.2 : Interfaces principales de JTA
Interface
Description
UserTransaction
Définit les méthodes qu’une application peut utiliser pour contrôler par programme les frontières de transactions. Les EJB BMT (beanmanaged transaction) s’en servent pour lancer, valider ou annuler une transaction (voir la section "Transactions gérées par les beans").
TransactionManager
Permet au conteneur EJB de délimiter les frontières de transaction du côté EJB.
Transaction
Permet d’effectuer des opérations sur la transaction dans un objet Transaction.
XAResource
Équivalent Java de l’interface standard X/Open XA (voir la section suivante).
XA et transactions distribuées
Comme nous venons de le voir, une transaction qui n’utilise qu’une seule ressource (comme à la Figure 9.1) est une transaction locale. Cependant, de nombreuses applications d’entreprise utilisent plusieurs ressources : si l’on revient à l’exemple du transfert de fonds, le compte épargne et le compte courant pourraient se trouver dans deux bases de données distinctes. Il faut alors gérer les transactions entre ces différentes ressources ou entre des ressources distribuées sur le réseau. Ces transactions
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 287
à l’échelle d’une entreprise nécessitent une coordination particulière impliquant XA et JTS (Java Transaction Service). La Figure 9.2 représente une application qui utilise une frontière de transaction entre plusieurs ressources. Elle peut ainsi stocker des données dans une base et envoyer un message JMS dans la même unité de traitement, par exemple. Figure 9.2
Application
Transaction XA impliquant deux ressources.
JTA Gestionnaire de transactions Ressource JTA / XA Gestionnaire de ressources
Gestionnaire de ressources
Ressource
Ressource
Pour disposer d’une transaction fiable entre plusieurs ressources, le gestionnaire de transactions doit utiliser une interface XA du gestionnaire de ressources, un standard de l’Open Group (http:// www.opengroup.org) pour le traitement des transactions distribuées (DTP) qui préserve les propriétés ACID. Cette interface est reconnue par JTA et permet à des gestionnaires de ressources hétérogènes provenant d’éditeurs différents de fonctionner ensemble en passant par une interface commune. XA utilise une validation de transaction en deux phases pour garantir que toutes les ressources valideront ou annuleront simultanément chaque transaction. Dans notre exemple de transfert de fonds, supposons que le compte épargne soit débité sur une première base de données et que la transaction soit validée. Puis le compte courant est crédité sur une seconde base, mais la transaction échoue : il faudrait donc revenir à la première base et annuler les modifications apportées par la transaction. Comme le montre la Figure 9.3, pour éviter ce problème d’incohérence des données, la validation en deux phases effectue une étape supplémentaire avant la validation finale. Au cours de la première phase, chaque gestionnaire de ressources est prévenu via une commande "de préparation" qu’une validation va avoir lieu, ce qui leur permet d’indiquer s’ils peuvent ou non appliquer leurs modifications. S’ils annoncent tous qu’ils sont prêts, la transaction peut se poursuivre et on demande à tous les gestionnaires de ressources de valider leurs transactions dans la seconde phase.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
288
Java EE 6 et GlassFish 3
Figure 9.3 Validation en deux phases.
Phase 1 Préparation
Phase 2 Validation
prépare prêt
valide Gestionnaire de ressources
Gestionnaire de transactions prêt
Gestionnaire de ressources
validée Gestionnaire de transactions validée
Gestionnaire de ressources
prépare
Gestionnaire de ressources
valide
La plupart du temps, les ressources sont distribuées sur le réseau (voir Figure 9.4). Un tel système utilise JTS, qui implémente la spécification OTS (Object Transaction Service) de l’OMG (Object Management Group) permettant aux gestionnaires de transactions de participer aux transactions distribuées via IIOP (Internet Inter-ORB Protocol). JTS est conçu pour les éditeurs qui fournissent les infrastructures de systèmes de transaction. Les développeurs EJB n’ont pas à s’en soucier : il suffit qu’ils utilisent JTA, qui s’interface avec JTS à un niveau supérieur. Figure 9.4 Une transaction distribuée XA.
Application JTA Gestionnaire de transactions JTA / XA
JTS / OTS
Gestionnaire de transactions JTA / XA
Gestionnaire de ressources
Gestionnaire de ressources
Ressource
Ressource
Support des transactions avec les EJB Lorsque l’on développe de la logique métier avec les EJB, il n’est pas nécessaire de se soucier de la structure interne des gestionnaires de transactions ou de ressources car JTA abstrait la plus grosse partie de la complexité sous-jacente. Grâce aux EJB, le développement d’une application transactionnelle est donc très simple car c’est le conteneur qui implémente les protocoles de bas niveau pour les transactions, comme
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 289
la validation en deux phases ou la propagation du contexte de transaction. Un conteneur EJB est donc un gestionnaire de transactions qui utilise à la fois JTA et JTS pour participer aux transactions distribuées impliquant d’autres conteneurs EJB. Dans une application Java EE typique, les beans de session établissent les frontières de la transaction, appellent des entités pour dialoguer avec la base de données ou envoient des messages JMS dans un contexte de transaction. Depuis sa création, le modèle EJB a été conçu pour gérer les transactions : elles font donc partie des EJB et chacune de leurs méthodes est, par défaut, automatiquement enveloppée dans une transaction. Ce comportement par défaut s’appelle transaction gérée par le conteneur (CMT), ou démarcation de transaction déclarative. Vous pouvez également choisir de gérer vous-même les transactions en utilisant des transactions gérées par le bean (BMT), ou démarcation de transaction par programme. C’est la démarcation de transaction qui détermine quand commencent et finissent les transactions. Transactions gérées par le conteneur
Lorsque l’on gère les transactions de façon déclarative, on délègue la politique de démarcation au conteneur. Il n’est pas nécessaire d’utiliser explicitement JTA dans le code (même s’il est utilisé en coulisse) ; on peut laisser le conteneur marquer les frontières de transactions en les ouvrant et en les validant à partir des métadonnées. Le conteneur EJB fournit les services de gestion des transactions aux beans de session et aux MDB (voir Chapitre 13). Au Chapitre 7, nous avons vu plusieurs exemples de beans de session, d’annotations et d’interfaces, mais rien de spécifique aux transactions. Le Listing 9.1 montre le code d’un bean sans état utilisant CMT. Comme vous pouvez le constater, aucune annotation particulière n’a été ajoutée et il n’y a pas d’interface spéciale à implémenter – comme on l’a déjà indiqué, les EJB sont transactionnels par nature. Grâce à la configuration par exception, c’est la gestion des transactions par défaut qui s’applique ici (comme nous le verrons plus loin, REQUIRED est l’attribut de transaction par défaut). Listing 9.1 : Bean sans état avec CMT @Stateless public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @EJB
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
290
Java EE 6 et GlassFish 3
private InventoryEJB inventory; public List findBooks() { Query query = em.createNamedQuery("findAllBooks"); return query.getResultList(); } public Book createBook(Book book) { em.persist(book); inventory.addItem(book); return book; } }
Vous pourriez vous demander ce qui rend le code du Listing 9.1 transactionnel : la réponse est le conteneur. La Figure 9.5 montre ce qui se passe quand un client invoque la méthode createBook() : son appel est intercepté par le conteneur, qui vérifie immédiatement avant l’appel de cette méthode si un contexte de transaction est associé à cet appel. Dans la négative, le conteneur ouvre par défaut une nouvelle transaction avant d’entrer dans la méthode, puis invoque celle-ci. À la fin de la méthode, le conteneur valide automatiquement la transaction (ou l’annule automatiquement si une exception particulière est lancée, comme nous le verrons dans la section "Traitement des exceptions"). Client
Conteneur EJB
ItemEJB
InventoryEJB
Transaction
1 : createBook 2 : begin 3 : createBook 4 : additem 5 : validation ou annulation
Figure 9.5 Le conteneur gère la transaction.
Dans le Listing 9.1 et à la Figure 9.5, il est intéressant de noter qu’une méthode métier d’un bean (ItemEJB.createBook()) peut être cliente d’une méthode métier d’un autre bean (InventoryEJB. AddItem()). Avec le comportement par défaut, le contexte de transaction utilisé pour createBook() (celui du client ou celui créé par le conteneur) est appliqué à addItem(). La validation finale a lieu si les deux méthodes se sont terminées correctement mais ce comportement peut être modifié à
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 291
l’aide de métadonnées (annotations ou descripteur de déploiement XML). L’attribut de transaction choisi (REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED ou NEVER) modifie la façon dont le conteneur démarque la transaction du client : soit il utilise la transaction du client, soit il exécute la méthode dans une nouvelle transaction, soit il l’exécute sans transaction, soit il lance une exception. Les attributs de transactions sont décrits dans le Tableau 9.3. Tableau 9.3 : Attributs CMT Attributes
Attribut
Description
REQUIRED
Cet attribut, qui est celui par défaut, signifie qu’une méthode doit toujours être invoquée dans une transaction. Le conteneur en crée une nouvelle si la méthode a été appelée par un client non transactionnel. Si le client dispose d’un contexte de transaction, la méthode métier s’exécute dans celui-ci. On utilise REQUIRED lorsque l’on modifie des données et que l’on ne sait pas si le client a lancé ou non une transaction.
REQUIRES_NEW
Le conteneur crée toujours une nouvelle transaction avant d’exécuter une méthode, que le client s’exécute ou non dans une transaction. Si le client est dans une transaction, le conteneur la suspend temporairement, en crée une seconde, la valide, puis revient à la première. Ceci signifie que le succès ou l’échec de la seconde transaction n’a pas d’effet sur la transaction existante du client. On utilise REQUIRED_NEW lorsque l’on ne souhaite pas qu’une annulation de la transaction ait un effet sur le client.
SUPPORTS
La méthode de l’EJB hérite du contexte de transaction du client. Si ce contexte est disponible, il est utilisé par la méthode ; sinon le conteneur invoque la méthode sans contexte de transaction. On utilise SUPPORTS lorsque l’on a un accès en lecture seule à la table de la base de données.
MANDATORY
Le conteneur exige une transaction avant d’appeler la méthode métier, mais n’en créera pas de nouvelle. Si le client a un contexte de transaction, celui-ci est propagé ; sinon une exception javax. ejb.exception" EJBTransactionRequiredException est levée.
NOT_SUPPORTED
La méthode de l’EJB ne peut pas être appelée dans un contexte de transaction. Si le client n’en possède pas, rien ne se passe ; s’il en a un, le conteneur suspend la transaction du client, appelle la méthode puis relance la transaction à la fin de l’appel.
NEVER
La méthode de l’EJB ne doit pas être appelée par un client transactionnel. Si le client s’exécute dans un contexte de transition, le conteneur lève une exception javax.ejb.EJBException.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
292
Java EE 6 et GlassFish 3
La Figure 9.6 illustre tous les comportements possibles d’un EJB en fonction de la présence ou non d’un contexte de transaction du client. Si, par exemple, la méthode createBook() n’a pas de contexte transactionnel et qu’elle appelle addItem() avec un attribut MANDATORY, une exception est lancée. Le bas de la Figure 9.6 montre les mêmes combinaisons, mais avec un client disposant d’un contexte transactionnel. ItemEJB
1. createBook() La méthode X n'est pas appelée dans une transaction.
InventoryEJB
additem
additem 1. createBook() La méthode X est dans une transaction.
Attribut CMT
Résultat
REQUIRED
Nouvelle transaction
REQUIRES NEW
Nouvelle transaction
SUPPORTS
Pas de transaction
MANDATORY
Exception
NOT SUPPORTED Pas de transaction NEVER
Pas de transaction
Attribut CMT
Résultat
REQUIRED
Transaction du client
REQUIRES NEW
Nouvelle transaction
SUPPORTS
Transaction du client
MANDATORY
Transaction du client
NOT SUPPORTED Pas de transaction NEVER
Exception
Figure 9.6 Deux appels à InventoryEJB avec des politiques de transactions différentes.
Pour appliquer l’un de ces six attributs de démarcation à un bean de session, il suffit d’utiliser l’annotation @javax.ejb.TransactionAttribute ou le descripteur de déploiement (l’élément du fichier ejb-jar.xml). Ces métadonnées peuvent s’appliquer aux différentes méthodes ou au bean entier – dans ce cas, toutes les méthodes métiers du bean héritent de la valeur de l’attribut. Dans le Listing 9.2, ItemEJB utilise une politique de démarcation SUPPORT, sauf la méthode createBook(), qui utilise REQUIRED. Listing 9.2 : Bean sans état avec CMT @Stateless @TransactionAttribute(TransactionAttributeType.SUPPORTS) public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @EJB private InventoryEJB inventory; public List findBooks() { Query query = em.createNamedQuery("findAllBooks"); return query.getResultList(); }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 293
@TransactionAttribute(TransactionAttributeType.REQUIRED) public Book createBook(Book book) { em.persist(book); inventory.addItem(book); return book; } }
INFO Le contexte de transaction du client ne se propage pas lors des appels de méthodes asynchrones. En outre, comme nous le verrons au Chapitre 13, les MDB n’autorisent que les attributs REQUIRED et NOT_SUPPORTED.
Marquage d’un CMT pour annulation
Nous avons vu que le conteneur délimitait automatiquement les transactions et effectuait à notre place les opérations de lancement, de validation et d’annulation. En tant que développeur, on peut cependant vouloir empêcher la validation d’une transaction en cas d’erreur ou d’une condition métier particulière. En outre, il faut bien comprendre qu’un bean CMT n’est pas autorisé à annuler explicitement la transaction : il faut utiliser le contexte de l’EJB (voir la section "Contexte de session" du Chapitre 7) pour informer le conteneur de l’annuler. Comme le montre le Listing 9.3, le bean InventoryEJB dispose d’une méthode oneItemSold() qui accède à la base de données via le gestionnaire de persistance et envoie un messsage JMS pour informer la société de transport qu’un article a été vendu et qu’il doit être livré. Si le niveau du stock est égal à zéro (ce qui signifie qu’il n’y a plus d’article en stock), la méthode doit explicitement annuler la transaction. Pour ce faire, le bean doit d’abord obtenir la SessionContext via l’injection de dépendances, puis appeler la méthode setRollbackOnly() de cette interface. Cet appel n’annule pas immédiatement la transaction mais positionne un indicateur dont tiendra compte le conteneur lorsqu’il terminera la transaction. Listing 9.3 : Un bean sans état marque la transaction pour annulation @Stateless public class InventoryEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @Resource
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
294
Java EE 6 et GlassFish 3
private SessionContext ctx; public void oneItemSold(Item item) { em.merge(item); item.decreaseAvailableStock(); sendShippingMessage(); if (inventoryLevel(item) == 0) ctx.setRollbackOnly(); } }
Un bean peut également appeler la méthode SessionContext.getRollbackOnly() pour tester si la transaction courante a été marquée pour annulation. Un autre moyen d’informer par programme le conteneur qu’il doit annuler une transaction consiste à lancer des types d’exceptions précis. Traitement des exceptions
Le traitement des exceptions en Java est, depuis la création du langage, assez troublant car il utilise la notion d’exception contrôlée non contrôlée. L’association des transactions et des exceptions dans les EJB est également assez épique... Avant d’aller plus loin, précisons que le lancement d’une exception dans une méthode métier ne marquera pas toujours la transaction pour annulation – cela dépend du type de l’exception ou des métadonnées qui la définissent. En fait, la spécification EJB 3.1 met en relief deux types d’exceptions : ■■
Les exceptions d’application. Ce sont les exceptions liées à la logique métier traitée par l’EJB. Une exception d’application peut, par exemple, être levée si des paramètres incorrects sont passés à une méthode, si le stock est trop faible ou si le numéro de carte de crédit est incorrect. Le lancement d’une exception d’application n’implique pas automatiquement que la transaction soit marquée pour annulation. Comme on l’explique plus loin dans le Tableau 9.4, le conteneur n’annule pas une transaction lorsque des exceptions contrôlées (celles qui héritent de java.lang.Exception) sont levées – par contre, il le fait pour les exceptions non contrôlées (qui héritent de RuntimeException).
■■
Les exceptions systèmes. Elles sont causées par des erreurs au niveau système, comme les erreurs JNDI, les erreurs de la JVM, l’impossibilité d’établir une connexion avec la base de données, etc. Une exception système peut être une sous-classe de RuntimeException ou de java.rmi.RemoteException (et donc une sous-classe de javax.ejb.EJBException). La levée d’une exception système marque la transaction pour annulation.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 295
Avec cette définition, nous savons maintenant que le conteneur annulera la transaction s’il détecte une exception système comme ArithmeticException, ClassCastException, IllegalArgumentException ou NullPointerException. Les exceptions d’application dépendent en revanche de nombreux facteurs. À titre d’exemple, le Listing 9.4 modifie le code du Listing 9.3 et utilise une exception d’application. Listing 9.4 : Bean sans état levant une exception d’application @Stateless public class InventoryEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; public void oneItemSold(Item item) throws InventoryLevelTooLowException { em.merge(item); item.decreaseAvailableStock(); sendShippingMessage();
}
}
if (inventoryLevel(item) == 0) throw new InventoryLevelTooLowException();
InventoryLevelTooLowException est une exception d’application car elle est liée à la logique métier de la méthode oneItemSold(). Selon que l’on veuille ou non annuler la transaction, on peut la faire hériter d’une exception contrôlée ou non contrôlée, ou l’annoter avec @javax.ejb.ApplicationException (ou l’élément XML équivalent dans le descripteur de déploiement). Cette annotation a un paramètre rollback qui peut être initialisé à true pour annuler explicitement la transaction. Dans le Listing 9.5, InventoryLevelTooLowException est une exception annotée et contrôlée.
Listing 9.5 : Exception d’application avec rollback = true @ApplicationException(rollback = true) public class InventoryLevelTooLowException extends Exception { public InventoryLevelTooLowException() { } public InventoryLevelTooLowException(String message) { super(message); } }
Si le bean InventoryEJB du Listing 9.4 lance l’exception définie dans le Listing 9.5, la transaction sera marquée pour annulation et c’est le conteneur qui se chargera de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
296
Java EE 6 et GlassFish 3
cette annulation à la fin de la transaction. Le Tableau 9.4 présente toutes les combinaisons possibles d’exceptions d’application. Sa première ligne pourrait être interprétée comme "si l’exception d’application hérite d’Exception et qu’elle ne soit pas annotée par @ApplicationException, son lancement ne marquera pas la transaction pour annulation". Tableau 9.4 : Combinaisons des exceptions d’applications
Hérite de
@ApplicationException
Description
Exception
Pas d’annotation
Par défaut, la levée d’une exception contrôlée ne marque pas la transaction pour annulation
Exception
rollback = true
La transaction est marquée pour annulation
Exception
rollback = false
La transaction n’est pas marquée pour annulation
RuntimeException
Pas d’annotation
Par défaut, la levée d’une exception non contrôlée marque la transaction pour annulation
RuntimeException
rollback = true
La transaction est marquée pour annulation
RuntimeException
rollback = false
La transaction n’est pas marquée pour annulation
Transactions gérées par le bean
Avec CMT, on laisse au conteneur le soin de réaliser la démarcation des transactions en précisant simplement un attribut et en utilisant le contexte de session ou des exceptions pour marquer une transaction pour annulation. Dans certains cas, toutefois, l’approche déclarative de CMT ne permet pas d’obtenir la finesse de démarcation voulue (une méthode ne peut pas participer à plusieurs transactions, par exemple). Pour résoudre ce problème, les EJB permettent de gérer les démarcations par programme avec BMT (Bean-Managed Transaction), qui autorise la gestion explicite des frontières de transaction (lancement, validation, annulation) avec JTA.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 297
Pour désactiver la démarcation CMT par défaut et basculer dans le mode BMT, un bean doit simplement utiliser l’annotation @javax.ejb.TransactionManagement (ou son équivalent XML dans le fichier ejb-jar.xml) : @Stateless @TransactionManagement(TransactionManagementType.BEAN) public class ItemEJB { ... }
Avec la démarcation BMT, l’application demande la transaction et le conteneur EJB crée la transaction physique puis s’occupe uniquement de quelques détails de bas niveau. En outre, il ne propage pas les transactions d’un EJB BMT vers un autre. L’interface principale pour mettre en œuvre BMT est javax.transaction.-UserTransaction. Elle permet au bean de délimiter une transaction, de connaître son statut, de fixer un délai d’expiration, etc. Cette interface est instanciée par le conteneur EJB et est rendue disponible via l’injection de dépendances, une recherche JNDI ou le SessionContext (avec la méthode SessionContext.get-UserTransaction()). Son API est décrite dans le Tableau 9.5. Tableau 9.5 : Méthodes de l’interface javax.transaction.UserTransaction
Interface
Description
begin
Débute une nouvelle transaction et l’associe au thread courant
commit
Valide la transaction attachée au thread courant
rollback
Annule la transaction attachée au thread courant
setRollbackOnly
Marque la transaction courante pour annulation
getStatus
Récupère le statut de la transaction courante
setTransactionTimeout
Modifie le délai d’expiration de la transaction courante
Le Listing 9.6 montre comment développer un bean BMT. On commence par obtenir une référence à l’interface UserTransaction par injection via l’annotation @Resource. La méthode oneItemSold() débute la transaction, effectue un traitement métier puis, en fonction d’une condition métier, valide ou annule cette transaction. Notez également que la transaction est marquée pour annulation dans le bloc catch (nous avons simplifié le traitement d’exception pour des raisons de lisibilité).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
298
Java EE 6 et GlassFish 3
Listing 9.6 : Bean sans état avec BMT @Stateless public class InventoryEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @Resource private UserTransaction ut; public void oneItemSold(Item item) { try { ut.begin(); em.merge(item); item.decreaseAvailableStock(); sendShippingMessage(); if (inventoryLevel(item) == 0) ut.rollback(); else ut.commit(); } catch (Exception e) { ut.setRollbackOnly(); } sendInventoryAlert(); } }
Dans le code CMT du Listing 9.3, c’est le conteneur qui débutait la transaction avant l’exécution de la méthode et la validait immédiatement après. Avec le code BMT du Listing 9.6, c’est vous qui définissez manuellement les frontières de la transaction dans la méthode elle-même.
Sécurité La sécurisation des applications est (ou devrait être) un souci majeur pour les sociétés. Ceci peut aller de la sécurisation d’un réseau au chiffrement des transferts de données, en passant par l’octroi de certaines permissions aux utilisateurs d’un système. Au cours de notre navigation quotidienne sur Internet, nous rencontrons de nombreux sites où nous devons entrer un nom d’utilisateur et un mot de passe pour avoir accès à certaines parties d’une application. La sécurité est devenue une nécessité sur le Web et, en conséquence, Java EE a défini plusieurs mécanismes pour sécuriser les applications.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 299
La sécurité nécessite de comprendre plusieurs concepts. L’un d’eux est la liaison des utilisateurs à un principal et le fait qu’ils peuvent avoir plusieurs rôles. Chaque rôle donne des permissions pour un ensemble de ressources mais, pour avoir une identité dans le domaine de sécurité, un utilisateur doit pouvoir être authentifié : la plate-forme contrôlera alors l’accès en autorisant les ressources en fonction du rôle de l’utilisateur. Principal et rôle
Les "principaux" et les rôles tiennent une place importante dans la sécurité logicielle. Un principal est un utilisateur qui a été authentifié (par un nom et un mot de passe stockés dans une base de données, par exemple). Les principaux peuvent être organisés en groupes, appelés rôles, qui leur permettent de partager un ensemble de permissions (accès au système de facturation ou possibilité d’envoyer des messages dans un workflow, par exemple). La Figure 9.7 montre comment les utilisateurs peuvent être représentés dans un système sécurisé. Comme vous pouvez le constater, un utilisateur authentifié est lié à un principal qui a un identifiant unique et qui peut être associé à plusieurs rôles. Le principal de l’utilisateur Frank, par exemple, est lié aux rôles Employé et Admin. Figure 9.7 Principaux et rôles.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
300
Java EE 6 et GlassFish 3
Authentification et habilitation
La sécurisation d’une application implique deux fonctions : l’authentification et l’habilitation. La première consiste à vérifier l’identité de l’utilisateur (son identifiant et son mot de passe, son OpenID, son empreinte biométrique, etc.) en utilisant un système d’authentification et en affectant un principal à cet utilisateur. L’habilitation consiste à déterminer si un principal (un utilisateur authentifié) a accès à une ressource particulière (un livre, par exemple) ou à une fonction donnée (supprimer un livre, par exemple). Selon son rôle, l’utilisateur peut avoir accès à toutes les ressources, à aucune ou à certaines d’entre elles. La Figure 9.8 décrit un scénario de sécurité classique. L’utilisateur doit entrer son identifiant et son mot de passe via une interface client (web ou Swing). Ces informations sont vérifiées avec JAAS (Java Authentication and Authorization Service) via un système d’authentification sous-jacent. Si l’authentification réussit, l’utilisateur est associé à un principal qui est ensuite lui-même associé à un ou plusieurs rôles. Lorsque l’utilisateur accède à un EJB sécurisé, le principal est transmis de façon transparente à l’EJB, qui l’utilise pour savoir si le rôle de l’appelant l’autorise à accéder aux méthodes qu’il tente d’exécuter. Figure 9.8 Scénario de sécurité classique avec JAAS.
Conteneur web ou AAC
Principal authentifié par mot de passe
Authentifie
Conteneur EJB
Autorise JAAS
Authentifie
Système d'authentification
Comme le montre la Figure 9.8, la sécurité de Java EE repose largement sur l’API JAAS. En fait, JAAS est l’API utilisée en interne par les couches web et EJB pour réaliser les opérations d’authentification et d’habilitation. Elle accède également aux systèmes d’authentification sous-jacents comme LDAP (Lightweight Directory Access Protocol), Microsoft Active Directory, etc.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 301
Gestion de la sécurité dans EJB Le but principal du modèle de sécurité EJB est de contrôler l’accès au code métier. Comme nous venons de le voir, l’authentification est prise en charge par la couche web (ou une application cliente) ; le principal et ses rôles sont ensuite transmis à la couche EJB et le service sécurité du conteneur EJB vérifie si le rôle d’un utilisateur authentifié l’autorise à accéder à une méthode. Comme la gestion des transactions, celle des habilitations peut s’effectuer de façon déclarative ou par programme. Dans le cas des habilitations déclaratives, le contrôle des accès est assuré par le conteneur EJB, tandis qu’avec les habilitations par programme c’est le code qui s’en charge en utilisant l’API JAAS. Sécurité déclarative
La politique de sécurité déclarative peut être définie dans le bean à l’aide d’annotations ou dans le descripteur de déploiement XML. Elle consiste à déclarer les rôles, à affecter des permissions aux méthodes (ou à tout le bean) ou à modifier temporairement une identité de sécurité. Tous ces contrôles s’effectuent par les annotations du Tableau 9.6, chacune d’elles pouvant porter sur une méthode et/ou sur le bean entier. Tableau 9.6 : Annotation de sécurité
Annotation
Bean
@PermitAll
X
@DenyAll
X
@RolesAllowed
X
@DeclareRoles
X
@RunAs
Méthode X
Description La méthode (ou tout le bean) est accessible par tout le monde (tous les rôles sont autorisés). Aucun rôle n’est autorisé à exécuter la méthode (tous les rôles sont refusés).
X
Donne la liste des rôles autorisés à exécuter la méthode (ou tout le bean). Définit les rôles pour la sécurité.
X
Affecte temporairement un nouveau rôle au principal.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
302
Java EE 6 et GlassFish 3
INFO Les annotations @TransactionManagement et @TransactionAttribute que nous avons présentées au début de ce chapitre sont définies dans le paquetage javax.ejb de la spécification EJB 3.1 (JSR 318). Les annotations de sécurité (@RolesAllowed, @DenyAll, etc.) font partie de la spécification Common Annotations 1.0 (JSR 250) et proviennent du paquetage javax.annotation.security.
L’annotation @RolesAllowed sert à autoriser une liste de rôles à accéder à une méthode. Elle peut s’appliquer à une méthode particulière ou à l’ensemble du bean (toutes ses méthodes métier héritent de cet accès). Elle peut prendre en paramètre un String unique (désignant le seul rôle autorisé) ou un tableau de String (tous les rôles habilités). L’annotation @DeclareRoles que nous étudierons plus tard permet de déclarer d’autres rôles. Dans le Listing 9.7, ItemEJB utilise @RolesAllowed à la fois au niveau du bean et des méthodes. Ce code indique que toutes les méthodes sont accessibles à un principal associé aux rôles utilisateur, employé ou admin. La méthode deleteBook(), en revanche, redéfinit la configuration du bean pour n’autoriser l’accès qu’au rôle admin. Listing 9.7 : Bean sans état autorisant certains rôles @Stateless @RolesAllowed({"utilisateur", "employé", "admin"}) public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; public Book findBookById(Long id) { return em.find(Book.class, id); } public Book createBook(Book book) { em.persist(book); return book; } @RolesAllowed("admin") public void deleteBook(Book book) { em.remove(em.merge(book)); } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 303
Les annotations @PermitAll et @DenyAll s’appliquent à tous les rôles : vous pouvez donc utiliser @PermitAll pour annoter un EJB ou une méthode particulière pour qu’ils puissent être invoqués par n’importe quel rôle. Inversement, @DenyAll interdit l’accès à une méthode à tous les rôles. Comme vous pouvez le constater dans le Listing 9.8, la méthode findBookById() est désormais accessible à n’importe quel rôle, pas simplement à utilisateur, employé ou admin. Par contre, la méthode findConfidentialBook() n’est pas accessible du tout. Listing 9.8 : Bean sans état utilisant les annotations @PermitAll et @DenyAll @Stateless @RolesAllowed({"utilisateur", "employé", "admin"}) public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @PermitAll public Book findBookById(Long id) { return em.find(Book.class, id); } public Book createBook(Book book) { em.persist(book); return book; } @RolesAllowed("admin") public void deleteBook(Book book) { em.remove(em.merge(book)); } @DenyAll public Book findConfidentialBook(Long secureId){ return em.find(ConfidentialBook.class, id); }
L’annotation @DeclareRoles est légèrement différente car elle ne sert ni à autoriser ni à interdire un accès – elle déclare des rôles pour toute l’application. Lorsque l’EJB du Listing 9.8 est déployé, le conteneur déclare automatiquement les rôles utilisateur, employé et admin en inspectant @RolesAllowed, mais vous pourriez vouloir déclarer d’autres rôles dans le domaine de sécurité avec @DeclareRoles. Cette annotation, qui ne s’applique qu’au niveau d’une classe, prend en paramètre un tableau de rôles et les déclare dans le domaine. En fait, les rôles peuvent donc être déclarés à l’aide de l’une de ces deux annotations ou de leur combinaison.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
304
Java EE 6 et GlassFish 3
Lorsqu’elles sont utilisées toutes les deux, c’est l’ensemble des rôles de @DeclareRoles et @RolesAllowed qui est déclaré. Ceci dit, les rôles étant généralement déclarés pour l’ensemble d’une application d’entreprise, il est plus judicieux de le faire dans le descripteur de déploiement qu’avec une annotation @DeclareRoles. Lorsque le bean ItemEJB du Listing 9.9 est déployé, les cinq rôles HR, deptVentes, utilisateur, employé et admin sont déclarés. Puis, avec l’annotation @Roles Allowed, certains d’entre eux permettent d’accéder à certaines méthodes. Listing 9.9 : Bean sans état déclarant des rôles @Stateless @DeclareRoles({"HR", "deptVentes"}) @RolesAllowed({"utilisateur", "employé", "admin"}) public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; public Book findBookById(Long id) { return em.find(Book.class, id); } public Book createBook(Book book) { em.persist(book); return book; } @RolesAllowed("admin") public void deleteBook(Book book) { em.remove(em.merge(book)); } }
La dernière annotation, @RunAs, permet d’affecter temporairement un nouveau rôle à un principal. Ceci peut être utile si, par exemple, on invoque un autre EJB depuis une méthode et que cet EJB exige un rôle différent. Dans le Listing 9.10, par exemple, ItemEJB autorise l’accès aux rôles utilisateur, employé et admin. Lorsque l’un de ces rôles accède à une méthode, celle-ci s’exécute avec le rôle temporaire deptStock (@RunAS("deptStock")), ce qui signifie que, lorsque la méthode createBook() est exécutée, InventoryEJB.addItem() sera invoquée avec le rôle deptStock.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 305
Listing 9.10 : Bean sans état s’exécutant avec un rôle différent @Stateless @RolesAllowed({"utilisateur", "employé", "admin"}) @RunAS("deptStock") public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @EJB private InventoryEJB inventory; public List findBooks() { Query query = em.createNamedQuery("findAllBooks"); return query.getResultList(); } public Book createBook(Book book) { em.persist(book); inventory.addItem(book); return book; } }
Comme vous pouvez le constater, la sécurité déclarative permet d’accéder de façon simple à une politique d’authentification puissante. Mais comment faire si vous devez fournir une sécurité spéciale à un utilisateur particulier ou appliquer une logique métier en fonction du rôle courant du principal ? La réponse est la sécurité par programmation. Sécurité par programmation
La sécurité déclarative couvre la majeure partie de la sécurité d’une application. Cependant, on a parfois besoin d’une finesse d’habilitation supplémentaire (pour autoriser un bloc de code au lieu de la méthode entière, pour autoriser ou interdire l’accès à une personne particulière, etc.). En ce cas, la gestion des habilitations par programmation permet d’autoriser ou de bloquer sélectivement l’accès à un rôle ou à un principal car on dispose alors d’un accès direct à l’interface javax. security.Principal de JAAS et au contexte de l’EJB pour vérifier le rôle du principal dans le code. L’interface SessionContext définit les méthodes suivantes pour gérer la sécurité : ■■ isCallerInRole()
teste si l’appelant a le rôle indiqué.
■■ getCallerPrincipal()
renvoie le
java.security.Principal
qui identifie
l’appelant.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
306
Java EE 6 et GlassFish 3
Pour comprendre l’utilisation de ces méthodes, étudions le bean ItemEJB du Listing 9.11 : celui-ci n’utilise aucune annotation de sécurité mais doit quand même faire certaines vérifications par programme. Le bean doit d’abord obtenir une référence à son contexte (via l’annotation @Resource) qui permettra à la méthode deleteBook() de vérifier si l’appelant a le rôle admin ou non. S’il ne l’a pas, la méthode lève java.lang.SecurityException pour prévenir l’utilisateur d’une violation des autorisations. La méthode createBook() effectue un traitement métier en utilisant les rôles et le principal : en se servant de la méthode getCallerPrincipal() pour obtenir l’objet Principal correspondant à l’appelant, elle peut vérifier qu’il s’agit de paul et ajouter une valeur spéciale à l’entité book. Listing 9.11 : Bean utilisant une sécurité par programmation @Stateless public class ItemEJB { @PersistenceContext(unitName = "chapter09PU") private EntityManager em; @Resource private SessionContext ctx; public Book findBookById(Long id) { return em.find(Book.class, id); } public void deleteBook(Book book) { if (!ctx.isCallerInRole("admin")) throw new SecurityException("Admins uniquement"); em.remove(em.merge(book)); } public Book createBook(Book book) { if (ctx.isCallerInRole("employé") && !ctx.isCallerInRole("admin")) { book.setCreatedBy("Employés uniquement"); } else if (ctx.getCallerPrincipal().getName().equals("paul")){ book.setCreatedBy("Utilisateur spécial"); } em.persist(book); return book; } }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 9
Transactions et sécurité 307
Résumé Dans ce dernier chapitre consacré aux EJB, nous avons vu comment gérer les transactions et la sécurité. Ces deux services très importants peuvent être définis de façon déclarative ou par programmation. Les transactions permettent à la couche métier de maintenir les données dans un état cohérent, même lorsque plusieurs applications y accèdent de façon concurrente. Elles respectent les propriétés ACID et peuvent être distribuées entre plusieurs ressources (bases de données, destinations JMS, services web, etc.). CMT permet de personnaliser aisément la démarcation des transactions effectuée par le conteneur EJB et vous pouvez influencer son comportement en marquant une transaction pour annulation en vous servant du contexte EJB ou des exceptions. Il est également possible d’utiliser BMT et JTA si vous avez besoin d’un contrôle plus fin sur la démarcation des transactions. Concernant la sécurité, n’oubliez pas que la couche métier n’authentifie pas les utilisateurs : elle autorise des rôles à accéder aux méthodes. La sécurité déclarative s’effectue au moyen d’un nombre relativement réduit d’annotations et permet de traiter la plupart des situations auxquelles sera confrontée une application d’entreprise. Là aussi, vous pouvez utiliser une sécurité par programmation et manipuler directement l’API JAAS. Les trois chapitres qui suivent expliquent comment développer une couche présentation avec JSF. Les pages JSF utilisent des beans gérés pour invoquer les méthodes métiers des EJB.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
10 JavaServer Faces Pour afficher graphiquement les informations provenant du serveur, nous avons besoin d’une interface utilisateur. Les applications utilisant des interfaces pour interagir avec l’utilisateur sont de différents types : applications de bureau, applications web s’exécutant dans un navigateur ou applications mobiles sur un terminal portable. Les Chapitres 10 à 12 sont consacrés aux interfaces web. Initialement, le World Wide Web (WWW) était un moyen de partager des documents écrits en HTML (Hypertext Markup Language). Le protocole HTTP (Hypertext Transfer Protocol) a été conçu pour véhiculer ces documents, qui étaient à l’origine essentiellement statiques (leur contenu n’évoluait pas beaucoup au cours du temps). Les pages statiques sont composées de HTML pur contenant éventuellement des graphiques eux aussi statiques (JPG, PNG, par exemple). Les pages dynamiques sont en revanche composées en temps réel à partir de données calculées à partir des informations fournies par l’utilisateur. Pour créer un contenu dynamique, il faut analyser les requêtes HTTP, comprendre leur signification et créer des réponses dans un format que le navigateur saura traiter. L’API des servlets simplifie ce processus en fournissant une vue orientée objet du monde HTTP (HttpRequest, HttpResponse, etc.). Cependant, le modèle des servlets était de trop bas niveau et c’est la raison pour laquelle on utilise désormais les JSP (JavaServer Pages) pour simplifier la création des pages dynamiques. En coulisse, une JSP est une servlet, sauf qu’elle est écrite essentiellement en HTML – avec un peu de Java pour effectuer les traitements. JSF (JavaServer Faces, ou simplement Faces) a été créé en réponse à certaines limitations de JSP et utilise un autre modèle consistant à porter des composants graphiques vers le Web. Inspiré par le modèle Swing et d’autres frameworks graphiques, JSF permet aux développeurs de penser en termes de composants, d’événements, de beans gérés et de leurs interactions plutôt qu’en termes de requêtes, de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
310
Java EE 6 et GlassFish 3
réponses et de langages à marqueurs. Son but est de faciliter et d’accélérer le développement des applications web en fournissant des composants graphiques (comme les zones de texte, les listes, les onglets et les grilles) afin d’adopter une approche RAD (Rapid Application Development). Ce chapitre est une introduction à JSF ; les Chapitres 11 et 12 présentent différentes technologies proposées par Java EE 6 pour créer des interfaces web (JSP, EL et JSTL) et s’intéressent essentiellement à JSF 2.0, qui est la technologie la plus puissante et la plus adaptée à la création d’applications web modernes en Java.
Introduction à JSF Lorsque l’on connaît déjà des frameworks web, l’architecture de JSF est facile à comprendre (voir Figure 10.1). Les applications JSF sont des applications web classiques qui interceptent HTTP via la servlet Faces et produisent du HTML. En coulisse, cette architecture permet de greffer n’importe quel langage de déclaration de page (PDL), de l’afficher sur des dispositifs différents (navigateur web, terminaux mobiles, etc.) et de créer des pages au moyen d’événements, d’écouteurs et de composants, comme en Swing. Ce dernier est un toolkit graphique intégré à Java depuis sa version 1.6 – c’est un framework permettant de créer des applications de bureau (pas des applications web) à l’aide de composants graphiques (widgets) et en utilisant le modèle événement-écouteur pour traiter les entrées des utilisateurs. JSF fournit également un ensemble de widgets standard (boutons, liens hypertextes, cases à cocher, zones de saisie, etc.) et facilite son extension par l’ajout de composants tiers. La Figure 10.1 représente son architecture au niveau le plus abstrait.
Architecture de JSF.
Requête HTTP (Ajax) Réponse HTTP
Faces Servlet
Moteur de rendu
Figure 10.1
XUL JSP XHTML Composant
Convertisseur
Composant Composant
Validateur
Navigation
faces-config.xml (facultatif)
Bean géré
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 311
Cette figure représente les parties importantes de JSF qui rendent cette architecture aussi riche et aussi souple : et faces-config.xml. FacesServlet est la servlet principale de l’application et peut éventuellement être configurée par un fichier descripteur faces-config.xml. Pages et composants. JSF permet d’utiliser plusieurs PDL (Presentation Description Language), comme JSP ou Facelets. Moteurs de rendu. Ils sont responsables de l’affichage d’un composant et de la traduction de la valeur saisie par l’utilisateur en valeur pour le composant. Convertisseurs. Ils effectuent les conversions entre les valeurs de composants (Date, Boolean, etc.) et les valeurs de marqueurs (String), et réciproquement. Validateurs. Ils garantissent que la valeur saisie par l’utilisateur est correcte. Bean géré et navigation. La logique métier s’effectue dans des beans gérés (ou "managés") qui contrôlent également la navigation entre les pages. Support d’Ajax. Comme l’explique le Chapitre 12, JSF 2.0 reconnaît nativement Ajax.
■■ FacesServlet
■■ ■■ ■■ ■■ ■■ ■■
FacesServlet et faces-config.xml
La plupart des frameworks web utilisent le patron de conception MVC (ModèleVue-Contrôleur) – JSF n’y fait pas exception. MVC permet de découpler la vue (la page) et le modèle (les données affichées dans la vue). Le contrôleur prend en charge les actions de l’utilisateur qui pourraient impliquer des modifications dans le modèle et dans les vues. Avec JSF, ce contrôleur est la servlet FacesServlet. Toutes les requêtes de l’utilisateur passent par cette servlet, qui les examine et appelle les différentes actions correspondantes du modèle en utilisant des beans gérés. est intégrée à JSF et le seul moyen de la configurer consiste à utiliser des métadonnées externes. Jusqu’à JSF 1.2, la seule source de configuration était le fichier faces-config.xml. À partir de JSF 2.0, ce fichier est facultatif et la plupart des métadonnées peuvent être définies par des annotations (sur les beans gérés, les convertisseurs, les composants, les moteurs de rendu et les validateurs).
FacesServlet
Pages et composants
Le framework JSF doit envoyer une page sur le dispositif de sortie du client (un navigateur, par exemple) et exige donc une technologie d’affichage appelée PDL.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
312
Java EE 6 et GlassFish 3
Une application JSF peut utiliser plusieurs technologies pour son PDL, comme JSP ou Facelets. Une implémentation conforme à la spécification JSF 2.0 doit inclure une implémentation complète de JSP, qui était le PDL par défaut de JSF 1.1 et JSF 1.2 – JSF 2.0 lui préfère désormais Facelets. JSP et Facelets sont tous les deux formés d’une arborescence de composants (également appelés widgets ou contrôles) fournissant des fonctionnalités spécifiques pour interagir avec l’utilisateur (champs de saisie, boutons, listes, etc.). JSF dispose d’un ensemble standard de composants et permet de créer facilement les vôtres. Pour gérer cette arborescence, une page passe par un cycle de vie complexe (initiali sation, événements, affichage, etc.). Le code du Listing 10.1 est une page Facelets en XHTML qui utilise les marqueurs JSF (xmlns:h="http:// java.sun.com/jsf/html") pour afficher un formulaire avec deux champs de saisie (l’ISBN et le titre d’un livre) et un bouton. Cette page est composée de plusieurs composants JSF : certains n’ont pas d’apparence visuelle, comme ceux qui déclarent l’en-tête (), le corps () ou le formulaire (). D’autres ont une représentation graphique et affichent un label (), un champ de saisie () ou un bouton (). Vous remarquerez que l’on peut également utiliser des marqueurs HTML purs (
,
, , etc.) dans la page. Listing 10.1 : Extrait d’une page XHTML Creates a new book Create a new book
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 313
APress - Beginning Java EE 6
Moteurs de rendu
JSF reconnaît deux modèles de programmation pour afficher les composants : l’implémentation directe et l’implémentation déléguée. Avec le modèle direct, les composants doivent eux-mêmes s’encoder vers une représentation graphique et réciproquement. Avec le modèle délégué, ces opérations sont confiées à un moteur de rendu, ce qui permet aux composants d’être indépendants de la technologie d’affichage (navigateur, terminal mobile, etc.) et donc d’avoir plusieurs représentations graphiques possibles. Un moteur de rendu s’occupe d’afficher un composant et de traduire la saisie d’un utilisateur en valeur de composant. On peut donc le considérer comme un traducteur placé entre le client et le serveur : il décode la requête de l’utilisateur pour initialiser les valeurs du composant et encode la réponse pour créer une représentation du composant que le client pourra comprendre et afficher. Les moteurs de rendu sont organisés en kits de rendu spécialisés dans un type spécifique de sortie. Pour garantir la portabilité de l’application, JSF inclut le support d’un kit de rendu standard et les moteurs de rendu pour HTML 4.01. Les implémentations de JSF peuvent ensuite créer leurs propres kits pour produire du WML (Wireless Markup Language), du SVG (Scalable Vector Graphics), etc. Convertisseurs et validateurs
Lorsque la page est affichée, l’utilisateur peut s’en servir pour entrer des données. Comme il n’y a pas de contraintes sur les types, un moteur de rendu ne peut pas prévoir l’affichage de l’objet. Voilà pourquoi les convertisseurs existent : ils traduisent
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
314
Java EE 6 et GlassFish 3
un objet (Integer, Date, Enum, Boolean, etc.) en chaîne afin qu’il puisse s’afficher et, inversement, construisent un objet à partir d’une chaîne qui a été saisie. JSF fournit un ensemble de convertisseurs pour les types classiques dans le paquetage javax. faces.convert, mais vous pouvez développer les vôtres ou ajouter des types provenant de tierces parties. Parfois, les données doivent également être validées avant d’être traitées par le backend : c’est le rôle des validateurs ; on peut ainsi associer un ou plusieurs validateurs à un composant unique afin de garantir que les données saisies sont correctes. JSF fournit quelques validateurs (LengthValidator, RegexValidator, etc.) et vous permet d’en créer d’autres en utilisant vos propres classes annotées. En cas d’erreur de conversion ou de validation, un message est envoyé dans la réponse à afficher. Beans gérés et navigation
Tous les concepts que nous venons de présenter – qu’est-ce qu’une page, qu’est-ce qu’un composant, comment sont-ils affichés, convertis et validés – sont liés à une page unique, mais les applications web sont généralement formées de plusieurs pages et doivent réaliser un traitement métier (en appelant une couche EJB, par exemple). Le passage d’une page à une autre, l’invocation d’EJB et la synchronisation des données avec les composants sont pris en charge par les beans gérés. Un bean géré est une classe Java spécialisée qui synchronise les valeurs avec les composants, traite la logique métier et gère la navigation entre les pages. On associe un composant à une propriété ou à une action spécifique d’un bean géré en utilisant EL (Expression Language). Voici un extrait de l’exemple précédent :
La première ligne lie directement la valeur du champ de saisie à la propriété book. isbn du bean géré bookController. Cette valeur est synchronisée avec la propriété du bean géré. Un bean géré peut également traiter des événements. La seconde ligne associe un bouton de soumission de formulaire à une action : lorsqu’on aura cliqué sur ce bouton, celui-ci déclenchera un événement sur le bean géré, qui exécutera alors une méthode écouteur (ici, la méthode doCreateBook()). Le Listing 10.2 contient le code du bean BookController. Cette classe Java est annotée par @ManagedBean et possède une propriété, book, qui est synchronisée avec
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 315
la valeur du composant de la page. La méthode doCreateBook() invoque un EJB sans état et renvoie une chaîne permettant de naviguer entre les pages. Listing 10.2 : Le bean géré BookController @ManagedBean public class BookController { @EJB private BookEJB bookEJB; private Book book = new Book(); public String doCreateBook() { book = bookEJB.createBook(book); return "listBooks.xhtml"; } // Getters, setters }
INFO Un bean géré est la classe qui agit comme un contrôleur, navigue d’une page à l’autre, appelle les EJB, etc. Les backing beans sont les objets qui contiennent les propriétés liées aux composants. Dans cet exemple, nous pourrions donc dire que BookController est un bean géré et que l’attribut book est le "backing bean".
Support d’Ajax
Une application web doit fournir une interface riche et rapide. Cette réactivité peut être obtenue en ne modifiant que de petites parties de la page de façon asynchrone, et c’est exactement pour cela qu’Ajax a été conçu. Les versions précédentes de JSF n’offraient pas de solution toute prête et des bibliothèques tierces, comme a4jsf, sont donc venues combler ce manque. À partir de JSF 2.0, le support d’Ajax a été ajouté sous la forme d’une bibliothèque JavaScript (jsf.js) définie dans la spécification. Le code suivant, par exemple, utilise la fonction request pour soumettre un formulaire de façon asynchrone :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
316
Java EE 6 et GlassFish 3
Résumé des spécifications de l’interface web Le développement web en Java a commencé en 1996 avec l’API servlet, un moyen très rudimentaire de créer du contenu web dynamique. Il fallait alors manipuler une API HTTP de bas niveau (HttpServletRequest, HttpServletResponse, HttpSession, etc.) pour rendre les marqueurs HTML à partir d’un code Java. Les JSP sont apparues en 1999 et ont ajouté un niveau d’abstraction supérieur à celui des servlets. En 2004, la première version de JSF a vu le jour et sa version 1.2 a été intégrée à Java EE 5 en 2006. JSF 2.0 fait désormais partie de Java EE 6. Bref historique des interfaces web
Au début du Web, les pages étaient statiques : un utilisateur demandait une ressource (une page, une image, une vidéo, etc.) et le serveur la lui renvoyait – simple, mais très limité. Avec l’augmentation de l’activité commerciale sur le Web, les sociétés se sont trouvées obligées de fournir du contenu dynamique à leurs clients. La première solution a donc consisté à utiliser CGI (Common Gateway Interface) : en utilisant des pages HTML et des scripts CGI écrits dans différents langages (allant de Perl à Visual Basic), une application pouvait accéder à des bases de données et servir ainsi du contenu dynamique. Mais CGI était de trop bas niveau (il fallait gérer les en-têtes HTTP, appeler les commandes HTTP, etc.) et une solution plus élaborée semblait nécessaire. En 1995, Java fit son apparition avec une API d’interface utilisateur indépendante des plates-formes, appelée AWT (Abstract Window Toolkit). Plus tard, avec Java SE 1.2, AWT, qui reposait sur l’interface utilisateur du système d’exploitation, fut remplacé par l’API Swing (qui dessine ses propres widgets en utilisant Java 2D). Dès les premiers jours de Java, le navigateur Netscape Navigator proposa le support de ce nouveau langage, ce qui marqua le début de l’ère des applets – des applications qui s’exécutent sur le client, dans un navigateur. Les applets permettent d’écrire des applications AWT ou Swing et de les intégrer dans une page web, mais leur utilisation ne décolla jamais vraiment. De son côté, Netscape avait également créé un langage de script appelé JavaScript qui s’exécutait directement dans le navigateur : malgré certaines incompatibilités entre les navigateurs, ce langage est toujours très utilisé actuellement car c’est un moyen efficace de créer des applications web dynamiques. Après l’échec des applets, Sun présenta les servlets comme un moyen de créer des clients web dynamiques légers. Les servlets étaient une alternative aux scripts CGI
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 317
car elles offraient une bibliothèque de plus haut niveau pour gérer HTTP, permettaient d’accéder à toute l’API de Java (ce qui incluait donc les accès aux bases de données, les appels distants, etc.) et pouvaient créer une réponse en HTML pouvant s’afficher chez le client. En 1999, Sun présenta JSP comme une amélioration du modèle des servlets mais, comme les JSP mélangeaient du code Java et du code HTML, un framework opensource, Struts, vit le jour en 2001 et proposa une nouvelle approche. Ce framework étendait l’API des servlets et encourageait les développeurs à adopter une architecture MVC. L’histoire récente est remplie d’autres frameworks tentant, chacun, de combler les lacunes des précédents (Tapestry, Wicket, WebWork, DWR, etc.). Aujourd’hui, le framework web conseillé pour Java EE 6 est JSF 2.0, qui rivalise avec Struts et Tapestry dans le monde Java. Rails et Grails rivalisent un peu partout avec JSF, tout comme Java rivalise avec Ruby et Groovy. Par ailleurs, GWT (Google Web Toolkit), Flex et JavaFX peuvent être complémentaires de JSF. JSP 2.2, EL 2.2 et JSTL 1.2
Du point de vue de leur architecture, les JSP sont une abstraction de haut niveau des servlets et ont été implémentées comme une extension de Servlet 2.1. JSP 1.2 et Servlet 2.3 ont été spécifiées ensemble dans la JSR 53 alors qu’en même temps JSTL (JSP Standard Tag Library) faisait l’objet de la JSR 52. Depuis 2002, la spécification JSP 2.0 a évolué séparément dans la JSR 152. En 2006, JSP 2.1 a été ajoutée à Java EE 5 et a facilité l’intégration entre JSF et JSP en introduisant un langage d’expressions (EL) unifié. Avec Java EE 6, les spécifications de JSP et EL sont passées à la version 2.0 – l’une des principales modifications est qu’il est désormais possible d’invoquer une méthode avec EL. JSF 2.0
JSF est une spécification publiée par le JCP (Java Community Process) et a été créée en 2001 par la JSR 127. Sa version de maintenance 1.1 est apparue en 2004 et ce n’est qu’en 2006 que JSF 1.2 a été ajoutée dans Java EE par la JSR 252 (avec Java EE 5). Le plus gros défi de cette version consistait à préserver la compatibilité ascendante et à intégrer JSP avec un EL unifié. Malgré ces efforts, JSF et JSP ne fonctionnent pas très bien ensemble et d’autres frameworks comme Facelets ont donc été introduits pour fournir une alternative aux JSP.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
318
Java EE 6 et GlassFish 3
JSF 2.0 est une version majeure (JSR 314) et est désormais le choix conseillé pour le développement web avec Java EE 6 (JSP reste maintenue, mais n’a reçu aucune amélioration importante avec Java EE 6). JSF 2.0 s’est inspiré de nombreux frameworks web open-source et leur ajoute de nouvelles fonctionnalités. Nouveautés de JSF 2.0
Avec ses nouvelles fonctionnalités, JSF 2.0 est une évolution de 1.2, mais il va également au-delà – les Facelets sont préférées à JSP, par exemple. Parmi les ajouts de JSF 2.0, citons : ■■
une autre technologie de présentation que JSP, reposant sur Facelets ;
■■
un nouveau mécanisme de gestion des ressources (pour les images, les scripts JavaScript, etc.) ;
■■
des portées supplémentaires (portée de vue et portée de composant) ;
■■
le développement plus simple grâce aux annotations pour les beans gérés, les moteurs de rendu, les convertisseurs, les validateurs, etc. ;
■■
la réduction de la configuration XML en exploitant les annotations et la configuration par exception (le fichier faces-config.xml est facultatif) ;
■■
le support d’Ajax ;
■■
le développement de composant facilité.
Implémentation de référence
Mojarra, qui est le nom d’une famille de poissons des Caraïbes, est l’implémentation de référence open-source de JSF 2.0. Elle est disponible via les mises à jour de GlassFish V3 et permet de développer des applications web JSF 2.0 en invoquant une couche métier EJB 3.1 et une couche de persistance JPA 2.0. C’est elle que nous utiliserons dans notre récapitulatif.
Récapitulatif Nous allons écrire une petite application web proposant deux pages web : l’une qui affiche un formulaire afin de pouvoir créer un livre (newBook.xhtml), l’autre qui
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 319
énumère tous les livres présents dans la base (listBooks.xhtml). Ces deux pages utilisent le bean géré BookController pour stocker les propriétés nécessaires et pour la navigation. En utilisant JPA pour la persistance et EJB pour la logique métier, tout s’emboîte : le bean géré délègue tous les traitements métier à BookEJB, qui contient deux méthodes, l’une pour stocker un livre dans une base de données (createBook()), une autre pour récupérer tous les livres (findBooks()). Ce bean de session sans état utilise l’API EntityManager pour manipuler une entité Book. La navigation est très simple : lorsqu’un livre est créé, on affiche la liste. Un lien sur la page de la liste permet de revenir ensuite à la page newBook.xhtml et de créer un autre livre. La Figure 10.2 montre l’interaction des composants de cette application ; ceux-ci assemblés dans un fichier war et déployés sur une instance de GlassFish et une base de données Derby. Figure 10.2
Book
Pages et classes impliquées dans l’application web.
-cm : EntityManager +findBooks() : List +createBook(book : Book) : Book
Cette application web utilisant la structure de répertoires de Maven, les classes, les fichiers et les pages web doivent donc être placés dans les répertoires suivants : ■■ src/main/java
contient l’entité
Book,
l’EJB
BookEJB
et le bean géré
BookController.
contient le fichier l’entité à la base de données.
■■ src/main/resources ■■ src/webapp
persistence.xml
utilisé pour associer
contient les deux pages web newBook.xhtml et listBooks.xhtml.
■■ src/webapp/WEB-INF
contient le fichier web.xml qui déclare la FacesServlet.
■■ pom.xml est un fichier POM (Project Object Model) de Maven décrivant le projet,
ses dépendances et ses extensions.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
320
Java EE 6 et GlassFish 3
L’entité Book
Nous ne détaillerons pas beaucoup le Listing 10.3 car vous devriez maintenant comprendre le code de l’entité Book. Outre les annotations de mapping, notez la requête nommée findAllBooks, qui permet de récupérer les livres à partir de la base de données. Listing 10.3 : Entité Book avec une requête nommée @Entity @NamedQuery(name = "findAllBooks", query = "SELECT b FROM Book b") public class Book { @Id @GeneratedValue private Long id; @Column(nullable = false) private String title; private Float price; @Column(length = 2000) private String description; private String isbn; private Integer nbOfPage; private Boolean illustrations; // Constructeurs, getters, setters }
Comme vous le savez désormais, cette entité doit également être associée à un fichier persistence.xml que, pour simplifier, nous ne reproduirons pas ici. L’EJB BookEJB
Le Listing 10.4 représente un bean de session sans état avec une vue sans interface, ce qui signifie que le client (c’est-à-dire le bean géré) n’a pas besoin d’interface (locale ou distante) et peut invoquer directement l’EJB. Ce dernier obtient par injection une référence à un gestionnaire d’entités grâce auquel il peut rendre persistante une entité Book (avec la méthode createBook()) et récupérer tous les livres de la base (avec la requête nommée findAllBooks). Cet EJB n’a besoin d’aucun descripteur de déploiement. Listing 10.4 : EJB sans état créant et récupérant des livres @Stateless public class BookEJB {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 321
@PersistenceContext(unitName = "chapter10PU") private EntityManager em; public List findBooks() { Query query = em.createNamedQuery("findAllBooks"); return query.getResultList(); } public Book createBook(Book book) { em.persist(book); return book; } }
Le bean géré BookController
L’un des rôles d’un bean géré consiste à interagir avec les autres couches de l’application (la couche EJB, par exemple) ou à effectuer des validations. Dans le Listing 10.5, BookController est un bean géré car il est annoté par @ManagedBean. La seconde annotation, @RequestScoped, définit la durée de vie du bean : ici, il vivra le temps de la requête (on peut également choisir d’autres portées). Ce bean géré contient deux attributs qui seront utilisés par les pages : est la liste des livres récupérés à partir de la base de données, qui doit s’afficher dans la page listBooks.xhtml.
■■ bookList
est l’objet qui sera associé au formulaire (dans la page newBook.xhtml) et rendu persistant.
■■ book
Tout le traitement métier (création et récupération des livres) s’effectue via BookEJB. Le bean géré obtient une référence à l’EJB par injection, via l’annotation @EJB, et dispose de deux méthodes qui seront invoquées par les pages : ■■ doNew().
Cette méthode n’effectue aucun traitement mais permet de naviguer vers newBook.xhtml. Comme nous le verrons au Chapitre 12, il existe plusieurs moyens de naviguer de page en page : le plus simple consiste à renvoyer le nom de la page cible.
■■ doCreateBook().
Cette méthode permet de créer un livre en invoquant l’EJB sans état et en lui passant l’attribut book. Puis elle appelle à nouveau l’EJB pour obtenir tous les livres de la base et stocke la liste dans l’attribut bookList du bean géré. Ensuite, la méthode renvoie le nom de la page vers laquelle elle doit naviguer.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
322
Java EE 6 et GlassFish 3
Le Listing 10.5 contient le code de BookController. Pour plus de lisibilité, nous avons omis les getters et les setters, mais ils sont nécessaires pour chaque attribut (book et bookList). Listing 10.5 : Le bean géré BookController qui invoque l’EJB @ManagedBean @RequestScoped public class BookController { @EJB private BookEJB bookEJB; private Book book = new Book(); private List bookList = new ArrayList(); public String doNew() { return "newBook.xhtml"; } public String doCreateBook() { book = bookEJB.createBook(book); bookList = bookEJB.findBooks(); return "listBooks.xhtml"; } // Getters, setters }
La page newBook.xhtml
La page newBook.xhtml du Listing 10.6 est un formulaire permettant à l’utilisateur de saisir les informations nécessaires à la création d’un livre (ISBN, titre, prix, description, nombre de pages et illustrations). Listing 10.6 : La page newBook.xhtml Creates a new book Create a new book
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 323
APress - Beginning Java EE 6
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
324
Java EE 6 et GlassFish 3
Comme le montre la Figure 10.3, la plupart des informations sont entrées dans des champs de saisie, sauf la description, qui utilise une zone de texte et les illustrations qui sont indiquées par une case à cocher. Figure 10.3 La page newBook.xhtml.
Create a new book ISBN : Tiltle : Price :
Description :
Number of pages : Illustrations :
Create a book APress Beginning Java EE 6
Un clic sur le bouton Create a book provoque l’appel de la méthode doCreateBook() du bean géré et l’EJB stocke alors le livre dans la base de données. Bien que ce code ait été simplifié, il contient l’essentiel. Il déclare d’abord l’espace de noms h pour les composants HTML de JSF : pour les utiliser, il faudra donc les préfixer par cet espace de noms (, , , etc.). Le langage d’expressions EL permet ensuite de lier dynamiquement la valeur du composant à la propriété correspondante du bean géré. Le code suivant, par exemple :
lie la valeur de l’attribut isbn de book avec le contenu de ce composant inputText lors de la soumission du formulaire. bookController étant le nom par défaut du bean géré, ce code est donc équivalent à celui-ci : bookController.getBook().setISBN("ce qui a été saisi")
La page utilise différents composants graphiques dont voici un bref résumé : permet de créer un formulaire dont les valeurs seront envoyées au serveur lorsqu’il sera soumis.
■■
■■ value="ISBN : ")
affiche un label à partir d’une chaîne fixe (comme ou en liant un bean à la propriété.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 325
■■ affiche une zone de texte et lie sa valeur à l’attribut description
du livre. ■■
affiche une case à cocher et la lie à l’attribut illus-
trations (un Boolean). affiche un bouton de soumission de formulaire qui, lorsqu’on cliquera dessus, invoquera la méthode doCreateBook() du bean géré (action="#{bookController.doCreateBook}").
■■
La page listBooks.xhtml
La méthode doCreateBook() du bean géré est appelée lors du clic sur le bouton de soumission de la page newBook.xhtml (voir Figure 10.3) ; elle stocke le livre dans la base et, si aucune exception n’a été lancée, renvoie le nom de la page à afficher ensuite, listBooks.xhtml, qui affiche tous les livres de la base (voir Figure 10.4). Un lien sur cette page permet ensuite de revenir à newBook.xhtml pour créer un autre livre. Figure 10.4 La page
List of the books
listBooks.xhtml. ISBN
Title
1234 234 H2G2 564 694
Price 12.0
Robots 18.5
256 6 56 Dune
Description
Number Of Pages
Illustrations
Scifi IT book
241
false
Asimov Best seller
317
true
529
false
23.25 The trilogy
Create a new book APress Beginning Java EE 6
Le code de la page listBooks.xhtml (voir Listing 10.7) utilise des composants différents, mais le principe est le même que celui de la page précédente. Le composant le plus important est celui qui affiche les données sous la forme d’un tableau :
L’élément est lié à l’attribut bookList du bean géré (une ArrayList de livres) et déclare la variable bk qui permettra de parcourir cette liste. Dans cet élément, on peut ensuite utiliser des expressions comme #{bk.isbn} pour obtenir l’attribut isbn d’un livre. Chaque colonne du tableau est définie par un élément . Le marqueur en bas de la page crée un lien qui, lorsqu’on clique dessus, appelle la méthode doNew() du bean géré (celle-ci permet de revenir à la page newBook.xhtml).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
326
Java EE 6 et GlassFish 3
Listing 10.7 : La page listBooks.xhtml List of the books List of the books
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 10
JavaServer Faces 327
Create a new book APress - Beginning Java EE 6
Configuration avec web.xml
Les applications web sont généralement configurées à l’aide d’un descripteur de déploiement web.xml. Nous avons écrit "généralement" car ce fichier est devenu facultatif avec la nouvelle spécification Servlet 3.0. Cependant, JSF 2.0 reposant sur Servlet 2.5 (et non sur Servlet 3.0), nous devons quand même déployer notre application web avec un descripteur. Les applications JSF ont besoin d’une servlet nommée FacesServlet qui agit comme un contrôleur frontal pour toute l’application. Cette servlet et son association doivent être définies dans le fichier web.xml, comme le montre le Listing 10.8. Listing 10.8 : Fichier web.xml déclarant une FacesServlet Faces Servlet javax.faces.webapp.FacesServlet 1 Faces Servlet *.faces
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
328
Java EE 6 et GlassFish 3
Le descripteur de déploiement associe à la servlet les requêtes d’URL se terminant par .faces, ce qui signifie que toute demande d’une page se terminant par .faces sera traitée par FacesServlet. Compilation et assemblage avec Maven
L’application web doit être compilée et assemblée dans un fichier war (war ). Le fichier pom.xml du Listing 10.9 déclare toutes les dépendances nécessaires à la compilation du code (jsf-api, javax.ejb et javax.persistence) et précise que cette compilation utilisera la version 1.6 du JDK. Avec JSF 2.0, le fichier faces-config.xml n’est plus obligatoire et nous ne l’utilisons pas ici. Listing 10.9 : Fichier pom.xml de Maven pour compiler et assembler l’application web 4.0.0 com.apress.javaee6 chapter10 war 1.0 javax.faces jsf-api 2.0.0 provided org.glassfish javax.ejb 3.0 provided org.eclipse.persistence javax.persistence 1.1.0 provided
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Pour compiler et assembler les classes, il suffit d’ouvrir un interpréteur en ligne de commande dans le répertoire contenant le fichier pom.xml et d’entrer la commande Maven suivante : mvn package
Cette commande crée le fichier chapter10-1.0.war dans le répertoire cible. Ouvrez-le et vous constaterez qu’il contient l’entité Book, le bean BookEJB, le bean géré BookController, les deux descripteurs de déploiement (persistence.xml et web.xml) et les deux pages web (newBook.xhtml et listBooks.xhtml). Déploiement dans GlassFish
L’application web assemblée doit ensuite être déployée dans GlassFish. Après avoir vérifié que Derby s’exécute et écoute sur son port par défaut, ouvrez un interpréteur en ligne de commande, placez-vous dans le répertoire target contenant le fichier chapter10-1.0.war et entrez la commande suivante : asadmin deploy chapter10-1.0.war
Si le déploiement réussit, la commande qui suit devrait renvoyer le nom et le type de l’application. Ici, il y a deux types : web car c’est une application web et ejb car elle contient un EJB : asadmin list-components chapter10-1.0
Exécution de l’application
Lorsque l’application a été déployée, ouvrez votre navigateur et faites-le pointer vers l’URL suivante : http://localhost:8080/chapter10-1.0/newBook.faces
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
330
Java EE 6 et GlassFish 3
Le fichier pointé est newBook.faces, pas newBook.xhtml, car avec l’extension .faces JSF sait qu’il doit traiter la page avant de l’afficher (voir l’association de.faces avec FacesServlet dans le Listing 10.8). Lorsque la page newBook s’affiche, saisissez les informations et cliquez sur le bouton d’envoi du formulaire pour être redirigé sur la page listBooks.
Résumé Aujourd’hui, la compétition entre les interfaces utilisateurs continue de plus belle avec la prolifération des RDA (Rich Desktop Application), des RIA (Rich Internet Application), des applications pour terminaux mobiles, etc. JSF est entré dans la course il y a quelques années déjà et continue de tenir son rang grâce aux nouvelles fonctionnalités de JSF 2.0. L’architecture de JSF repose sur des composants et une API riche permettant de développer des moteurs de rendu, des convertisseurs, des validateurs, etc. Elle reconnaît plusieurs langages, bien que le langage de déclaration de page (PDL) préféré de JSF 2.0 soit Facelets. Les annotations ont été introduites avec JSF 2.0 et sont désormais utilisées dans la plupart des spécifications de Java EE 6. Le Chapitre 11 s’intéresse à la partie présentation de Java EE 6 et couvre les spécifications JSP 2.2, EL 2.2 et JSTL 1.2, il introduit également Facelets et se concentre principalement sur JSF. Le Chapitre 12 aborde tous les aspects dynamiques de la spécification. Vous y apprendrez le fonctionnement de la navigation, les beans gérés et comment écrire votre propre convertisseur et validateur.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
11 Pages et composants Nous vivons dans le monde de l’Internet. Munis d’un backend transactionnel qui traite des milliers de requêtes et communique avec des systèmes hétérogènes au moyen de services web, nous avons maintenant besoin d’une couche de présentation pour interagir avec les utilisateurs – de préférence une interface qui s’exécute dans un navigateur car les navigateurs sont partout et parce que les interfaces web sont plus riches, plus dynamiques et plus simples à utiliser. Les RIA (Rich Internet Applications) sont de plus en plus appréciées car les utilisateurs peuvent profiter de leur connaissance de leurs navigateurs : ils ont besoin de consulter des catalogues de livres et de CD, mais ils veulent également accéder au courrier électronique et à des documents, recevoir des notifications par courrier ou voir une partie de leur navigateur se mettre à jour en fonction des informations reçues du serveur. Ajoutons à cela que la philosophie du Web 2.0 est de faire partager toutes sortes d’informations à des groupes d’amis qui peuvent interagir les uns avec les autres et l’on comprend que les interfaces web soient de plus en plus compliquées à développer. Aux premiers jours de Java, les développeurs émettaient directement du HTML à partir des servlets. Puis nous sommes passés des servlets à JSP (Java Server Pages), qui utilise des marqueurs personnalisés. Désormais, Java EE 6 et sa nouvelle version de JSF simplifie encore plus le développement des interfaces web. Dans ce chapitre, nous présenterons différentes technologies utilisées par Java EE 6 pour créer des pages web. Nous expliquerons d’abord quelques concepts de base comme HTML, CSS et JavaScript, puis nous passerons à JSP, EL et JSTL. Nous introduirons alors Facelets, le langage de présentation (PDL) conseillé pour JSF. Le reste du chapitre s’intéressera à la création d’interfaces web avec JSF ou des composants personnalisés. Le chapitre suivant expliquera comment naviguer entre les pages et interagir avec un backend pour afficher des données dynamiques.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
332
Java EE 6 et GlassFish 3
Pages web Lorsque l’on crée une application web, on affiche généralement un contenu dynamique : une liste d’articles d’un catalogue (des CD et des livres, par exemple), les informations associées à un identifiant de client, un panier virtuel contenant les articles que l’utilisateur veut acheter, etc. Inversement, le contenu statique, comme l’adresse d’un éditeur et les FAQ expliquant comment acheter ou se faire livrer les articles, change rarement, voire jamais – ce contenu peut également être une image, une vidéo ou un dessin. Le but ultime de la création d’une page est son affichage dans un navigateur. Elle doit donc utiliser les langages compris par les navigateurs : HTML, XHTML, CSS et JavaScript. HTML
Hypertext Markup Language (HTML) est le langage qui prédomine dans les pages web. Il repose sur SGML (Standard Generalized Markup Language), un métalangage standard permettant de définir des langages à marqueurs. HTML utilise des balises, ou marqueurs, pour structurer le texte en paragraphes, listes, liens, boutons, zones de texte, etc. Une page HTML est un document texte utilisé par les navigateurs pour présenter du texte et des images : ce sont des fichiers texte portant souvent l’extension .html ou .htm. Une page web est formée d’un contenu, de marqueurs permettant de changer certains aspects de ce contenu et d’objets externes comme des images, des vidéos, du code JavaScript ou des fichiers CSS. La section "Récapitulatif" du chapitre précédent a montré deux pages JSF, dont l’une affichait un formulaire pour créer un nouveau livre. Le Listing 11.1 montre cette page écrite en HTML pur, sans utiliser aucun marqueur JSF. Listing 11.1 : La page newBook.html Create a new book
ISBN :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 333
Title :
Price :
Description :
Number of pages :
Illustrations :
APress - Beginning Java EE 6
Normalement, une page HTML valide commence par un marqueur qui agit comme un conteneur du document. Il est suivi des marqueurs et . Ce dernier contient la partie visible – ici, un tableau constitué de labels et de champs de saisie, et un bouton. Comme vous pouvez le constater, le fichier newBook.html du Listing 11.1 ne respecte pas ces règles mais les navigateurs peuvent afficher des pages HTML non valides (jusqu’à un certain point). Le résultat affiché ressemblera donc à la Figure 11.1. Figure 11.1 Représentation graphique de la page newBook.html.
Create a new book ISBN : Tiltle : Price :
Description :
Number of pages : Illustrations :
Create APress Beginning Java EE 6
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
334
Java EE 6 et GlassFish 3
La représentation graphique de la Figure 11.1 est celle que l’on attendait ; pourtant, le Listing 11.1 n’est pas correctement formaté en termes de XML : ■■
La page n’a pas de marqueurs , ou .
■■
Le marqueur
n’est pas fermé.
La plupart des navigateurs autorisent ce type d’erreur et afficheront correctement le formulaire. En revanche, si vous voulez traiter ce document avec des parsers XML, par exemple, le traitement échouera. Pour en comprendre la raison, étudions une page web qui utilise une structure XML stricte avec XHTML (eXtensible Hypertext Markup Language). XHTML
XHTML a été créé peu de temps après HTML 4.01. Ses racines puisent dans HTML, mais avec une reformulation en XML strict. Ceci signifie qu’un document XHTML est un document XML qui respecte un certain schéma et peut être représenté graphiquement par les navigateurs – un fichier XHTML (qui porte l’extension .xhtml) peut être directement utilisé comme du XML ou être affiché dans un navigateur. Par rapport à HTML, il a l’avantage de permettre une validation et une manipulation du document à l’aide d’outils XML standard (XSL ou eXtensible Stylesheet Language ; XSLT ou XSL Transformations ; etc.). Le Listing 11.2 montre la version XHTML de la page web du Listing 11.1. Listing 11.2 : La page newBook.xhtml Creates a new book Create a new book
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 335
ISBN :
Title :
Price :
Description :
Number of pages :
Illustrations :
APress - Beginning Java EE 6
Notez les différences entre les Listings 11.1 et 11.2 : ce dernier respecte une structure stricte et contient les marqueurs , et ; tous les marqueurs sont fermés, même les vides (chaque
est fermé et on utilise au lieu de ) ; les valeurs des attributs sont toujours entre apostrophes ou entre guillemets (
ou
, mais pas
) ; tous les marqueurs sont en minuscules (
au lieu de
). Le respect strict des règles syntaxiques de XML et les contraintes de schéma rendent XHTML plus facile à maintenir et à traiter que HTML, et c’est la raison pour laquelle il est désormais le langage préféré pour les pages web. CSS
Les navigateurs utilisent des langages côté client comme HTML, XHTML, CSS et JavaScript. CSS (Cascading Style Sheets) sert à décrire la présentation d’un document écrit en HTML ou en XHTML. Il permet de définir les couleurs, les polices, la
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
336
Java EE 6 et GlassFish 3
disposition et les autres aspects de la présentation d’un document et, donc, de séparer son contenu (écrit en XHTML) de sa présentation (écrite en CSS). Comme HTTP, HTML et XHTML, les spécifications de CSS sont édictées par le W3C (World Wide Web Consortium). Supposons, par exemple, que vous vouliez modifier les labels de la page newBook. xhtml pour qu’ils soient tous en italique (font-style: italic;), de couleur bleue (color: #000099;) et dans une taille de police plus grande (font-size: 22px;). Au lieu de répéter ces modifications pour chaque marqueur, il suffit de définir un style CSS (dans un marqueur ) et de lui donner un alias (title et row, par exemple) : la page appliquera alors ce style pour tous les éléments qui utilisent cet alias afin de modifier leur présentation (). Listing 11.3 : La page newBook.xhtml avec des styles CSS Creates a new book .title { font-family: Arial, Helvetica, sans-serif; font-size: 22px; color: #000099; font-style: italic; } .row { font-family: Arial, Helvetica, sans-serif; color: #000000; font-style: italic; } Create a new book
ISBN :
Title :
Price :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 337
Description :
Number of pages :
Illustrations :
APress - Beginning Java EE 6
Dans cet exemple, le code CSS est intégré à la page XHTML mais, dans une vraie application, tous les styles seraient placés dans un fichier distinct qui serait importé par la page web. Le webmestre peut ainsi créer un ou plusieurs fichiers CSS pour différents groupes de pages et les contributeurs de contenu peuvent écrire ou modifier leurs pages sans être concernés par l’aspect final de leurs documents. À la Figure 11.2, tous les labels sont désormais en italique et le titre de la page apparaît en bleu. Figure 11.2 Représentation graphique de la page newBook.xhtml.
Create a new book ISBN : Tiltle : Price :
Description :
Number of pages : Illustrations :
Create APress Beginning Java EE 6
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
338
Java EE 6 et GlassFish 3
DOM
Une page XHTML est un document XML et a donc une représentation DOM (Document Object Model). DOM est une spécification du W3C pour accéder et modifier le contenu et la structure des documents XML ainsi qu’une API abstraite pour interroger, parcourir et manipuler ce type de document – il peut être considéré comme une représentation arborescente de la structure d’un document. La Figure 11.3 montre une représentation DOM de la page newBook.xhtml : la racine est le marqueur html, ses deux fils sont head et body et ce dernier a lui-même un fils table avec une liste de fils tr. Figure 11.3 Représentation arborescente de la page newBook.xhtml.
DOM fournit un moyen standard d’interaction avec les documents XML. Grâce à lui, vous pouvez parcourir l’arbre d’un document et modifier le contenu d’un nœud. Moyennant un peu de code JavaScript, il est possible d’ajouter un comportement dynamique à une page web. Comme nous le verrons au chapitre suivant, Ajax utilise JavaScript sur la représentation DOM d’une page. JavaScript
Les langages que nous avons évoqués jusqu’à maintenant permettent de représenter le contenu statique et les aspects graphiques d’une page web. Cependant, une page doit souvent interagir avec l’utilisateur en affichant du contenu dynamique. Ce contenu dynamique peut être traité par des technologies côté serveur comme JSP ou JSF, mais les navigateurs peuvent également en produire de leur côté en exécutant du code JavaScript. JavaScript est un langage de script pour le développement web côté client. Contrairement à ce que son nom pourrait laisser supposer, il n’a rien à voir avec le langage
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 339
de programmation Java car c’est un langage interprété et faiblement typé. Avec Java Script, il est possible de créer des applications web dynamiques en écrivant des fonctions qui agissent sur le DOM d’une page. Il a été standardisé par l’ECMA (European Computer Manufacturers Association) sous le nom d’ECMAScript. Toute page écrite en respectant les standards XHTML, CSS et JavaScript devrait s’afficher et se comporter de façon quasiment identique avec tout navigateur respectant ces normes. Le Listing 11.4 contient un exemple de code JavaScript manipulant le DOM de la page newBook.xhtml qui affiche un formulaire permettant de saisir des informations sur un livre. Le prix du livre doit être fourni par l’utilisateur côté client avant d’atteindre le serveur : une fonction JavaScript (priceRequired()) permet de valider ce champ en testant s’il est vide ou non. Listing 11.4 : La page newBook.xhtml avec du JavaScript Creates a new book function priceRequired() { if (document.getElementById("price").value == "") { document.getElementById("priceError").innerHTML = "Please, fill the price !"; } Create a new book
ISBN :
Title :
Price :
Description :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
340
Java EE 6 et GlassFish 3
Number of pages :
Illustrations :
APress - Beginning Java EE 6
Dans le Listing 11.4, la fonction priceRequired() est intégrée dans la page au moyen d’un marqueur et est appelée lorsque le champ de saisie du prix perd le focus (représenté par l’événement onblur). Elle utilise l’objet document implicite qui représente le DOM du document XHTML. L’appel getElementById("price") recherche un élément ayant un identifiant price () : on récupère sa valeur et l’on teste si elle est vide. Si c’est le cas, la fonction recherche un autre élément appelé priceError (getElementById("priceError")) et fixe sa valeur à "Please, fill the price !". Cette procédure de validation affichera donc le message de la Figure 11.4 si le prix n’a pas été indiqué. Figure 11.4 La page newBook.html affiche un message d’erreur.
Create a new book ISBN : Tiltle : Price :
Please, fill the price !
Description :
Number of pages : Illustrations :
Create APress Beginning Java EE 6
JavaScript est un langage puissant : nous n’en avons présenté qu’une petite partie pour montrer son interaction avec DOM mais il est important de comprendre qu’une
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 341
fonction JavaScript peut accéder à un nœud de la page (par son nom ou son identifiant) et modifier dynamiquement son contenu côté client. Nous donnerons plus de détails dans la section "Ajax" du prochain chapitre.
Java Server Pages Nous venons de présenter des technologies et des langages, comme XHTML ou CSS, qui représentent le contenu et l’aspect visuel d’une page web. Pour ajouter de l’interactivité et modifier dynamiquement des parties d’une page, vous pouvez utiliser des fonctions JavaScript qui s’exécuteront dans le navigateur mais, la plupart du temps, vous devrez faire appel à une couche métier d’EJB pour afficher des informations provenant d’une base de données. Ce contenu dynamique peut être obtenu à l’aide de JSP (ou JSF avec JSP ou Facelets, comme nous le verrons plus loin). JSP a été ajouté à J2EE 1.2 en 1999 et permet de créer dynamiquement des pages web en réponse à une requête d’un client. Les pages sont traitées sur le serveur et compilées sous forme de servlets. Les pages JSP ressemblent à des pages HTML ou XHTML, sauf qu’elles contiennent des marqueurs spéciaux pour effectuer des traitements sur le serveur et appeler du code Java côté serveur. La plupart du travail de JSP repose sur l’API servlet. Les servlets ont été créées pour permettre à un serveur d’accepter des requêtes HTTP des clients et de créer des réponses dynamiques. Comme JSP, elles peuvent se servir de n’importe quelle ressource serveur comme les EJB, les bases de données, les services web et d’autres composants. Les JSP sont dynamiques parce qu’elles exécutent du code Java pour former une réponse en fonction d’une requête. Les JSP s’exécutent sur un serveur dans un conteneur de servlets et répondent aux requêtes des clients, qui sont des utilisateurs accédant à une application web au moyen d’un navigateur via HTTP, le même protocole que celui qu’ils utilisent pour demander des pages XHTML au serveur. Le conteneur de servlets gère le cycle de vie d’une JSP en : ■■
compilant le code JSP dans une servlet ;
■■
chargeant et initialisant la JSP ;
■■
traitant les requêtes des clients et les faisant suivre à la JSP ;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
342
Java EE 6 et GlassFish 3
■■
renvoyant les réponses aux clients (ces réponses ne contiennent que des marqueurs HTML ou XHTML pour pouvoir s’afficher dans un navigateur) ;
■■
déchargeant la JSP et arrêtant de lui envoyer des requêtes (lorsque le serveur s’arrête, par exemple).
Une page JSP pouvant produire du code HTML ou XHTML, vous pouvez utiliser des extensions différentes pour l’indiquer – .jsp pour HTML et .jspx pour XHTML, par exemple. Examinons le code suivant : Lists all the books Lists all the books
Comme vous pouvez le constater, une JSP valide peut ne contenir que des marqueurs HTML : vous pourriez sauvegarder ce code dans un fichier listBooks.jsp et le déployer dans un conteneur de servlets qui renverrait alors une simple page HTML. En fait, une page JSP ressemble à du HTML, mais elle peut également contenir des marqueurs supplémentaires qui permettent d’ajouter du contenu dynamique afin que les réponses produites dépendent des requêtes. La spécification JSP définit les éléments suivants : ■■
directives ;
■■
scripts ;
■■
actions.
Comme nous le verrons, il existe deux syntaxes pour ces éléments : la syntaxe XML pour les pages XHTML () et la syntaxe JSP, qui n’est pas conforme à XML (). Directives
Les directives fournissent des informations sur la JSP et ne produisent rien. Il existe trois directives : page, include et taglib. Les deux syntaxes possibles sont :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 343
La directive page sert à indiquer les attributs de page tels que le langage de programmation de la page (Java, ici), le type MIME, l’encodage des caractères de la réponse, si la JSP est une page d’erreur, etc.
La directive include sert à inclure une autre page (HTML, XHTML ou JSP) dans la page courante. Vous pouvez l’utiliser pour inclure une page standard (un en-tête ou un pied de page, par exemple) dans plusieurs JSP.
La section "Bibliothèque des marqueurs JSP standard" montre que l’on peut étendre les JSP à l’aide d’une bibliothèque de marqueurs. La directive taglib déclare qu’une page utilise l’une de ces bibliothèques en l’identifiant de façon unique par une URI et un préfixe. Avec la syntaxe XML, ces deux informations sont regroupées dans un espace de noms unique (xmlns). Dans l’exemple suivant, la bibliothèque de marqueurs http://java.sun.com/jstl/core est disponible pour la page via le préfixe c :
Scripts
Les scripts incluent du code Java permettant de manipuler des objets et d’effectuer des traitements affectant le contenu. Ils peuvent utiliser les deux syntaxes suivantes : ceci est une déclaration ceci est un scriptlet ceci est une expression
Les déclarations permettent de déclarer les variables ou les méthodes qui seront disponibles pour tous les autres scripts de la page. La déclaration n’apparaît que dans la JSP traduite (c’est-à-dire dans la servlet), pas dans ce qui est envoyé au client. Le code suivant, par exemple, déclare une instance d’ArrayList qui sera globale à toute la page : ArrayList books = new ArrayList();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
344
Java EE 6 et GlassFish 3
Les scriptlets contiennent du code Java permettant de décrire les actions à réaliser en réponse aux requêtes. Ils peuvent servir à effectuer des itérations ou à exécuter conditionnellement d’autres éléments de la JSP. Comme les déclarations, le code d’un scriptlet n’apparaît que dans la JSP traduite (la servlet). Le code suivant, par exemple, ajoute un objet Book à l’ArrayList déclarée plus haut : books.add(new Book("H2G2", 12f, "Scifi IT book", "1234-234", 241, true));
Les expressions servent à envoyer la valeur d’une expression Java au client. Elles sont évaluées au moment de la réponse et leur résultat est converti en chaîne de caractères puis inséré dans le flux affiché par le navigateur. Le fragment de code suivant, par exemple, affichera l’ISBN d’un livre : book.getIsbn()
Les déclarations, les scriptlets et les expressions doivent contenir du code Java correct. Si vous choisissez d’utiliser la syntaxe XML, leur contenu doit également être du XML valide. Le code suivant, par exemple, déclare une ArrayList de livres en utilisant une classe générique :
Si vous voulez faire la même déclaration dans un format XML strict, vous ne pouvez pas utiliser les symboles < et > car ils sont réservés à l’ouverture et à la fermeture des marqueurs XML. Vous devez donc utiliser une section CDATA (qui signifie Character DATA) afin que le parser XML ne tente pas de l’analyser : books = new ArrayList(); ]]>
Actions
Les actions standard sont définies par la spécification JSP et forcent la page à effectuer certaines actions (inclure des ressources externes, faire suivre une requête vers une autre page ou utiliser les propriétés d’objets Java). Elles ressemblent à des marqueurs HTML car elles sont représentées par des éléments XML préfixés par jsp
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 345
(, , etc.). Le Tableau 11.1 énumère toutes les actions disponibles. Tableau 11.1 : Éléments des actions JSP
Action
Description
useBean
Associe une instance d’objet à une portée donnée et à un identifiant.
setProperty
Fixe la valeur d’une propriété d’un bean.
getProperty
Affiche la valeur d’une propriété d’un bean.
include
Permet d’inclure des ressources statiques et dynamiques dans le même contexte que celui de la page courante.
forward
Fait suivre la requête courante à une ressource statique, une JSP ou une servlet dans le même contexte que celui de la page courante.
param
Utilisé avec les éléments include, forward et params. La page incluse ou transférée verra l’objet requête initial avec les paramètres originaux, plus les nouveaux.
plugin
Permet à une JSP de produire du HTML contenant des constructions spécifiques au navigateur (OBJECT ou EMBED), qui provoquera le téléchargement d’une extension.
params
Passe des paramètres. Fait partie de l’action plugin.
element
Définit dynamiquement la valeur du marqueur d’un élément XML.
attribute
Définit un attribut XML. Fait partie de l’action element.
body
Définit le corps d’un élément XML. Fait partie de l’action element.
Récapitulatif
Tous ces éléments permettent d’invoquer du code Java et toutes sortes de composants (EJB, bases de données, services web, etc.). À titre d’exemple, nous allons créer une page qui affichera une liste de livres stockés dans une ArrayList. Ici, nous n’accéderons pas à une base de données : nous nous contenterons d’une ArrayList initialisée avec un nombre déterminé d’objets Book, que nous parcourrons pour afficher les attributs de chaque livre (ISBN, titre, description, etc.). La Figure 11.5 montre le résultat attendu.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
346
Java EE 6 et GlassFish 3
Figure 11.5 La page listBooks.jsp affiche une liste de livres.
List of the books ISBN
Title
1234 234 H2G2 56 694
Price 12.0
Robots 18.5
256 6 56 Dune
Description
Number Of Pages
Illustrations
Scifi IT book
241
true
Best seller
317
true
529
true
23.25 The trilogy
APress Beginning Java EE 6
Nous avons besoin de plusieurs éléments pour construire cette page. Comme le montre le Listing 11.5, il faut importer les classes java.util.ArrayList et Book avec une directive (). Puis on déclare un attribut books, instance d’ArrayList, afin qu’il soit accessible à toute la page (). Ensuite, un scriplet ajoute des objets livres dans une ArrayList et un autre parcourt cette liste avec une instruction for. Pour afficher les attributs de chaque livre, nous utilisons des éléments expression (). Le Listing 11.5 présente le code complet de cette page. Listing 11.5 : La page listBooks.jsp List all the books Lists all the books
ISBN
Title
Price
Description
Number of pages
Illustrations
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 347
APress - Beginning Java EE 6
Vous remarquerez que l’on peut librement entrelacer du code Java, des marqueurs HTML et du texte. Tout ce qui est dans un scriptlet (entre ) est du code Java qui sera exécuté sur le serveur et tout ce qui est à l’extérieur est du texte qui sera affiché dans la page de réponse. Notez également que le bloc de l’instruction for commence et se termine dans des scriptlets différents. Une JSP peut donc rapidement devenir difficile à relire si l’on commence à trop mélanger des marqueurs HTML avec du code Java. En outre, il n’y a pas de séparation entre la logique métier et la présentation, ce qui complique la maintenance des pages car on mélange deux langages destinés à deux catégories d’intervenants : Java pour les développeurs métiers et XHTML/CSS pour les concepteurs web. Les JSP peuvent utiliser des bibliothèques de marqueurs et le langage d’expressions (EL). JSTL (JSP Standard Tag Library) standardise un certain nombre d’actions classiques en utilisant un langage à marqueurs familier pour les développeurs web, tandis qu’EL utilise une syntaxe plus simple pour effectuer certaines actions des scripts JSP.
Langage d’expressions (EL) Nous venons de voir comment utiliser des scripts pour intégrer du code dans une page JSP. Les instructions EL fournissent une syntaxe plus simple pour effectuer des actions similaires et elles sont plus faciles à utiliser pour ceux qui ne développent pas en Java. Elles permettent d’afficher les valeurs des variables ou d’accéder aux
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
348
Java EE 6 et GlassFish 3
attributs des objets et disposent d’un grand nombre d’opérateurs mathématiques, logiques et relationnels. La syntaxe de base d’une instruction EL est de la forme : ${expr}
où expr est une expression valide qui est analysée et interprétée. À partir de JSP 2.1, vous pouvez également utiliser la syntaxe suivante : #{expr}
Avec les JSP, ${expr} et #{expr} seront analysées et interprétées exactement de la même manière. Avec JSF, en revanche, leur traitement sera différent car, comme nous le verrons, le cycle de vie des pages JSF n’est pas le même que celui des JSP. ${expr} sera évaluée immédiatement (l’expression est compilée en même temps que la JSP et n’est évaluée qu’une fois : lorsque la JSP s’exécute) alors que #{expr} est évaluée plus tard (lorsque sa valeur sera nécessaire). Ces deux EL ayant été unifiés, une page JSP et une page JSF peuvent utiliser les deux moyennant les différences de leurs cycles de vie. Les expressions EL peuvent utiliser la plupart des opérateurs Java habituels : ■■ arithmétiques, +, -, *, / (div), % (mod) ; ■■
relationnels, ==
(eq), != (ne), < (lt), > (gt), = (ge) ;
■■
logiques, &&
■■
autres, (), empty, [], ..
(and), || (or), ! (not) ;
Notez que certains opérateurs ont à la fois une forme symbolique et une forme littérale (> est équivalent à gt, / à div, etc.), ce qui permet de rendre une JSP conforme à XML sans avoir besoin d’utiliser des références d’entités (comme < pour Currency
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 353
La page importe les bibliothèques des marqueurs fondamentaux et de formatage à l’aide de directives . La ligne initialise la variable now avec la date courante, et le marqueur la formate selon différents critères : uniquement l’heure, uniquement la date et les deux ensemble. La ligne fixe la langue américaine et formate la valeur monétaire 20.50 pour obtenir $20.50. La locale est ensuite modifiée pour la Grande-Bretagne afin que cette valeur soit exprimée en livres sterling. Cette JSP produit donc le résultat suivant : Dates 11:31:12 14 may 2009 14/02/09 11:31 14 may 2009 11:31:12 CET Currency $20.50 £20.50
Actions SQL
Les actions SQL de la JSTL permettent d’effectuer des requêtes sur une base de données (insertions, modifications et suppressions), d’accéder aux résultats de ces requêtes et même de mettre en place un contexte transactionnel. Nous avons déjà vu comment accéder à une base de données avec les entités JPA et les EJB mais, pour des applications spécifiques, on a parfois besoin d’accéder à une base à partir d’une page web (pour une application web d’administration non critique utilisée occasionnellement par un unique utilisateur, par exemple) : dans ce cas, les marqueurs de la bibliothèque SQL (voir Tableau 11.5) peuvent se révéler utiles. Tableau 11.5 : Actions SQL
Action
Description
Interroge une base de données.
Exécute une instruction SQL INSERT, UPDATE ou DELETE.
Établit un contexte transactionnel pour les marqueurs et .
Indique la source de données.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
354
Java EE 6 et GlassFish 3
Action
Description
Fixe les valeurs des marqueurs d’emplacements (?) d’une instruction SQL.
Fixe les valeurs des marqueurs d’emplacements (?) d’une instruction SQL pour les valeurs de type java.util.Date.
La page JSP du Listing 11.8 accède à une base de données, récupère toutes les lignes de la table BOOK et les affiche. Listing 11.8 : JSP accédant à une base de données pour récupérer tous les livres Lists all the books Lists all the books select * from book
ISBN
Title
Price
Description
Number of pages
Illustrations
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 355
APress - Beginning Java EE 6
Cette JSP doit d’abord importer la bibliothèque sql avec une directive puis indiquer la source de données (ici, la source par défaut jdbc/__default fournie avec GlassFish). Le résultat de l’exécution de la requête select * from book est stocké dans la variable books et toutes les lignes obtenues se trouvent dans la collection books.rows, que l’on parcourt avec le marqueur . Le résultat sera identique à celui que nous avons déjà présenté à la Figure 11.5. Actions XML
Par certains aspects, la bibliothèque de marqueurs XML ressemble à la bibliothèque des marqueurs fondamentaux : elle permet d’effectuer une analyse XML, d’itérer sur les éléments des collections, d’effectuer des opérations reposant sur les expressions Xpath et de réaliser des transformations à l’aide de documents XSL. Le Tableau 11.6 énumère les actions de cette bibliothèque. Tableau 11.6 : Actions XML
Action
Description
Analyse un document XML.
Évalue une expression XPATH et produit son résultat.
Évalue une expression XPATH et stocke son résultat dans une variable.
Évalue l’expression XPATH si l’expression est vraie.
Fournit plusieurs alternatives exclusives.
Représente une alternative d’une action .
Représente la dernière alternative d’une action .
Évalue une expression XPATH et répète son contenu sur le résultat.
Applique une feuille de style XSLT à un document XML.
Fixe les paramètres de la transformation .
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
356
Java EE 6 et GlassFish 3
Nous avons besoin d’un document XML pour effectuer les transformations réalisées par ces marqueurs. Le fichier books.xml du Listing 11.9 contient une liste de livres stockés sous forme d’éléments et d’attributs XML. Listing 11.9 : Le fichier books.xml H2G2 Scifi IT book Robots Best seller Dune The trilogy
Ce fichier doit être déployé avec la JSP ou être importé par une URL. La page JSP du Listing 11.10 l’analyse et affiche tous les livres. Listing 11.10 : JSP analysant le fichier books.xml et affichant son contenu
ISBN
Title
Price
Description
Number of pages
Illustrations
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 357
Pour commencer, cette JSP doit importer la bibliothèque de marqueurs XML (avec la directive ) puis charger le fichier books.xml dans la variable bookUrl à l’aide du marqueur . bookUrl contient le texte brut, qui doit être analysé par le marqueur – le DOM résultant sera stocké dans la variable doc. Une fois que le document a été analysé, nous pouvons le parcourir et afficher les valeurs en utilisant des expressions XPATH avec (/@isbn représente un attribut XML et /title, un élément). Le résultat obtenu sera identique à celui de la Figure 11.5. Fonctions
Les fonctions ne sont pas des marqueurs mais sont quand même définies dans la spécification JSTL. Elles peuvent être utilisées avec EL et sont principalement employées pour traiter les chaînes de caractères : ${fn:contains("H2G2", "H2")}
Ce code teste si une chaîne contient une sous-chaîne particulière : ici, cet appel renverra true car H2G2 contient H2. L’appel suivant renvoie la longueur d’une chaîne ou d’une collection : dans cet exemple précis son résultat sera 4. ${fn:length("H2G2")}
Une JSP peut afficher les résultats des fonctions (avec un marqueur ) ou les utiliser dans un test ou une boucle : H2G2 is four caracters long
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
358
Java EE 6 et GlassFish 3
Le Tableau 11.7 énumère toutes les fonctions fournies par la bibliothèque. Tableau 11.7 : Fonctions
Fonction
Description
fn:contains
Teste si une chaîne contient la sous-chaîne indiquée.
fn:containsIgnoreCase
Idem, sans tenir compte de la casse.
fn:endsWith
Teste si une chaîne se termine par le suffixe indiqué.
fn:escapeXml
Protège les caractères pouvant être interprétés comme du XML.
fn:indexOf
Renvoie l’indice de la première occurrence d’une sous-chaîne dans une chaîne.
fn:join
Joint tous les éléments d’un tableau pour former une chaîne.
fn:length
Renvoie le nombre d’éléments d’une collection ou le nombre de caractères d’une chaîne.
fn:replace
Renvoie une chaîne où toutes les occurrences de la souschaîne indiquée ont été remplacées par une autre chaîne.
fn:split
Découpe une chaîne pour obtenir un tableau de sous-chaînes.
fn:startsWith
Teste si une chaîne commence par le préfixe indiqué.
fn:substring
Renvoie une sous-chaîne.
fn:substringAfter
Renvoie la sous-chaîne située après la sous-chaîne indiquée.
fn:substringBefore
Renvoie la sous-chaîne située avant la sous-chaîne indiquée.
fn:toLowerCase
Convertit une chaîne en minuscules.
fn:toUpperCase
Convertit une chaîne en majuscules.
fn:trim
Supprime les espaces aux deux extrémités d’une chaîne.
Facelets Lorsque JSF a été créé, le but consistait à réutiliser JSP comme PDL principal car elle faisait déjà partie de Java EE. JSP utilisait EL et JSTL et l’idée consistait donc à réutiliser toutes ces technologies avec JSF. JSP est un langage de page et JSF, une couche de composants située au-dessus. Cependant, les cycles de vie de JSP et de JSF ne s’accordent pas. Pour produire une réponse, les marqueurs de JSP sont traités de haut en bas, dans l’ordre où ils apparaissent ; alors que le cycle de vie de JSF est un peu plus compliqué puisque la production de l’arborescence des composants et
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 359
leur traitement ont lieu dans des phases différentes. Facelets entre donc en jeu pour correspondre au cycle de vie des JSF. Facelets est une alternative open-source à JSP. À la différence de JSP, EL et JSTL, Facelets n’a pas de JSR et ne fait pas partie de Java EE : c’est un remplaçant de JSP qui fournit une alternative XML (XHTML) pour les pages d’une application JSF. Facelets ayant été conçu en tenant compte de JSF, il fournit un modèle de programmation plus simple que celui de JSP. Facelets dispose d’une bibliothèque de marqueurs permettant d’écrire l’interface utilisateur et reconnaît en partie les marqueurs JSTL. Bien que la bibliothèque de fonctions soit intégralement disponible, seuls quelques marqueurs fondamentaux (c:if, c:forEach, c:catch et c:set) sont reconnus. La caractéristique essentielle de Facelets est son mécanisme de templates de pages, qui est bien plus souple que celui de JSP. Il permet également de créer des composants personnalisés utilisables dans le modèle arborescent de JSF. La bibliothèque des marqueurs Facelets est définie par l’URI http://java.sun. com/jsf/facelets et utilise généralement le préfixe ui. Ces marqueurs sont présentés dans le Tableau 11.8. Tableau 11.8 : Marqueurs Facelets
Marqueur
Description
Définit une composition qui utilise éventuellement un template. Plusieurs compositions peuvent utiliser le même template.
Crée un composant.
Capture les informations de débogage.
Définit le contenu inséré dans une page par un template.
Décore une partie du contenu d’une page.
Ajoute un fragment de page.
Encapsule et réutilise un contenu dans plusieurs pages XHTML, comme le marqueur de JSP.
Insère un contenu dans un template.
Passe des paramètres à un fichier inclus par ou à un template.
Alternative à .
Supprime du contenu d’une page.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
360
Java EE 6 et GlassFish 3
Les pages Facelets sont écrites en XHTML et ressemblent à ce que nous avons déjà vu avec le Listing 11.2. Nous verrons comment utiliser les templates dans la section qui leur est consacrée dans ce chapitre.
JavaServer Faces Nous avons commencé ce chapitre en présentant JSP, JSTL et Facelets car ces technologies sont essentielles pour comprendre JSF 2.0. Vous devez choisir un PDL pour écrire une page JSF : jusqu’à JSF 1.2, le PDL préféré était JSP mais, les cycles de vie des pages JSP et JSF étant différents, Facelets (pages en XHTML) est désormais le PDL conseillé. Ceci ne signifie pas que vous ne puissiez pas utiliser JSP mais, si vous le faites, vous serez très limité en termes de marqueurs et de fonctionnalités (vous n’aurez pas accès aux marqueurs de Facelets, notamment). Le cycle de vie des JSP est relativement simple. Un source JSP est compilé en servlet, à laquelle le conteneur web passera les requêtes HTTP qu’il reçoit. La servlet traite la requête puis renvoie la réponse au client. JSF a un cycle de vie plus complexe, et c’est la raison pour laquelle, dans la section "Langage d’expressions (EL)", nous avons présenté deux syntaxes différentes : l’une utilisant le symbole Dollar (${expression}), l’autre, le symbole dièse (#{expression}). $ est utilisé pour les expressions qui peuvent s’exécuter immédiatement (lorsque l’on sait que les objets de l’expression sont disponibles) alors que # sert aux expressions différées (qui doivent être évaluées plus tard dans le cycle de vie). Avec JSF 1.2, les deux syntaxes étaient unifiées, mais cela impliquait trop de confusions et beaucoup d’erreurs. Le Tableau 11.9 énumère toutes les bibliothèques de marqueurs auxquelles a accès une page utilisant Facelets comme PDL. On y retrouve la bibliothèque fondamentale et celle des fonctions que nous avons présentées dans la section consacrée à JSTL, les marqueurs Facelets (avec le préfixe ui) et les nouveaux marqueurs fondamentaux, html et composites de JSF. Les autres marqueurs JSTL (formatage, SQL et XML) ne sont pas reconnus par Facelets. Intéressons-nous aux composants HTML de JSF permettant de créer des interfaces web riches. Le Chapitre 12 présentera l’essentiel de la bibliothèque fondamentale de JSF avec les convertisseurs et les validateurs, mais examinons d’abord le cycle de vie d’une page JSF.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 361
Tableau 11.9 : Bibliothèques de marqueurs autorisés avec le PDL Facelets
URI
Préfixe classique
Description
http://java.sun.com/jsf/html
h
Contient les composants et leurs rendus HTML (h:commandButton, h:commandLink, h:inputText, etc.).
http://java.sun.com/jsf/core
f
Contient les actions personnalisées indépendantes d’un rendu particulier (f:selectItem, f:validateLength, f:convertNumber, etc.).
http://java.sun.com/jsf/facelets
ui
Marqueurs pour le support des templates.
http://java.sun.com/jsf/composite
composite
Sert à déclarer et à définir des composants composites.
http://java.sun.com/jsp/jstl/core
c
Les pages Facelets peuvent utiliser certains marqueurs fondamentaux (c:if, c:forEach et c:catch).
http://java.sun.com/jsp/jstl/ functions
fn
Les pages Facelets peuvent utiliser tous les marqueurs de fonctions.
Cycle de vie
Une page JSF est une arborescence de composants avec un cycle de vie spécifique qu’il faut bien avoir compris pour savoir à quel moment les composants sont validés ou quand le modèle est mis à jour. Un clic sur un bouton provoque l’envoi d’une requête du navigateur vers le serveur et cette requête est traduite en événement qui peut être traité par l’application sur le serveur. Toutes les données saisies par l’utilisateur passent par une étape de validation avant que le modèle soit mis à jour et que du code métier soit appelé. JSF se charge alors de vérifier que chaque composant graphique (composants parent et fils) est correctement rendu par le navigateur. Les différentes phases du cycle de vie d’une page JSF sont représentées par la Figure 11.6.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
362
Java EE 6 et GlassFish 3
Figure 11.6 Cycle de vie de JSF.
Fin de la réponse Requête
Restauration de la vue
Application des valeurs de la requête
Traitement des événements
Fin de la réponse Fin de la réponse
Réponse
Traitement des événements
Fin de la réponse Validations
Traitement des événements
Fin de la réponse Appel de l'application
Traitement des événements
Modification des valeurs du modèle
Erreurs de conversion ou de validation
Le cycle de vie de JSF se divise en six phases : 1. Restauration de la vue. JSF trouve la vue cible et lui applique les entrées de l’utilisateur. S’il s’agit de la première visite, JSF crée la vue comme un composant UIViewRoot (racine de l’arborescence de composants, qui constitue une page particulière). Pour les requêtes suivantes, il récupère l’UIViewRoot précédemment sauvegardée pour traiter la requête HTTP courante. 2. Application des valeurs de la requête. Les valeurs fournies avec la requête (champs de saisie, d’un formulaire, valeurs des cookies ou à partir des en-têtes HTTP) sont appliquées aux différents composants de la page. Seuls les composants UI modifient leur état, non les objets métiers qui forment le modèle. 3. Validations. Lorsque tous les composants UI ont reçu leurs valeurs, JSF traverse l’arborescence de composants et demande à chacun d’eux de s’assurer que la valeur qui leur a été soumise est correcte. Si la conversion et la validation réussissent pour tous les composants, le cycle de vie passe à la phase suivante. Sinon il passe à la phase de Rendu de la réponse avec les messages d’erreur de validation et de conversion appropriés. 4. Modification des valeurs du modèle. Lorsque toutes les valeurs des composants ont été affectées et validées, les beans gérés qui leur sont associés peuvent être mis à jour. 5. Appel de l’application. On peut maintenant exécuter la logique métier. Les actions qui ont été déclenchées seront exécutées sur le bean géré. La navigation entre en jeu car c’est la valeur qu’elle renvoie qui déterminera la réponse.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 363
6. Rendu de la réponse. Le but principal de cette phase consiste à renvoyer la réponse à l’utilisateur. Son but secondaire est de sauvegarder l’état de la vue pour pouvoir la restaurer dans la phase de restauration si l’utilisateur redemande la vue. Le thread d’exécution d’un cycle requête/réponse peut passer ou non par chacune de ces étapes en fonction de la requête et de ce qui se passe au cours de son traitement : en cas d’erreur, notamment, le flux d’exécution passe directement à la phase de Rendu de la réponse. Quatre de ces étapes peuvent produire des messages d’erreur : Application des valeurs de la requête (2), Validations (3), Modification des valeurs du modèle (4) et Appel de l’application (5). Avec ou sans erreur, la phase de Rendu de la réponse (6) renvoie toujours le résultat à l’utilisateur. Composants HTML standard
L’architecture JSF est conçue pour être indépendante de tout protocole ou langage à marqueurs particulier et pour écrire des applications pour les clients HTML qui communiquent via HTTP. Une interface utilisateur pour une page web donnée est créée en assemblant des composants qui fournissent des fonctionnalités spécifiques afin d’interagir avec l’utilisateur (labels, cases à cocher, etc.) – JSF met à disposition un certain nombre de classes composants couvrant la plupart des besoins classiques. Une page est une arborescence de classes héritant de javax.faces.component. UIComponent et ayant des propriétés, des méthodes et des événements. La racine de l’arbre est une instance de UIViewRoot et tous les autres composants respectent une relation d’héritage. Intéressons-nous à ces composants dans une page web. Commandes
Les commandes sont les contrôles sur lesquels l’utilisateur peut cliquer pour déclencher une action. Ces composants sont généralement représentés sous forme de boutons ou de liens hypertextes, indiqués par les marqueurs du Tableau 11.10. Tableau 11.10 : Marqueurs de commandes
Marqueur
Description
Représente un élément HTML pour un bouton de type submit ou reset.
Représente un élément HTML pour un lien agissant comme un bouton submit. Ce composant doit être placé dans un formulaire.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
364
Java EE 6 et GlassFish 3
Le code suivant crée des boutons de soumission et de réinitialisation du formulaire, des images cliquables ou des liens permettant de déclencher un événement : A hyperlink
Par défaut, un commandButton est de type submit. Pour utiliser une image comme bouton, utilisez non pas l’attribut value (qui est le nom du bouton) mais l’attribut image pour indiquer le chemin d’accès du fichier image que vous souhaitez afficher. Voici le résultat graphique que produira ce code :
Les boutons et les liens ont tous les deux un attribut action permettant d’appeler une méthode d’un bean géré. Voici comment invoquer la méthode doNew() du bookController, par exemple : Create a new book
Entrées
Les entrées sont des composants qui affichent leur valeur courante et permettent à l’utilisateur de saisir différentes informations textuelles. Il peut s’agir de champs de saisie, de zones de texte ou de composants pour entrer un mot de passe ou des données cachées. Leurs marqueurs sont énumérés dans le Tableau 11.11.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 365
Tableau 11.11 : Marqueurs d’entrées
Marqueur
Description
Représente un élément d’entrée HTML de type caché (non affiché).
Représente un élément d’entrée HTML de type mot de passe. Pour des raisons de sécurité, tout ce qui a été saisi ne sera pas affiché, sauf si la propriété redisplay vaut true.
Représente un élément d’entrée HTML de type texte.
Représente une zone de texte HTML.
De nombreuses pages web contiennent des formulaires pour que l’utilisateur puisse saisir des données ou se connecter en fournissant un mot de passe. En outre, les composants d’entrée utilisent plusieurs attributs permettant de modifier leur longueur, leur contenu ou leur aspect :
Tous les composants ont un attribut value pour fixer leur valeur par défaut. L’attribut maxLength permet de s’assurer que le texte saisi ne dépasse pas une longueur donnée et l’attribut size modifie la taille par défaut du composant. Le code précédent produira donc le résultat suivant :
An input text A longer input text A text area
Sorties
Les composants de sortie affichent une valeur qui peut éventuellement avoir été obtenue à partir d’un bean géré, une expression valeur ou un texte littéral. L’utilisateur ne peut pas modifier ce contenu car il n’est qu’en lecture seule. Le Tableau 11.12 énumère les marqueurs de sortie disponibles.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
366
Java EE 6 et GlassFish 3
Tableau 11.12 : Marqueurs de sortie
Marqueur
Description
Produit un élément de HTML
Produit un élément
Produit un texte littéral
La plupart des pages web affichent du texte. Pour ce faire, vous pouvez utiliser des éléments HTML classiques mais, grâce à EL, les marqueurs de sortie de JSF permettent d’afficher le contenu d’une variable liée à un bean géré. Vous pouvez ainsi afficher du texte avec et des liens hypertextes avec . Notez que les marqueurs et sont différents car le dernier affiche le lien sans invoquer de méthode lorsqu’on clique dessus – il crée simplement un lien externe ou une ancre. A link
Ce code n’a pas de représentation graphique particulière, il produira le code HTML suivant : The title of the book A text A link
Sélections
Les composants de sélection (voir Tableau 11.13) permettent de choisir une ou plusieurs valeurs dans une liste. Graphiquement, ils sont représentés par des cases à cocher, des boutons radio, des listes ou des combo box. Tableau 11.13 : Marqueurs de sélection
Marqueur
Description
Produit une case à cocher représentant une valeur booléenne unique. Cette case sera initialement cochée ou décochée selon la valeur de sa propriété checked.
Produit une liste de cases à cocher.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 367
Tableau 11.13 : Marqueurs de sélection (suite)
Marqueur
Description
Produit un composant à choix multiples, dans lequel on peut choisir une ou plusieurs options.
Produit un élément HTML.
Produit un composant à choix unique, dans lequel on ne peut choisir qu’une seule option.
Produit un composant à choix unique, dans lequel on ne peut choisir qu’une seule option. N’affiche qu’une option à la fois.
Produit une liste de boutons radio.
Les marqueurs de ce tableau ont une représentation graphique mais ont besoin d’imbriquer d’autres marqueurs ( ou ) pour contenir les options disponibles. Pour représenter une combo box contenant une liste de genres littéraires, par exemple, il faut imbriquer un ensemble de marqueurs dans un marqueur :
La Figure 11.7 montre toutes les représentations possibles de ces marqueurs. Certaines listes sont à choix multiples, d’autres n’autorisent qu’un seul choix ; comme tous les autres composants, vous pouvez directement lier la valeur d’un bean géré (List, Set, etc.) à l’une de ces listes.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Figure 11.7 Les différentes représentations graphiques des listes.
Graphiques
Il n’existe qu’un seul composant pour afficher les images : . Ce marqueur utilise un élément HTML pour afficher une image que les utilisateurs n’auront pas le droit de manipuler. Ses différents attributs permettent de modifier la taille de l’image, de l’utiliser comme image cliquable, etc. Une image peut être liée à une propriété d’un bean géré et provenir d’un fichier sur le système ou d’une base de données. Le code suivant, par exemple, affiche une image en modifiant sa taille :
Grilles et tableaux
Les données doivent très souvent être affichées sous forme de tableau. JSF fournit donc le marqueur permettant de parcourir une liste d’éléments afin de produire un tableau (reportez-vous au code de la page listBooks.xhtml qui apparaît dans le Listing 10.7 du chapitre précédent). Les tableaux permettent également de créer une interface utilisateur "en grille". Dans ce cas, vous pouvez utiliser les marqueurs et pour disposer les composants (voir Tableau 11.14).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 369
Tableau 11.14 : Marqueurs de grilles et de tableaux
Marqueur
Description
Représente un ensemble de données qui seront affichées dans un élément
de HTML.
Produit une colonne de données dans un composant .
Produit un élément
HTML.
Conteneur de composants pouvant s’imbriquer dans un .
À la différence de , le marqueur n’utilise pas de modèle de données sous-jacent pour produire les lignes de données – c’est un conteneur permettant de produire les autres composants JSF dans une grille de lignes et de colonnes. Vous pouvez préciser le nombre de colonnes : déterminera le nombre de lignes nécessaire (l’attribut column indique le nombre de colonnes à produire avant de débuter une nouvelle ligne). Le code suivant, par exemple, produira une grille de trois colonnes sur deux lignes :
Pour combiner plusieurs composants dans la même colonne, utilisez un qui produira ses fils comme un seul composant. Vous pouvez également définir un en-tête et un pied à l’aide du marqueur spécial .
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
370
Java EE 6 et GlassFish 3
Les deux grilles que nous venons de décrire auront les représentations graphiques suivantes. La première n’aura ni en-tête ni pied ; la seconde aura les deux : One
Two
Three
Four
Five
Six
Header One
Two
Three
Four
Five
Six
Footer
Messages d’erreur
Les applications peuvent parfois lancer des exceptions en réponse à des données mal formatées ou pour certaines raisons techniques. Dans ce cas, il ne faut afficher dans l’interface utilisateur que ce qui est nécessaire afin d’attirer son attention et pour qu’il puisse corriger le problème. Le mécanisme de gestion des messages d’erreur passe par l’utilisation des marqueurs et (voir Tableau 11.15). est lié à un composant précis, tandis que permet de définir un message global pour tous les composants de la page. Tableau 11.15 : Marqueurs de messages
Marqueur
Description
Affiche un seul message d’erreur.
Affiche tous les messages d’erreur en attente.
Les messages peuvent avoir des importances différentes (INFO, WARN, ERROR et FATAL) correspondant chacune à un style CSS (respectivement infoStyle, warnStyle, errorStyle et fatalStyle) : chaque type de message sera donc affiché dans un style différent. Le code suivant, par exemple, affichera tous les messages en rouge : Enter a title:
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 371
Cette page affichera un champ de saisie lié à une propriété d’un bean géré. Ici, cette propriété est obligatoire : un message d’erreur s’affichera si l’utilisateur clique sur le bouton Save alors que ce champ est vide. Validation Error : Value is required. Enter a tiltle :
Save
Informations complémentaires
Les marqueurs énumérés dans le Tableau 11.16 n’ont pas de représentation graphique mais possèdent un équivalent HTML. Bien que les marqueurs natifs de HTML puissent être utilisés directement sans problème, les marqueurs JSF ont des attributs supplémentaires qui facilitent le développement. Vous pouvez, par exemple, ajouter une bibliothèque JavaScript à l’aide du marqueur HTML standard , mais le marqueur de JSF permet d’utiliser la nouvelle gestion des ressources, comme nous le verrons dans la section "Gestion des ressources". Tableau 11.16 : Marqueurs divers
Marqueur
Description
Produit un élément HTML.
Produit un élément HTML.
Produit un élément HTML.
Produit un élément HTML.
Produit un élément HTML.
Templates
Une application web typique contient plusieurs pages partageant toutes le même aspect, un en-tête, un pied de page, un menu, etc. Facelets permet de définir une disposition de page dans un fichier template qui pourra être utilisé par toutes les pages : ce fichier définit les zones (avec le marqueur ) dont le contenu sera remplacé grâce aux marqueurs , , ou des pages clientes. Le Tableau 11.17 énumère les marqueurs de templates.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
372
Java EE 6 et GlassFish 3
Tableau 11.17 : Marqueurs de templates
Marqueur
Description
Définit une composition utilisant éventuellement un template. Le même template peut être utilisé par plusieurs compositions.
Définit un contenu qui sera inséré dans l’élément correspondant du template.
Permet de décorer le contenu d’une page.
Ajoute un fragment à une page.
Définit un point d’insertion dans un template dans lequel on pourra ensuite insérer un contenu placé dans un marqueur .
À titre d’exemple, réutilisons la page qui affichait un formulaire pour créer un livre (voir Figure 11.1). Nous pourrions considérer que le titre est l’en-tête de la page et que le texte "Apress - Beginning Java EE 6" est le pied de page. Le contenu du template layout.xml ressemblerait donc au code du Listing 11.11. Listing 11.11 : Le fichier layout.xml est un template Facelets Default title Default title Default content APress - Beginning Java EE 6
Le template doit d’abord définir la bibliothèque de marqueurs nécessaire (xmlns:ui="http://java.sun.com/ jsf/facelets"). Puis il utilise un marqueur
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 373
pour insérer un attribut title dans les marqueurs HTML . Le corps de la page sera inséré dans l’attribut content.
et
Pour utiliser ce template, la page newBook.xhtml présentée dans le Listing 11.12 doit le déclarer (). Puis le principe consiste à lier les attributs définis par les marqueurs de la page à ceux des marqueurs du template. Dans notre exemple, le titre de la page, "Create a new book", est stocké dans la variable title (avec ) et sera donc lié au marqueur correspondant dans le template (). Il en va de même pour le reste de la page, qui est inséré dans la variable content (). Listing 11.12 : La page newBook.xhtml utilise le template layout.xml
Create a new book
ISBN :
Title :
Price :
Description :
Number of pages :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
374
Java EE 6 et GlassFish 3
Illustrations :
La Figure 11.8 montre que le résultat obtenu est identique à celui de la Figure 11.1. Figure 11.8
Create a new book
La page newBook.html avec le template layout.xhtml.
ISBN : Tiltle : Price :
Description :
Number of pages : Illustrations :
Create a book APress Beginning Java EE 6
Gestion des ressources
La plupart des composants ont besoin de ressources externes pour s’afficher correctement : a besoin d’une image, peut également afficher une image pour représenter le bouton, référence un fichier JavaScript et les composants peuvent également appliquer des styles CSS. Avec JSF, une ressource est un élément statique qui peut être transmis aux éléments afin d’être affiché (images) ou traité (JavaScript, CSS) par le navigateur. Les versions précédentes de JSF ne fournissaient pas de mécanisme particulier pour servir les ressources : lorsque l’on voulait en fournir une, il fallait la placer dans le répertoire WEB-INF pour que le navigateur du client puisse y accéder. Pour la modifier, il fallait remplacer le fichier et, pour gérer les ressources localisées (une image avec un texte anglais et une autre avec un texte français, par exemple),
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 375
il fallait utiliser des répertoires différents. JSF 2.0 permet désormais d’assembler directement les ressources dans un fichier jar séparé, avec un numéro de version et une locale, et de le placer à la racine de l’application web, sous le répertoire suivant : resources/
ou : META-INF/resources/
est formé de plusieurs sous-répertoires indiqués sous la
forme : [locale/][nomBib/][versionBib/]nomRessource[/versionRessource]
Tous les éléments entre crochets sont facultatifs. La locale est le code du langage, suivi éventuellement d’un code de pays (en, en_US, pt, pt_BR). Comme l’indique cette syntaxe, vous pouvez ajouter un numéro de version à la bibliothèque ou à la ressource elle-même. Voici quelques exemples : book.gif en/book.gif en_us/book.gif en/myLibrary/book.gif myLibrary/book.gif myLibrary/1_0/book.gif myLibrary/1_0/book.gif/2_3.gif
Vous pouvez ensuite utiliser une ressource – l’image book.gif, par exemple – directement dans un composant ou en précisant le nom de la bibliothèque (library="myLibrary"). La ressource correspondant à la locale du client sera automatiquement choisie.
Composants composites
Tous les composants que nous venons de présenter font partie de JSF et sont disponibles dans toutes les implémentations qui respectent la spécification. En outre, comme elle repose sur des composants réutilisables, JSF fournit le moyen de créer et d’intégrer aisément dans les applications ses propres composants ou des composants provenant de tierces parties.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
376
Java EE 6 et GlassFish 3
Nous avons déjà mentionné le fait que tous les composants héritaient, directement ou indirectement, de la classe javax.faces.component.UIComponent. Avant JSF 2.0, pour créer son propre composant il fallait étendre la classe component la plus proche du nouveau composant (UICommand, UIGraphic, UIOutput, etc.), la déclarer dans le fichier faces-config.xml et fournir un descripteur de marqueur et une représentation. Ces étapes étaient complexes : d’autres frameworks comme Facelets ont alors montré qu’il était possible de créer plus simplement des composants puissants. Le but des composants composites est de permettre aux développeurs de créer de vrais composants graphiques réutilisables sans avoir besoin d’écrire du code Java ou de mettre en place une configuration XML. Cette nouvelle approche consiste à créer une page XHTML contenant les composants, puis de l’utiliser comme composant dans d’autres pages. Cette page XHTML est alors vue comme un véritable composant supportant des validateurs, des convertisseurs et des écouteurs. Les composants composites peuvent contenir n’importe quel marqueur valide et utiliser des templates. Ils sont traités comme des ressources et doivent donc se trouver dans les nouveaux répertoires standard des ressources. Le Tableau 11.18 énumère les marqueurs permettant de les créer et de les définir. Tableau 11.18 : Marqueurs pour la déclaration et la définition des composants composites
Marqueur
Description
Déclare le contrat d’un composant.
Définit l’implémentation d’un composant.
Déclare un attribut pouvant être fourni à une instance du composant. Un marqueur peut en contenir plusieurs.
Déclare que ce composant supporte une facet.
Utilisé dans un marqueur .
La facet insérée sera représentée dans le composant.
Utilisé dans un marqueur . Tous
les composants fils ou les templates seront insérés dans la représentation de ce composant.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 377
Tableau 11.18 : Marqueurs pour la déclaration et la définition des composants composites (suite)
Marqueur
Description
Le composant dont le contrat est déclaré par le marqueur dans lequel est imbriqué cet élément devra exposer une implémentation de ValueHolder.
Le composant dont le contrat est déclaré par le marqueur dans lequel est imbriqué cet élément devra exposer une implémentation d’editableValueHolder.
Le composant dont le contrat est déclaré par le marqueur dans lequel est imbriqué cet élément devra exposer une implémentation de l’interface actionSource.
Étudions un exemple montrant la facilité avec laquelle on peut créer un composant graphique et l’utiliser dans d’autres pages. Dans les chapitres précédents, l’application CD-BookStore vendait deux sortes d’articles : des livres et des CD. Au Chapitre 3, nous les avons représentés comme trois objets différents : Book et CD héritaient d’Item. Ce dernier contenait les attributs communs (title, price et description) alors que Book et CD contenaient des attributs spécialisés (isbn, publisher, nbOfPage et illustrations pour Book ; musicCompany, numberOfCDs, totalDuration et gender pour CD). Pour que l’application web puisse créer de nouveaux livres et de nouveaux CD, on a donc besoin de deux formulaires différents, mais les attributs d’Item pourraient être dans une page distincte qui agirait comme un composant à part entière. La Figure 11.9 montre ces deux formulaires. Nous allons donc créer un composant composite contenant deux champs de saisie (pour le titre et le prix) et une zone de texte (pour la description). L’écriture d’un composant avec JSF 2.0 est relativement proche de celle que l’on utilise pour Java : on écrit d’abord une interface, (voir Listing 11.13), qui sert de point d’entrée pour le composant – elle décrit les noms et les paramètres qu’il utilise. Puis on passe à l’implémentation : est le corps du composant écrit en XHTML avec des marqueurs JSF ou des templates.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
378
Java EE 6 et GlassFish 3
Create a new CD
Create a new book
Tiltle :
Tiltle :
Price :
Price :
Description :
Description :
Music company :
ISBN :
Number of CDs :
Number of pages :
Total duration :
Illustrations :
Gender :
Create a cd
Create a book APress Beginning Java EE 6
APress Beginning Java EE 6
Figure 11.9 Deux formulaires : l’un pour créer un CD, l’autre pour créer un livre.
L’interface et l’implémentation se trouvent dans la même page. Ici, notre implémentation utilise les éléments
et
car nous supposons que le composant sera placé dans un tableau
de deux colonnes. Listing 11.13 : La page newItem.xhtml contient un composant composite
Title :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 379
Price :
Description :
Ce composant déclare une interface avec deux attributs : item représente l’entité Item (et les sous-classes Book et CD) et style est une feuille de style CSS utilisée pour la présentation. Ces attributs sont ensuite utilisés par l’implémentation du composant à l’aide de la syntaxe suivante : #{compositeComponent.attrs.style}
Ce code indique un appel de la méthode getAttributes() du composant composite courant ; le code recherche ensuite dans l’objet Map qu’elle renvoie la valeur correspondant à la clé style. Avant d’expliquer comment utiliser ce composant, il faut se rappeler les principes de la gestion des ressources et la notion de configuration par exception : le composant doit être stocké dans un fichier situé dans une bibliothèque de ressources. Ici, par exemple, ce fichier s’appelle newItem.xhtml et a été placé dans le répertoire /resources/apress. Si l’on se fie au comportement par défaut, l’utilisation du composant nécessite simplement de déclarer une bibliothèque appelée apress et de lui associer un espace de noms XML :
Puis on appelle le composant newItem (le nom de la page) en lui passant les paramètres qu’il attend : item désigne l’entité Item et style est le paramètre facultatif désignant une feuille de style CSS :
Le Listing 11.14 montre la page newBook.xhtml représentant le formulaire pour entrer les informations sur un livre. Elle inclut le composant newItem et ajoute des
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
380
Java EE 6 et GlassFish 3
champs de saisie pour l’ISBN et le nombre de pages, ainsi qu’une case à cocher pour indiquer si le livre contient, ou non, des illustrations. Listing 11.14 : La page newBook.xhtml utilise le composant newItem Creates a new book Create a new book
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 381
APress - Beginning Java EE 6
Objets implicites
L’implémentation du composant composite que nous venons de créer utilise un objet compositeComponent – pourtant, on ne l’a déclaré nulle part : il est simplement là pour permettre d’accéder aux attributs du composant. Ces types d’objets sont appelés objets implicites (ou variables implicites) : ce sont des identificateurs spéciaux qui correspondent à des objets spécifiques souvent utilisés. Ils sont implicites parce qu’une page y a accès et peut les utiliser sans avoir besoin de les déclarer ou de les initialiser explicitement. Ces objets (énumérés dans le Tableau 11.19) sont utilisés dans des expressions EL. Tableau 11.19 : Objets implicites
Objet implicite
Description
Type renvoyé
application
Représente l’environnement de l’application web. Sert à obtenir les paramètres de configuration de cette application.
Object
applicationScope
Associe les noms d’attributs de l’application à leurs valeurs.
Map
component
Désigne le composant courant.
UIComponent
compositeComponent
Désigne le composant composite courant.
UIComponent
cookie
Désigne un Map contenant les noms des cookies (clés) et des objets Cookie.
Map
facesContext
Désigne l’instance FacesContext de cette requête.
FacesContext
header
Fait correspondre chaque nom d’en-tête HTTP à une seule valeur de type String.
Map
headerValue
Fait correspondre chaque nom d’en-tête HTTP à un tableau String[] contenant toutes les valeurs de cet en-tête.
Map
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
382
Java EE 6 et GlassFish 3
Tableau 11.19 : Objets implicites (suite)
Objet implicite
Description
Type renvoyé
initParam
Fait correspondre les noms des paramètres d’initialisation du contexte à leurs valeurs de type String.
Map
param
Fait correspondre chaque nom de paramètre à une seule valeur de type String.
Map
paramValues
Fait correspondre chaque nom de paramètre à un tableau String[] contenant toutes les valeurs de ce paramètre.
Map
request
Représente l’objet requête HTTP.
Object
requestScope
Fait correspondre les noms des attributs de la requête à leurs valeurs.
Map
resource
Indique l’objet ressource.
Object
session
Représente l’objet session http.
Object
sessionScope
Fait correspondre les noms des attributs de la session à leurs valeurs.
Map
view
Représente la vue courante.
UIViewRoot
viewScope
Fait correspondre les noms des attributs de la vue à leurs valeurs.
Map
Tous ces objets implicites sont de vrais objets avec des interfaces : vous pouvez accéder à leurs attributs avec EL (consultez la spécification). #{view. Locale}, par exemple, permet d’obtenir la locale de la vue courante (en_US, pt_PT, etc.). Si vous stockez un livre dans la portée de la session, par exemple, vous pouvez y accéder par #{sessionScope.book}. Vous pouvez même utiliser un algorithme plus élaboré pour afficher tous les en-têtes HTTP et leurs valeurs : headerValues =
Si vous exécutez cette page, vous obtiendrez le résultat suivant :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 11
Pages et composants 383
Résumé Ce chapitre a présenté les différents moyens de créer des pages web statiques à l’aide de langages comme HTML, XHTML ou CSS et dynamiques avec JavaScript ou les technologies côté serveur. Pour créer des interfaces web dynamiques avec Java EE 6, vous avez le choix entre plusieurs spécifications. JSP 2.2, EL 2.2 et JSTL 1.2 ont été créées en pensant aux servlets, leurs pages sont formées d’informations HTML et de code Java compilés dans une servlet qui renvoie une réponse à une requête donnée. Bien que l’on puisse se servir de JSP comme PDL (Presentation Description Language) pour JSF, il est préférable d’utiliser Facelets afin de disposer de la puissance de l’architecture des composants JSF et de son cycle de vie élaboré. JSF fournit un ensemble de widgets standard (boutons, liens, cases à cocher, etc.) et un nouveau modèle pour créer ses propres composants (composants composites). JSF 2.0 dispose également d’un nouveau mécanisme de gestion des ressources permettant de gérer de façon simple les locales et les versions des ressources externes. JSF 2.0 utilise une architecture de composants UI sophistiquée ; ses composants peuvent être convertis et validés, et interagir avec les beans gérés, qui sont présentés dans le prochain chapitre.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
12 Traitement et navigation Au chapitre précédent, nous avons vu comment créer des pages web avec différentes technologies (HTML, JSP, JSTL, etc.) en insistant sur le fait que JSF est la spécification conseillée pour écrire des applications web modernes avec Java EE. Cependant, créer des pages contenant des composants graphiques ne suffit pas : ces pages doivent interagir avec un backend (un processus en arrière-plan), il faut pouvoir naviguer entre les pages et valider et convertir les données. JSF est une spécification très riche : les beans gérés permettent d’invoquer la couche métier, de naviguer dans votre application, et, grâce à un ensemble de classes, vous pouvez convertir les valeurs des composants ou les valider pour qu’ils correspondent aux règles métiers. Grâce aux annotations, le développement de convertisseurs et de validateurs personnalisés est désormais chose facile. JSF 2.0 apporte la simplicité et la richesse aux interfaces utilisateurs dynamiques. Il reconnaît nativement Ajax en fournissant une bibliothèque JavaScript permettant d’effectuer des appels asynchrones vers le serveur et de rafraîchir une page par parties. La création d’interfaces utilisateurs, le contrôle de la navigation dans l’application et les appels synchrones ou asynchrones de la logique métier sont possibles parce que JSF utilise le modèle de conception MVC (Modèle-Vue-Contrôleur). Chaque partie est donc isolée des autres, ce qui permet de modifier l’interface utilisateur sans conséquence sur la logique métier et vice versa.
Le modèle MVC JSF et la plupart des frameworks web encouragent la séparation des problèmes en utilisant des variantes du modèle MVC. Ce dernier est un modèle d’architecture permettant d’isoler la logique métier de l’interface utilisateur car la première ne se
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
386
Java EE 6 et GlassFish 3
mélange pas bien avec la seconde : leur mélange produit des applications plus difficiles à maintenir et qui supportent moins bien la montée en charge. Dans la section "JavaServer Pages" du chapitre précédent, nous avons vu une page JSP qui contenait à la fois du code Java et des instructions SQL : bien que ce soit techniquement correct, imaginez la difficulté de maintenir une telle page... Elle mélange deux types de développement différents (celui de concepteur graphique et celui de programmeur métier) et pourrait finir par utiliser bien plus d’API encore (accès aux bases de données, appels d’EJB, etc.), par gérer les exceptions ou par effectuer des traitements métiers complexes. Avec MVC, l’application utilise un couplage faible, ce qui facilite la modification de son aspect visuel ou des règles métiers sous-jacentes sans pour autant affecter l’autre composante. Comme le montre la Figure 12.1, la partie "modèle" de MVC représente les données de l’application ; la "vue" correspond à l’interface utilisateur et le "contrôleur" gère la communication entre les deux. Figure 12.1
Client
Server
Requête HTTP
Le modèle de conception MVC. Navigateur
Contrôleur (FacesServlet)
Réponse HTTP
crée et gère
manipule et redirige
Modèle (backing bean) accède
Vue (pages XHTML)
Le modèle est représenté par le contenu, qui est souvent stocké dans une base de données et affiché dans la vue ; il ne se soucie pas de l’aspect que verra l’utilisateur. Avec JSF, il peut être formé de backing beans, d’appels EJB, d’entités JPA, etc. La vue JSF est la véritable page XHTML (XHTML est réservé aux interfaces web, mais il pourrait s’agir d’un autre type de vue, comme WML pour les dispositifs mobiles). Comme au chapitre précédent, une vue fournit une représentation graphique d’un modèle et un modèle peut avoir plusieurs vues pour afficher un livre sous forme de formulaire ou de liste, par exemple. Lorsqu’un utilisateur manipule une vue, celle-ci informe un contrôleur des modifications souhaitées. Ce contrôleur se charge alors de rassembler, convertir et valider les données, appelle la logique métier puis produit le contenu en XHTML. Avec JSF, le contrôleur est un objet FacesServlet.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 387
FacesServlet FacesServlet est une implémentation de javax.servlet.Servlet qui sert de contrô-
leur central par lequel passent toutes les requêtes. Comme le montre la Figure 12.2, la survenue d’un élément (lorsque l’utilisateur clique sur un bouton, par exemple) provoque l’envoi d’une notification au serveur via HTTP ; celle-ci est interceptée par javax.faces.webapp.FacesServlet, qui examine la requête et exécute différentes actions sur le modèle à l’aide de beans gérés. Figure 12.2
Cycle de vie
Interactions de FacesServlet.
3. Traitement en 6 étapes FacesContext
2. Passe le contrôle au cycle de vie
1. Crée un FaceContext Bouton
Événement
FacesServlet
En coulisse, la FacesServlet prend les requêtes entrantes et donne le contrôle à l’objet javax.faces.lifecycle.Lifecycle. À l’aide d’une méthode fabrique, elle crée un objet javax.faces.context.FacesContext qui contient et traite les informations d’état de chaque requête. L’objet Lifecycle utilise ce FacesContext en six étapes (décrites au chapitre précédent) avant de produire la réponse. Les requêtes qui doivent être traitées par la FacesServlet sont redirigées à l’aide d’une association de servlet dans le descripteur de déploiement. Les pages web, les beans gérés, les convertisseurs, etc. doivent être assemblés avec le fichier web.xml du Listing 12.1. Listing 12.1 : Fichier web.xml définissant la FacesServlet Faces Servlet javax.faces.webapp.FacesServlet 1 Faces Servlet
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
388
Java EE 6 et GlassFish 3
*.faces javax.faces.PROJECT_STAGE Development
Ce fichier définit la javax.faces.webapp.FacesServlet en lui donnant un nom (Faces Servlet, ici) et une association. Dans cet exemple, toutes les requêtes portant l’extension .faces sont associées pour être gérées par la servlet – toute requête de la forme http://localhost:8080/ chapter10-1.0/newBook.faces sera donc traitée par JSF. Vous pouvez également configurer quelques paramètres spécifiques à JSF dans l’élément (voir Tableau 12.1). Tableau 12.1 : Paramètres de configuration spécifiques à JSF
Paramètre
Description
javax.faces.CONFIG_FILES
Définit une liste de chemins de ressources liées au contexte dans laquelle JSF recherchera les ressources.
javax.faces.DEFAULT_SUFFIX
Permet de définir une liste de suffixes possibles pour les pages ayant du contenu JSF (.xhtml, par exemple).
javax.faces.LIFECYCLE_ID
Identifie l’instance LifeCycle utilisée pour traiter les requêtes JSF.
javax.faces.STATE_SAVING_ METHOD
Définit l’emplacement de sauvegarde de l’état. Les valeurs possibles sont server (valeur par défaut qui indique que l’état sera généralement sauvegardé dans un objet HttpSession) et client (l’état sera sauvegardé dans un champ caché lors du prochain envoi de formulaire).
javax.faces.PROJECT_STAGE
Décrit l’étape dans laquelle se trouve cette application JSF dans le cycle de vie (Development, UnitTest, SystemTest ou Production). Cette information peut être utilisée par une implémentation de JSF pour améliorer les performances lors de la phase de production en utilisant un cache pour les ressources, par exemple.
javax.faces.DISABLE_FACELET_ JSF_VIEWHANDLER
Désactive Facelets comme langage de déclaration de page (PDL).
javax.faces.LIBRARIES
Liste des chemins qui seront considérés comme une bibliothèque de marqueurs Facelets.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 389
FacesContext
JSF définit la classe abstraite javax.faces.context.FacesContext pour représenter les informations contextuelles associées au traitement d’une requête et à la production de la réponse correspondante. Cette classe permet d’interagir avec l’interface utilisateur et le reste de l’environnement JSF. Pour y accéder, vous devez soit utiliser l’objet implicite facesContext dans vos pages (les objets implicites ont été présentés au chapitre précédent), soit obtenir une référence dans vos beans gérés à l’aide de la méthode statique getCurrentInstance() : celle-ci renverra l’instance de FacesContext pour le thread courant et vous pourrez alors invoquer les méthodes du Tableau 12.2. Tableau 12.2 : Quelques méthodes de FacesContext
Méthode
Description
addMessage
Ajoute un message d’erreur.
getApplication
Renvoie l’instance Application associée à cette application web.
getAttributes
Renvoie un objet Map représentant les attributs associés à l’instance FacesContext.
getCurrentInstance
Renvoie l’instance FacesContext pour la requête traitée par le thread courant.
getMaximumSeverity
Renvoie le niveau d’importance maximal pour tout FacesMessage mis en file d’attente.
getMessages
Renvoie une collection de FacesMessage.
getViewRoot
Renvoie le composant racine associé à la requête.
release
Libère les ressources associées à cette instance de FacesContext.
renderResponse
Signale à l’implémentation JSF que le contrôle devra être transmis à la phase Render response dès la fin de l’étape de traitement courante de la requête, en ignorant les étapes qui n’ont pas encore été exécutées.
responseComplete
Signale à l’implémentation JSF que la réponse HTTP de cette requête a déjà été produite et que le cycle de vie du traitement de la requête doit se terminer dès la fin de l’étape en cours.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
390
Java EE 6 et GlassFish 3
Configuration de Faces
La FacesServlet est interne aux implémentations de JSF ; bien que vous n’ayez pas accès à son code, vous pouvez la configurer avec des métadonnées. Vous savez désormais qu’il existe deux moyens d’indiquer des métadonnées avec Java EE 6 : les annotations et les descripteurs de déploiement XML (/WEB-INF/faces-config. xml). Avant JSF 2.0, le seul choix possible était XML mais, désormais, les beans gérés, les convertisseurs, les moteurs de rendu et les validateurs pouvant utiliser les annotations, les fichiers de configuration XML sont devenus facultatifs. Nous conseillons l’emploi des annotations mais, pour montrer à quoi ressemble un fichier faces-config.xml, le Listing 12.2 définit une locale et un ensemble de messages pour l’internationalisation et certaines règles de navigation. Nous verrons ensuite comment naviguer avec et sans faces-config.xml. Listing 12.2 : Extrait d’un fichier faces-config.xml fr messages msg * doCreateBook-success /listBooks.htm
Beans gérés Comme on l’a indiqué plus haut, le modèle MVC encourage la séparation entre le modèle, la vue et le contrôleur. Avec Java EE, les pages JSF forment la vue et la
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 391
est le contrôleur. Les beans gérés, quant à eux, sont une passerelle vers le modèle. Les beans gérés sont des classes Java annotées. Ils constituent le cœur des applications web car ils exécutent la logique métier (ou la délèguent aux EJB, par exemple), gèrent la navigation entre les pages et stockent les données. Une application JSF typique contient un ou plusieurs beans gérés qui peuvent être partagés par plusieurs pages. Les données sont stockées dans les attributs du bean géré, qui, en ce cas, est également appelé "backing bean". Un backing bean définit les données auxquelles est lié un composant de l’interface utilisateur (la cible d’un formulaire, par exemple). Pour établir cette liaison, on utilise EL, le langage d’expressions.
FacesServlet
Écriture d’un bean géré
Écrire un bean géré est aussi simple qu’écrire un EJB ou une entité JPA puisqu’il s’agit simplement de créer une classe Java annotée par @ManagedBean (voir Listing 12.3) – il n’y a nul besoin de créer des entrées dans faces-config.xml, de créer des classes utilitaires ou d’hériter d’une classe quelconque : JSF 2.0 utilisant également le mécanisme de configuration par exception, une seule annotation suffit pour utiliser tous les comportements par défaut et pour déployer une application web utilisant un bean géré. Listing 12.3 : Bean géré simple @ManagedBean public class BookController { private Book book = new Book(); public String doCreateBook() { createBook(book); return "listBooks.xhtml"; } }
// Constructeurs, getters, setters
Le Listing 12.3 met en évidence le modèle de programmation d’un bean géré : il stocke l’état (l’attribut book), définit les méthodes d’action (doCreateBook()) utilisées par une page et gère la navigation (return "listBooks.xhtml"). Modèle d’un bean géré
Les beans gérés sont des classes Java prises en charge par la FacesServlet. Les composants de l’interface utilisateur sont liés aux propriétés du bean (backing bean) et
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
392
Java EE 6 et GlassFish 3
peuvent invoquer des méthodes d’action. Un bean géré doit respecter les contraintes suivantes : ■■
La classe doit être annotée par @javax.faces.model.ManagedBean ou son équivalent dans le descripteur de déploiement XML faces-config.xml.
■■
La classe doit avoir une portée (qui vaut par défaut @RequestScoped).
■■
La classe doit être publique et non finale ni abstraite.
■■
La classe doit fournir un constructeur public sans paramètre qui sera utilisé par le conteneur pour créer les instances.
■■
La classe ne doit pas définir de méthode finalize().
■■
Pour être liés à un composant, les attributs doivent avoir des getters et des setters publics.
Bien qu’un bean géré puisse être un simple POJO annoté, sa configuration peut être personnalisée grâce aux éléments de @ManagedBean et @ManagedProperty (ou leurs équivalents XML). @ManagedBean
La présence de l’annotation @javax.faces.model.ManagedBean sur une classe l’enregistre automatiquement comme un bean géré. La Figure 12.4 présente l’API de cette annotation, dont tous les éléments sont facultatifs. Listing 12.4 : API de l’annotation ManagedBean @Target(TYPE) @Retention(RUNTIME) public @interface ManagedBean { String name() default ""; boolean eager() default false; }
L’élément name indique le nom du bean géré (par défaut, ce nom est celui de la classe commençant par une minuscule). Si l’élément eager vaut true, le bean géré est instancié dès le démarrage de l’application. Les composants de l’interface utilisateur étant liés aux propriétés d’un bean géré, changer son nom par défaut a des répercussions sur la façon d’appeler une propriété ou une méthode. Le code du Listing 12.5, par exemple, renomme le bean géré Book– Controller en myManagedBean.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 393
Listing 12.5 : Changement du nom par défaut d’un bean géré @ManagedBean(name = "myManagedBean") public class BookController { private Book book = new Book(); public String doCreateBook() { createBook(book); return "listBooks.xhtml"; } // Constructeurs, getters, setters }
Pour invoquer les attributs ou les méthodes de ce bean, vous devez donc utiliser son nouveau nom : Create a new book
Portées
Les objets créés dans le cadre d’un bean géré ont une certaine durée de vie et peuvent ou non être accessibles aux composants de l’interface utilisateur ou aux objets de l’application. Cette durée de vie et cette accessibilité sont regroupées dans la notion de portée. Cinq annotations permettent de définir la portée d’un bean géré : ■■ @ApplicationScoped.
Il s’agit de l’annotation la moins restrictive, avec la plus longue durée de vie. Les objets créés sont disponibles dans tous les cycles requête/ réponse de tous les clients utilisant l’application tant que celle-ci est active. Ces objets peuvent être appelés de façon concurrente et doivent donc être threadsafe (c’est-à-dire utiliser le mot-clé synchronized). Les objets ayant cette portée peuvent utiliser d’autres objets sans portée ou avec une portée d’application.
■■ @SessionScoped. Ces objets sont disponibles pour tous les cycles requête/réponse
de la session du client. Leur état persiste entre les requêtes et dure jusqu’à la fin de la session. Ils peuvent utiliser d’autres objets sans portée, avec une portée de session ou d’application. ■■ @ViewScoped. Ces objets sont disponibles dans une vue donnée jusqu’à sa modi-
fication. Leur état persiste jusqu’à ce que l’utilisateur navigue vers une autre vue,
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
394
Java EE 6 et GlassFish 3
auquel cas il est supprimé. Ils peuvent utiliser d’autres objets sans portée, avec une portée de vue, de session ou d’application. ■■ @RequestScoped.
Il s’agit de la portée par défaut. Ces objets sont disponibles du début d’une requête jusqu’au moment où la réponse est envoyée au client. Un client pouvant exécuter plusieurs requêtes tout en restant sur la même vue, la durée de @ViewScoped est supérieure à celle de @RequestScoped. Les objets ayant cette portée peuvent utiliser d’autres objets sans portée, avec une portée de requête, de vue, de session ou d’application.
■■ @NoneScoped.
Les beans gérés ayant cette portée ne sont visibles dans aucune page JSF ; ils définissent des objets utilisés par d’autres beans gérés de l’application. Ils peuvent utiliser d’autres objets avec la même portée.
La portée des beans gérés doit être judicieusement choisie : vous ne devez leur donner que la portée dont ils ont besoin. Des beans ayant une portée trop grande (@ ApplicationScoped, par exemple) augmentent l’utilisation mémoire et l’utilisation du disque pour leur persistance éventuelle. Il n’y a aucune raison de donner une portée d’application à un objet qui n’est utilisé que dans un seul composant. Inversement, un objet ayant une portée trop restreinte ne sera pas disponible dans certaines parties de l’application. Le code du Listing 12.6 définit un bean géré avec une portée d’application. Il sera instancié dès le lancement de l’application (eager = true) et initialise l’attribut defaultBook dès qu’il est construit (@PostConstruct). Il pourrait donc être le bean idéal pour initialiser des parties de l’application web ou pour être référencé par des propriétés d’autres beans gérés. Listing 12.6 : Bean géré avec une portée d’application et une instanciation eager @ManagedBean(eager = true) @ApplicationScoped public class InitController { private Book defaultBook; @PostConstruct private void init() { defaultBook=new Book("default title", 0, "default description", "0000-000", 100, true); } // Constructeurs, getters, setters }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 395
@ManagedProperty
Dans un bean géré, vous pouvez demander au système d’injecter une valeur dans une propriété (un attribut avec des getters et/ou des setters) en utilisant le fichier faces-config.xml ou l’annotation @javax.faces.model.ManagedProperty, dont l’attribut value peut recevoir une chaîne ou une expression EL. Le Listing 12.7 montre quelques exemples d’initialisations. Listing 12.7 : Initialisation des propriétés d’un bean géré @ManagedBean public class BookController { @ManagedProperty(value = "#{initController.defaultBook}") private Book book; @ManagedProperty(value = "this is a title") private String aTitle; @ManagedProperty(value = "999") private Integer aPrice; // Constructeurs, getters, setters & méthodes }
Dans le Listing 12.7, les propriétés aTitle et aPrice sont initialisées avec une valeur de type String. L’attribut aTitle, de type String, sera initialisé avec "this is a title" et l’attribut aPrice, qui est un Integer, sera initialisé avec le nombre 999 (bien que "999" soit une chaîne, celle-ci sera convertie en Integer). Les propriétés étant évaluées lors de l’exécution (généralement lorsqu’une vue est affichée), celles qui font référence à d’autres beans gérés peuvent aussi être initialisées. Ici, par exemple, book est initialisé par une expression utilisant la propriété defaultBook du bean géré initController (#{initController.defaultBook}) que nous avons présenté plus haut. Le Listing 12.6 montre que defaultBook est un attribut de type Book initialisé par le bean InitController : lorsque BookController est initialisé, l’implémentation JSF injectera donc cet attribut. Il est généralement conseillé d’initialiser les littéraux dans le fichier faces-config.xml et d’utiliser les annotations pour les références croisées entre les beans gérés (en utilisant EL). Annotation du cycle de vie et des méthodes de rappel
Le chapitre précédent a expliqué le cycle de vie d’une page (qui compte six phases, de la réception de la requête à la production de la réponse), mais le cycle de vie des
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
396
Java EE 6 et GlassFish 3
beans gérés (voir Figure 12.3) est totalement différent : en fait, il est identique à celui des beans de session sans état. Figure 12.3 Cycle de vie d’un bean géré.
N'existe pas @PostConstruct
@PreDestroy Prêt
Appel de méthode
Les beans gérés qui s’exécutent dans un conteneur de servlet peuvent utiliser les annotations @PostConstruct et @PreDestroy. Après avoir créé une instance de bean géré, le conteneur appelle la méthode de rappel @PostConstruct s’il y en a une. Puis le bean est lié à une portée et répond à toutes les requêtes de tous les utilisateurs. Avant de supprimer le bean, le conteneur appelle la méthode @PreDestroy. Ces méthodes permettent donc d’initialiser les attributs ou de créer et libérer les ressources externes. Navigation
Les applications web sont formées de plusieurs pages entre lesquelles vous devez naviguer. Selon les cas, il peut exister différents niveaux de navigation avec des flux de pages plus ou moins élaborés. JSF dispose de plusieurs options de navigation et vous permet de contrôler le flux page par page ou pour toute l’application. Les composants et permettent de passer simplement d’une page à une autre en cliquant sur un bouton ou sur un lien sans effectuer aucun traitement. Il suffit d’initialiser leur attribut action avec le nom de la page vers laquelle vous voulez vous rendre :
Cependant, la plupart du temps, ceci ne suffira pas car vous aurez besoin d’accéder à une couche métier ou à une base de données pour récupérer ou traiter des données. En ce cas, vous aurez besoin d’un bean géré. Dans la section "Récapitulatif" du Chapitre 10, une première page (newBook.xhtml) affichait un formulaire permettant de créer un livre. Lorsque l’on cliquait sur le bouton Create, le livre était créé puis le bean géré passait à la page listBooks.xhtml, qui affichait tous les livres. Cette page contenait un lien Create a new book permettant de revenir à la page précédente (voir Figure 12.4).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 397
newBook.xhtml
listBooks.xhtml
Create a new book ISBN
List of the books ISBN
Title
Tiltle
1234 234 H2G2
Price
564 694
Price 12 0
Robots 18 5
256 6 56 Dune
Description
Number Of Pages
Illustrations
Scifi IT book
241
false
Asimov Best seller
317
true
529
false
23 25 The trilogy
Create a new book
Description
APress Beginning Java EE 6
Number of pages Illustrations
Create APress Beginning Java EE 6
Figure 12.4 Navigation entre newBook.xhtml et listBooks.xhtml.
Le flux des pages est simple, mais ces deux pages ont pourtant besoin d’un bean géré (BookController) pour traiter la logique métier et la navigation. Elles utilisent les composants bouton et lien pour naviguer et interagir avec ce bean. La page newBook.xhtml utilise un bouton pour appeler la méthode doCreateBook() du bean géré :
La page listBooks.xhtml utilise un lien pour appeler la méthode doNewBookForm() : Create a new book
Les composants bouton et lien n’appellent pas directement la page vers laquelle ils doivent se rendre : ils appellent des méthodes du bean géré qui prennent en charge cette navigation et laissent le code décider de la page qui sera chargée ensuite. La navigation utilise un ensemble de règles qui définissent tous les chemins de navigation possibles de l’application. Dans le Listing 12.8, le code du bean géré utilise la forme la plus simple de ces règles de navigation : chaque méthode définit la page vers laquelle elle doit aller. Listing 12.8 : Bean géré définissant explicitement la navigation @ManagedBean public class BookController { @EJB private BookEJB bookEJB;
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
398
Java EE 6 et GlassFish 3
private Book book = new Book(); private List bookList = new ArrayList(); public String doNewBookForm() { return "newBook.xhtml"; } public String doCreateBook() { book = bookEJB.createBook(book); bookList = bookEJB.findBooks(); return "listBooks.xhtml"; } // Constructeurs, getters, setters }
Quand le invoque la méthode doCreateBook(), celle-ci crée un livre (à l’aide d’un bean de session sans état) et renvoie le nom de la page vers laquelle naviguer ensuite : listBooks.xhtml. La FacesServlet redirigera alors le flux de page vers la page désirée. La chaîne renvoyée peut prendre plusieurs formes. Ici, nous avons utilisé la plus simple : le nom de la page. L’extension de fichier par défaut étant .xhtml, nous aurions même pu simplifier le code en supprimant l’extension : public String doNewBookForm() { return "newBook"; }
Avec JSF, le flux de navigation peut être défini en externe via faces-config.xml, à l’aide d’éléments qui identifient la page de départ, une condition et la page vers laquelle naviguer lorsque la condition sera vérifiée. Celle-ci utilise un nom logique au lieu du nom physique de la page. Comme le montre le Listing 12.9, le code précédent aurait pu utiliser, par exemple, le nom success. Listing 12.9 : Extrait d’un bean géré utilisant des noms logiques @ManagedBean public class BookController { // ... public String doNewBookForm() { return "success"; } public String doCreateBook() { book = bookEJB.createBook(book); bookList = bookEJB.findBooks();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
} }
Traitement et navigation 399
return "success";
// Constructeurs, getters, setters
Les deux méthodes renvoyant le même nom logique, le fichier faces-config.xml doit donc faire correspondre ce nom à la page newBook.xhtml dans un cas et à la page listBooks.xhtml dans l’autre. Le Listing 12.10 montre la structure de facesconfig.xml : l’élément définit la page dans laquelle a lieu l’action. Dans le premier cas, on part de newBook.xhtml avant d’appeler le bean géré : si le nom logique renvoyé est success (), la FacesServlet fera suivre l’appel à la page listBooks.xhtml (). Listing 12.10 : Fichier faces-config.xml définissant la navigation newBook.xhtml success listBooks.xhtml listBooks.xhtml success newBook.xhtml
La navigation pouvant avoir lieu directement dans les beans gérés ou au moyen de faces-config.xml, quelle solution utiliser plutôt qu’une autre ? La première motivation pour renvoyer directement le nom de la page dans les beans gérés est la simplicité : le code Java est explicite et il n’y a pas besoin de passer par un fichier XML supplémentaire. Si, en revanche, le flux des pages d’une application web est complexe, il peut être judicieux de le décrire à un seul endroit afin que les modifications soient centralisées au lieu d’être dispersées dans plusieurs pages. Vous pouvez également mélanger ces deux approches et effectuer une partie de la navigation dans vos beans et une autre dans le fichier faces-config.xml.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
400
Java EE 6 et GlassFish 3
Il existe un cas où une configuration XML est très utile : c’est lorsque plusieurs pages contiennent des liens globaux (lorsque, par exemple, la connexion ou la déconnexion peuvent être appelées à partir de toutes les pages de l’application) car il serait assez lourd de devoir les définir dans chaque page. Avec XML, vous pouvez définir des règles de navigation globales (ce qui n’est pas possible avec les beans gérés) : * logout logout.xhtml
Si une action s’applique à toutes les pages d’une application, vous pouvez utiliser un élément sans ou utiliser un joker (*). Le code précédent indique que, quelle que soit la page où il se trouve, l’utilisateur sera dirigé vers la page logout.xhtml si la méthode du bean géré renvoie le nom logique logout. Les exemples précédents ont montré une navigation simple où une page n’avait qu’une seule règle de navigation et une seule page destination. Ce n’est pas un cas si fréquent : les utilisateurs peuvent généralement être redirigés vers des pages différentes, en fonction de certaines conditions. Cette navigation, là encore, peut être mise en place dans les beans gérés et dans le fichier faces-config.xml. Le code suivant utilise une instruction switch pour rediriger l’utilisateur vers trois pages possibles. Si l’on renvoie la valeur null, l’utilisateur reviendra sur la page sur laquelle il se trouve déjà. public String doNewBookForm() { switch (value) { case 1: return "page1.xhtml"; break; case 2: return "page2.xhtml"; break; case 3: return "page3.xhtml"; break; default: return null; break; } }
Gestion des messages
Les beans gérés traitent la logique métier, appellent les EJB, utilisent les bases de données, etc. Parfois, cependant, un problème peut survenir et, en ce cas, l’utilisateur doit en être informé par un message qui peut être un message d’erreur de l’application (concernant la logique métier ou la connexion à la base ou au réseau) ou un message d’erreur de saisie (un ISBN incorrect ou un champ vide, par exemple). Les erreurs d’application peuvent produire une page particulière demandant
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 401
à l’utilisateur de réessayer dans un moment, par exemple, alors que les erreurs de saisie peuvent s’afficher dans la même page avec un texte décrivant l’erreur. On peut également utiliser des messages pour informer l’utilisateur qu’un livre a été correctement ajouté à la base de données. Au chapitre précédent, nous avons utilisé des marqueurs pour afficher des messages sur les pages ( et ). Pour produire ces messages, JSF vous permet de les placer dans une file d’attente en appelant la méthode FacesContext. addMessage() dans les beans gérés. Sa signature est la suivante : void addMessage(String clientId, FacesMessage message)
Cette méthode ajoute un FacesMessage à l’ensemble des messages à afficher. Son premier paramètre est l’identifiant d’un client qui désigne le composant d’interface auquel le message est rattaché. S’il vaut null, ceci signifie que le message n’est lié à aucun composant particulier et qu’il est global à toutes les pages. Un message est formé d’un texte résumé, d’un texte détaillé et d’un niveau d’importance (fatal, error, warning et info). Les messages peuvent également être internationalisés par des ensembles de textes localisés (message bundles). FacesMessage(Severity severity, String summary, String detail)
Le code suivant est un extrait d’un bean géré qui crée un livre. Selon que cette création réussit ou qu’une exception survient, un message d’information ou d’erreur est ajouté à la file d’attente des messages à afficher. Notez que ces deux messages sont globaux car l’identifiant du client vaut null : FacesContext ctx = FacesContext.getCurrentInstance(); try { book = bookEJB.createBook(book); ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Book has been created", "The book" + book.getTitle() + " has been created with id=" + book.getId()) ); } catch (Exception e) { ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Book hasn’t been created", e.getMessage()) ); } }
Ces messages étant globaux, on peut les afficher dans une page à l’aide d’un simple marqueur . On peut également préférer afficher un message à un
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
402
Java EE 6 et GlassFish 3
endroit précis pour un composant spécifique (ce qui est généralement le cas avec les erreurs de validation ou de conversion). La Figure 12.5, par exemple, montre une page avec un message spécifiquement destiné au champ de saisie du prix. Figure 12.5 Page affichant un message pour un composant d’interface précis.
Create a new book ISBN : Tiltle : Price :
Please, fill the price !
Description :
Number of pages : Illustrations :
Create APress Beginning Java EE 6
Dans cette page, le champ de saisie du prix a un identifiant (id="priceId") auquel fait référence le marqueur (for="priceId"). En conséquence, ce message précis ne s’affichera que pour ce composant :
Si le champ du prix n’a pas été rempli, un message s’affiche à côté du champ de saisie. Le code qui suit vérifie le prix saisi et crée un message d’avertissement associé à l’identifiant du composant si la valeur n’est pas correcte : if (book.getPrice() == null || "".equals(book.getPrice())) { ctx.addMessage("priceId", new FacesMessage(SEVERITY_WARN, "Please, fill the price !", "Enter a number value"));
JSF utilise également ce mécanisme de message pour les convertisseurs et les validateurs.
Conversion et validation Nous venons de voir comment gérer les messages pour informer l’utilisateur sur les actions à entreprendre. L’une d’elles consiste à corriger une saisie incorrecte (un ISBN non valide, par exemple). JSF fournit un mécanisme standard de conversion
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 403
et de validation permettant de traiter les saisies des utilisateurs afin d’assurer l’intégrité des données. Lorsque vous invoquez des méthodes métiers, vous pouvez donc vous fier à des données valides : la conversion et la validation permettent aux développeurs de se concentrer sur la logique métier au lieu de passer du temps à vérifier que les données saisies ne sont pas null, qu’elles appartiennent bien à un intervalle précis, etc. La conversion a lieu lorsque les données saisies par l’utilisateur doivent être transformées de String en un objet et vice versa. Elle garantit que les informations sont du bon type – en convertissant, par exemple, un String en java.util.Date, un String en Integer ou des dollars en euros. Comme pour la validation, elle garantit que les données contiennent ce qui est attendu (une date au format jj/mm/aaaa, un réel compris entre 3,14 et 3,15, etc.). Comme le montre la Figure 12.6, la conversion et la validation interviennent au cours des différentes phases du cycle de vie de la page (que nous avons présenté au chapitre précédent). Valeurs des composants Valeurs des composants validées converties en objets • validation standard • conversion par défaut • validation personnalisée • conversion personnalisée • appel de la méthode getAsObject()
Restauration de la vue
Application des valeurs de la requête
Traitement des événements
validations
Traitement des événements
Appel de l'application
Traitement des événements
Mise à jour des valeurs du modèle
Valeurs des composants reconverties pour l'affichage • appel de la méthode getAsString()
Affichage de la réponse
Traitement des événements
Erreurs de conversion ou de validation
Figure 12.6 Conversion et validation au cours du cycle de vie d’une page.
Au cours de la phase Application des valeurs de la requête de la Figure 12.6, la valeur du composant de l’interface est convertie dans l’objet cible puis validée au cours de la phase Traitement des validations. Il est logique que la conversion et la validation interviennent avant que les données du composant ne soient liées
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
404
Java EE 6 et GlassFish 3
au backing bean (ce qui a lieu au cours de la phase Mise à jour des valeurs du modèle). En cas d’erreur, des messages d’erreur seront ajoutés et le cycle de vie sera écourté afin de passer directement à l’Affichage de la réponse (les messages seront alors affichés sur l’interface utilisateur avec ). Au cours de⁄cette phase, les propriétés du backing bean sont reconverties en chaînes pour pouvoir être affichées. JSF fournit un ensemble de convertisseurs et de validateurs standard et vous permet de créer les vôtres très facilement. Convertisseurs
Lorsqu’un formulaire est affiché par un navigateur, l’utilisateur remplit les champs et appuie sur un bouton ayant pour effet de transporter les données vers le serveur dans une requête HTTP formée de chaînes. Avant de mettre à jour le modèle du bean géré, ces données textuelles doivent être converties dans les objets cibles (Float, Integer, BigDecimal, etc.). L’opération inverse aura lieu lorsque les données seront renvoyées au client dans la réponse pour être affichées par le navigateur. JSF fournit des convertisseurs pour les types classiques comme les dates et les nombres. Si une propriété du bean géré est d’un type primitif (Integer, int, Float, float, etc.), JSF convertira automatiquement la valeur du composant d’interface dans le type adéquat et inversement. Si elle est d’un autre type, vous devrez fournir votre propre convertisseur. Le Tableau 12.3 énumère tous les convertisseurs standard du paquetage javax.faces.convert. Tableau 12.3 : Convertisseurs standard
Convertisseur
Description
BigDecimalConverter
Convertit un String en java.math.BigDecimal et vice versa.
BigIntegerConverter
Convertit un String en java.math.BigInteger et vice versa.
BooleanConverter
Convertit un String en Boolean (et boolean) et vice versa.
ByteConverter
Convertit un String en Byte (et byte) et vice versa.
CharacterConverter
Convertit un String en Character (et char) et vice versa.
DateTimeConverter
Convertit un String en java.util.Date et vice versa.
DoubleConverter
Convertit un String en Double (et double) et vice versa.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 405
Tableau 12.3 : Convertisseurs standard (suite)
Convertisseur
Description
EnumConverter
Convertit un String en Enum (et enum) et vice versa.
FloatConverter
Convertit un String en Float (et float) et vice versa.
IntegerConverter
Convertit un String en Integer (et int) et vice versa.
LongConverter
Convertit un String en Long (et long) et vice versa.
NumberConverter
Convertit un String en classe abstraite java.lang.Number et vice versa.
ShortConverter
Convertit un String en Short (et short) et vice versa.
JSF convertira automatiquement les valeurs saisies en nombre lorsque la propriété du bean géré est d’un type numérique primitif et en date ou en heure lorsque la propriété est d’un type date. Si ces conversions automatiques ne conviennent pas, vous pouvez les contrôler explicitement via les marqueurs standard convertNumber et convertDateTime. Pour ce faire, vous devez imbriquer le convertisseur dans un marqueur d’entrée ou de sortie. Il sera appelé par JSF au cours du cycle de vie. Le marqueur convertNumber possède des attributs permettant de convertir la valeur d’entrée en nombre (comportement par défaut), en valeur monétaire ou en pourcentage. Vous pouvez préciser un symbole monétaire ou un nombre de chiffres après la virgule, ainsi qu’un motif déterminant le format du nombre et la façon dont il sera analysé :
Le marqueur convertDateTime convertit les dates dans différents formats (date, heure ou les deux). Il possède plusieurs attributs pour contrôler cette conversion ainsi que les zones horaires. L’attribut pattern permet d’indiquer le format de la chaîne de date à convertir :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
406
Java EE 6 et GlassFish 3
Convertisseurs personnalisés
Parfois, la conversion de nombres, de dates, d’énumérations, etc. ne suffit pas et nécessite une conversion adaptée à la situation. Il suffit pour cela d’écrire une classe qui implémente l’interface javax.faces. convert.Converter et de lui associer des métadonnées. Cette interface expose deux méthodes : Object getAsObject(FacesContext ctx, UIComponent component, String value) String getAsString(FacesContext ctx, UIComponent component, Object value)
La méthode getAsObject() convertit la valeur chaîne d’un composant d’interface utilisateur dans le type correspondant et renvoie la nouvelle instance ; elle lance une exception ConverterException si la conversion échoue. Inversement, getAs String() convertit l’objet en chaîne afin qu’il puisse être affiché à l’aide d’un langage à marqueurs (comme XHTML). Pour utiliser ce convertisseur dans l’application web, il faut l’enregistrer : une méthode consiste à le déclarer dans le fichier faces-config.xml, l’autre, à utiliser l’annotation @FacesConverter. Le Listing 12.11 montre comment écrire un convertisseur personnalisé pour convertir un prix en dollars en valeur en euros. On commence par associer ce convertisseur au nom euroConverter (value = "euroConverter") à l’aide de l’annotation @ FacesConverter, puis on implémente l’interface Converter. Cet exemple ne redéfinit que la méthode getAsString() pour qu’elle renvoie une représentation textuelle d’un prix en euros. Listing 12.11 : Convertisseur en euros @FacesConverter(value = "euroConverter") public class EuroConverter implements Converter { @Override public Object getAsObject(FacesContext ctx, UIComponent component, String value) { return value; } @Override public String getAsString(FacesContext ctx, UIComponent component, Object value) {
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Pour utiliser ce convertisseur, on utilise soit l’attribut converter d’un marqueur, soit le marqueur : dans les deux cas, il faut fournir le nom du convertisseur défini par l’annotation @FacesConverter (euroConverter, ici). Le code suivant affiche deux textes, l’un représentant le prix en dollars, l’autre ce prix converti en euros :
Vous pouvez également utiliser l’attribut converter du marqueur outputText :
Validateurs
Les applications web doivent garantir que les données saisies par les utilisateurs sont appropriées. Cette vérification peut avoir lieu côté client avec JavaScript ou côté serveur avec des validateurs. JSF simplifie la validation des données en fournissant des validateurs standard et en permettant d’en créer de nouveaux, adaptés à vos besoins. Les validateurs agissent comme des contrôles de premier niveau en validant les valeurs des composants de l’interface utilisateur avant qu’elles ne soient traitées par le bean géré. Généralement, les composants d’interface mettent en œuvre une validation simple, comme vérifier qu’une valeur est obligatoire. Le marqueur suivant, par exemple, exige qu’une valeur soit entrée dans le champ de saisie :
Si aucune valeur n’est saisie, JSF renvoie la page avec un message indiquant qu’il faut en fournir une (la page doit avoir un marqueur ) en utilisant le même mécanisme de messages que nous avons déjà décrit. Mais JSF fournit également un ensemble de validateurs plus élaborés (voir Tableau 12.4) définis dans le paquetage javax.faces.validator.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
408
Java EE 6 et GlassFish 3
Tableau 12.4 : Validateurs standard
Convertisseur
Description
DoubleRangeValidator
Compare la valeur du composant aux valeurs minimales et maximales indiquées (de type double).
LengthValidator
Teste le nombre de caractères de la valeur textuelle du composant.
LongRangeValidator
Compare la valeur du composant aux valeurs minimales et maximales indiquées (de type long).
RegexValidator
Compare la valeur du composant à une expression régulière.
Ces validateurs permettent de traiter les cas génériques comme la longueur d’une chaîne ou un intervalle de valeurs ; ils peuvent être associés facilement à un composant, de la même façon que les convertisseurs (un même composant peut contenir les deux). Le code suivant, par exemple, garantit que le titre d’un livre fait entre 2 et 20 caractères et que son prix varie de 1 à 500 dollars :
Validateurs personnalisés
Les validateurs standard de JSF peuvent ne pas convenir à vos besoins : vous avez peut-être des données qui respectent certains formats métier, comme un code postal ou une adresse de courrier électronique. En ce cas, vous devrez créer votre propre validateur. Comme les convertisseurs, un validateur est une classe qui doit implémenter une interface et redéfinir une méthode. Dans le cas des validateurs, cette interface est javax.faces.validator.Validator, qui n’expose que la méthode validate() : void validate(FacesContext context, UIComponent component, Object value)
Le paramètre value est celui qui doit être vérifié en fonction d’une certaine logique métier. S’il passe le test de validation, vous pouvez simplement sortir de cette méthode et le cycle de la page se poursuivra. Dans le cas contraire, vous pouvez lancer une exception ValidatorException et inclure un FacesMessage avec des messages
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 409
r ésumés et détaillés pour décrire l’erreur de validation. Ce validateur doit être enregistré dans le fichier faces-config.xml ou à l’aide de l’annotation @FacesValidator. Le Listing 12.12, par exemple, contient le code d’un validateur qui garantit que l’ISBN saisi par l’utilisateur est au bon format. Listing 12.12 : Validateur d’ISBN @FacesValidator(value = "isbnValidator") public class IsbnValidator implements Validator { private Pattern pattern; private Matcher matcher; @Override public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { String componentValue = value.toString(); pattern = Pattern.compile("(?=[-0-9xX]{13}$)"); matcher = pattern.matcher(componentValue); if (!matcher.find()) { String message = MessageFormat.format("{0} is not a valid isbn format", componentValue); FacesMessage facesMessage = new FacesMessage(SEVERITY_ERROR, message, message); throw new ValidatorException(facesMessage); } } }
Le code du Listing 12.12 commence par associer le validateur au nom isbnValidator afin que l’on puisse l’utiliser dans une page. Puis il implémente l’interface Validator en ajoutant le code de validation à la méthode validate(), qui utilise une expression régulière pour vérifier que l’ISBN est au bon format – dans le cas contraire, il ajoute un message au contexte et lance une exception. Dans ce cas, JSF terminera automatiquement le cycle de vie de la page, la rappellera et affichera le message d’erreur. Vous pouvez utiliser ce validateur dans vos pages en passant par l’attribut validator ou par un marqueur imbriqué : // ou
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
410
Java EE 6 et GlassFish 3
Ajax Le protocole HTTP repose sur un mécanisme requête/réponse : un client a besoin d’une information, il envoie une requête et reçoit une réponse du serveur – généralement une page web complète. La communication va toujours dans ce sens : c’est le client qui est à l’initiative de la requête. Cependant, les applications web doivent produire des interfaces riches et réactives et répondre aux événements du serveur, mettre à jour des parties d’une page, agréger des widgets, etc. Dans un cycle requête/ réponse classique, le serveur devrait envoyer toute la page web, même s’il ne faut en modifier qu’une petite partie : si la taille de cette page n’est pas négligeable, ceci consomme de la bande passante et le temps de réponse sera médiocre car le navigateur devra recharger toute la page. Pour améliorer la réactivité du navigateur et fournir plus de fluidité à l’utilisateur, il ne faut modifier que de petites parties de la page, et c’est là qu’Ajax entre en jeu. Ajax (acronyme d’Asynchronous JavaScript and XML) est un ensemble de techniques de développement web permettant de créer des applications web interactives. Grâce à lui, les applications récupèrent de façon asynchrone des portions de données à partir du serveur sans interférer avec l’affichage et le comportement de la page en cours de consultation. Lorsque les données sont reçues par le client, seules les parties qui ont besoin d’être modifiées le seront : pour cela, on utilise le DOM de la page et du code JavaScript. Il existe également un mécanisme appelé Reverse Ajax (ou programmation Comet) pour pousser les données du serveur vers le navigateur. Ces mécanismes sont utilisés dans la plupart de nos applications web quotidiennes et sont désormais intégrés à JSF 2.0. Concepts généraux
Le terme Ajax a été inventé en 2005 pour désigner un ensemble d’alternatives permettant de charger des données de façon asynchrone dans les pages web. En 1999, Microsoft avait créé l’objet XMLHttpRequest comme un contrôle ActiveX dans Internet Explorer 5. En 2006, le W3C produisit le premier draft de la spécification de l’objet XMLHttpRequest, qui est désormais reconnu par la plupart des navigateurs. Au même moment, plusieurs sociétés réfléchirent au moyen de garantir qu’Ajax devienne un standard reposant sur des technologies ouvertes. Le résultat de ce travail fut la création de l’OpenAjax Alliance, composée d’éditeurs de logiciels, de projets open-source et de sociétés utilisant les technologies Ajax. Comme le montre la Figure 12.7, dans les applications web traditionnelles le navigateur doit demander des documents HTML complets au serveur. L’utilisateur clique
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 411
sur un bouton pour envoyer ou recevoir l’information, attend la réponse du serveur puis reçoit l’intégralité de la page qui se charge dans le navigateur. Ajax, par contre, utilise des transferts de données asynchrones entre le navigateur et le serveur, ce qui permet aux pages de demander des petits morceaux d’information (données au format JSON ou XML). L’utilisateur reste sur la même page pendant que du code JavaScript demande ou envoie des données au serveur de façon asynchrone, et seules des parties de cette page seront rafraîchies, ce qui a pour effet de produire des applications plus réactives et des interfaces plus fluides. Figure 12.7
Appel HTTP
Appel Ajax
Appels HTTP classiques vs. appels HTTP Ajax.
Navigateur
Navigateur
Page web
Page web
Appel JavaScript
XML
Bibliothèque Ajax Requête HTTP
Page XHTML Requête XMLHttp
Serveur
XML
Serveur
En principe, Ajax repose sur les technologies suivantes : ■■
XHTML et CSS pour la présentation ;
■■
DOM pour l’affichage dynamique et l’interaction avec les données ;
■■
XML et XSLT pour les échanges, la manipulation et l’affichage des données XML ;
■■
l’objet XMLHttpRequest pour la communication asynchrone ;
■■
JavaScript pour relier toutes ces technologies.
joue un rôle important dans Ajax car c’est une API DOM utilisée par JavaScript pour transférer du XML du navigateur vers le serveur. Les données renvoyées en réponse doivent être récupérées sur le client pour modifier dynamiquement les parties de la page avec JavaScript. Ces données peuvent être dans différents formats, comme XHTML, JSON, voire du texte brut. XMLHttpRequest
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
412
Java EE 6 et GlassFish 3
Ajax étant disponible nativement dans JSF 2.0, vous n’avez plus besoin d’écrire de code JavaScript pour gérer l’objet XMLHttpRequest : il suffit d’utiliser la bibliothèque JavaScript qui a été spécifiée et qui est disponible dans les implémentations de JSF 2.0. Ajax et JSF
Les versions précédentes de JSF n’offrant pas de solution native pour Ajax, des bibliothèques tierces sont venues combler ce manque, ce qui augmentait la complexité du code au détriment des performances. À partir de JSF 2.0, tout est beaucoup plus simple puisque Ajax a été ajouté à la spécification et est désormais intégré dans toutes les implémentations. La bibliothèque JavaScript jsf.js permet de réaliser les interactions Ajax, ce qui signifie qu’il n’est plus nécessaire d’écrire ses propres scripts pour manipuler directement les objets XMLHttpRequest : vous pouvez vous servir d’un ensemble de fonctions standard pour envoyer des requêtes asynchrones et recevoir les données. Pour utiliser cette bibliothèque dans vos pages, il suffit d’ajouter la ressource jsf.js avec la ligne suivante :
Le marqueur produit un élément faisant référence au fichier jsf.js de la bibliothèque javax.faces (l’espace de noms de premier niveau javax est enregistré par l’OpenAjax Alliance). Cette API JavaScript sert à lancer les interactions côté client. La fonction utilisée directement dans les pages s’appelle request : c’est elle qui est responsable de l’envoi d’une requête Ajax au serveur. Sa signature est la suivante : jsf.ajax.request(ELEMENT, |EVENT|, { |OPTIONS| });
est le composant JSF ou l’élément XHTML qui déclenchera l’événement – pour la soumission d’un formulaire, il s’agira généralement d’un bouton. EVENT est l’événement JavaScript supporté par cet élément, comme onmousedown, onclick, onblur, etc. Le paramètre OPTIONS est un tableau pouvant contenir les paires nom/ valeur suivantes : ELEMENT
■■ execute: ’’.
Envoie au serveur la liste des identifiants de composants pour qu’ils soient traités au cours de la phase d’exécution de la requête.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 413
■■ render:’’. Traduit la liste des
identifiants de composants qui sont à mettre à jour au cours de la phase de rendu de la requête. Le code suivant, par exemple, affiche un bouton qui appelle la fonction jsf. ajax.request lorsqu’on clique dessus (événement onclick). Le paramètre this désigne l’élément lui-même (le bouton) et les options désignent les identifiants des composants :
Lorsque le client fait une requête Ajax, le cycle de vie de la page sur le serveur reste le même (il passe par les mêmes six phases). L’avantage principal est que la réponse ne renvoie au navigateur qu’un petit extrait de données au lieu d’une page HTML complète. La phase Application de la requête détermine si la requête adressée est "partielle" ou non : l’objet PartialViewContext est utilisé tout au long du cycle de vie de la page et contient les méthodes et les propriétés pertinentes permettant de traiter une requête partielle et de produire une réponse partielle. À la fin du cycle de vie, la réponse Ajax (ou, à proprement parler, la réponse partielle) est envoyée au client au cours de la phase Rendu de la réponse – elle est généralement formée de XHTML, XML ou JSON, qui sera analysé par le code JavaScript s’exécutant côté client. Récapitulatif
L’exemple du Listing 12.13 montre comment utiliser Ajax et son support par JSF. La section "Récapitulatif" du Chapitre 10 a montré comment insérer de nouveaux livres dans la base de données à l’aide d’un bean géré nommé BookController. La navigation était simple : dès que le livre était créé, l’utilisateur était redirigé vers la page affichant la liste de tous les livres ; en cliquant sur un lien, il pouvait revenir sur le formulaire de création. Nous allons reprendre cet exemple pour lui ajouter quelques fonctionnalités Ajax. Nous voulons maintenant afficher sur la même page le formulaire et la liste des livres (voir Figure 12.8). À chaque fois qu’un livre est créé en cliquant sur le bouton du formulaire, la liste sera rafraîchie afin de faire apparaître ce nouveau livre.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
414
Java EE 6 et GlassFish 3
Figure 12.8 Une seule page pour créer et afficher tous les livres.
Create a new book ISBN :
256 6 56
Tiltle :
Dune
Price :
23.25 The trilogy
Description :
Number of pages : 529 Illustrations :
Create a book
List of the books ISBN
Title
1234 234 H2G2 564 694
Price dollar 12.0
Robots 18.5
Description
Number Of Pages
Illustrations
Scifi IT book
241
false
Best seller
317
true
APress Beginning Java EE 6
Le formulaire en haut de la page ne change pas : seule la liste a besoin d’être rafraîchie. Le code du Listing 12.13 contient le code du formulaire. Pour intégrer Ajax, la page doit d’abord définir la bibliothèque jsf.js à l’aide du marqueur – rien ne change vraiment par rapport au code du Chapitre 10. La variable bookCtrl fait référence au bean géré BookController, qui est responsable de toute la logique métier (en invoquant un EJB pour stocker et récupérer les livres). On accède aux attributs de l’entité Book via le langage d’expression (#{bookCtrl. book.isbn} est lié à l’ISBN). Chaque composant d’entrée possède un identifiant (id="isbn", id="title", etc.) : ceci est très important car cela permet d’identifier chaque nœud du DOM ayant besoin d’interagir de façon asynchrone avec le serveur. Ces identifiants doivent être uniques pour toute la page car l’application doit pouvoir faire correspondre les données à un composant spécifique. Listing 12.13 : La partie formulaire de la page newBook.xhtml
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 415
Create a new book Create a new book
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
416
Java EE 6 et GlassFish 3
Le marqueur représente le bouton qui déclenche l’appel Ajax. Lorsque l’utilisateur clique dessus (événement onclick), la fonction jsf.ajax. request est invoquée avec, pour paramètres, les identifiants des composants (isbn, title, etc.). Grâce à eux, les valeurs des composants correspondants sont alors postées vers le serveur. La méthode doCreateBook() du bean géré est appelée, le nouveau livre est créé et la liste des livres est récupérée. L’affichage de cette liste côté client est effectué avec Ajax grâce à la bibliothèque JS de JSF et la fonction jsf.ajax.request, appelée par l’événement onclick. L’élément render fait référence à booklist, qui est l’identifiant du tableau qui affiche tous les livres (voir Listing 12.14). Listing 12.14 : La partie liste de la page newBook.xhtml List of the books
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 12
Traitement et navigation 417
APress - Beginning Java EE 6
La réponse partielle du serveur contient la portion XHTML à modifier. Le code JavaScript recherche l’élément booklist de la page et applique les modifications nécessaires. Le code de cette réponse dans le Listing 12.15 est assez simple à comprendre : il précise qu’il faut modifier le composant identifié par booklist (). Le corps de l’élément est le fragment XHTML qui doit remplacer les données courantes du tableau. Listing 12.15 : La réponse partielle reçue par le client
ISBN
Title
Price
Description
Number Of Pages
Illustrations
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
418
Java EE 6 et GlassFish 3
1234-234
H2G2
12.0
Scifi IT book
241
false
564-694
Robots
18.5
Best seller
317
true
Résumé Le chapitre précédent avait étudié l’aspect graphique de JSF alors que ce chapitre s’est intéressé à sa partie dynamique. JSF met en œuvre le modèle de conception MVC : sa spécification s’étend de la création des interfaces utilisateurs à l’aide de composants au traitement des données avec les beans gérés. Les beans gérés sont au cœur de JSF car ce sont eux qui traitent la logique métier, appellent les EJB, les bases de données, etc. et qui permettent de naviguer entre les pages. Ils ont une portée et un cycle de vie (qui ressemble à celui des beans de session sans état), et ils déclarent des méthodes et des propriétés qui sont liées aux composants d’interface grâce au langage d’expression. Les annotations et la configuration par exception ont permis de beaucoup simplifier JSF 2.0 car la plupart des configurations XML sont désormais facultatives. Nous avons montré comment chaque composant d’entrée peut gérer les conversions et les validations. JSF définit un ensemble de convertisseurs et de validateurs pour les situations les plus courantes, mais vous pouvez également créer et enregistrer les vôtres de façon très simple. Ajax existe depuis quelques années et JSF 2.0 dispose maintenant pour cette technologie d’un support natif qui permet aux pages web d’invoquer de façon asynchrone des beans gérés. JSF 2.0 définit une bibliothèque JavaScript standard afin que les développeurs n’aient plus besoin d’écrire de scripts et puissent simplement utiliser des fonctions pour rafraîchir des portions de pages.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
13 Envoi de messages La plupart des communications entre les composants que nous avons vus jusqu’à présent sont synchrones : une classe en appelle une autre, un bean géré invoque un EJB, qui appelle une entité, etc. Dans ces cas-là, l’appelant et la cible doivent être en cours d’exécution pour que la communication réussisse et l’appelant doit attendre que la cible termine son exécution avant de pouvoir continuer. À l’exception des appels asynchrones dans EJB, la plupart des composants de Java EE utilisent des appels synchrones (locaux ou distants). Lorsque nous parlons de messages, nous pensons à une communication asynchrone, faiblement couplée, entre les composants. MOM (Middleware Orienté Messages) est un logiciel (un fournisseur) qui autorise les messages asynchrones entre des systèmes hétérogènes. On peut le considérer comme un tampon placé entre les systèmes, qui produisent et consomment les messages à leurs propres rythmes (un système peut, par exemple, tourner 24 heures sur 24, 7 jours sur 7 alors qu’un autre ne tourne que la nuit). Il est faiblement couplé car ceux qui envoient les messages ne savent pas qui les recevra à l’autre extrémité du canal de communication. Pour communiquer, l’expéditeur et le récepteur ne doivent pas nécessairement fonctionner en même temps – en fait, ils ne se connaissent même pas puisqu’ils utilisent un tampon intermédiaire. De ce point de vue, le MOM est totalement différent des technologies comme RMI (Remote Method Invocation), qui exigent qu’une application connaisse les signatures des méthodes d’une application distante. Aujourd’hui, une société typique utilise plusieurs applications, souvent écrites dans des langages différents, qui réalisent des tâches bien définies. Le MOM leur permet de fonctionner de façon indépendante tout en faisant partie d’un workflow. Les messages sont une bonne solution pour intégrer les applications existantes et nouvelles en les couplant faiblement, de façon asynchrone, pourvu qu’émetteur et destinataire se mettent d’accord sur le format du message et sur le tampon intermédiaire.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
420
Java EE 6 et GlassFish 3
Cette communication peut être locale à une organisation ou distribuée entre plusieurs services externes.
Présentation des messages Le MOM, qui existe déjà depuis un certain temps, utilise un vocabulaire spécial. Lorsqu’un message est envoyé, le logiciel qui le stocke et l’expédie est appelé un fournisseur (ou, parfois, un broker). L’émetteur du message est appelé producteur et l’emplacement de stockage du message, une destination. Le composant qui reçoit le message est appelé consommateur – tout composant intéressé par un message à cette destination précise peut le consommer. La Figure 13.1 illustre ces concepts. Figure 13.1 Architecture du MOM.
Mess
Producteur
envoie
Fournisseur de messages
Destination
reçoit
Consommateur
Avec Java EE, tous ces concepts sont pris en charge par l’API JMS (Java Message Service), qui propose un ensemble d’interfaces et de classes permettant de se connecter à un fournisseur, de créer un message, de l’envoyer et de le recevoir. JMS ne transporte pas les messages : il a besoin d’un fournisseur qui s’occupe de le faire. Lorsqu’ils s’exécutent dans un conteneur, les MDB (Message-Driven Beans) peuvent servir à recevoir les messages de façon gérée par le conteneur. JMS
JMS est une API standard de Java permettant aux applications de créer, d’envoyer, de recevoir et de lire les messages de façon asynchrone. Elle définit un ensemble d’interfaces et de classes que les programmes peuvent utiliser pour communiquer avec d’autres fournisseurs de messages. JMS ressemble à JDBC : cette dernière permet de se connecter à plusieurs bases de données (Derby, MySQL, Oracle, DB2, etc.), alors que JMS permet de se connecter à plusieurs fournisseurs (OpenMQ, MQSeries, SonicMQ, etc.). L’API JMS couvre toutes les fonctionnalités nécessaires aux messages, c’est-à-dire leur envoi et leur réception via des destinations :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 421
■■
Les producteurs de messages. L’API permet aux clients de générer un message (émetteurs et éditeurs).
■■
Les consommateurs de messages. Elle permet aux applications clientes de consommer des messages (récepteurs ou abonnés).
■■
Les messages. Un message est formé d’un en-tête, de propriétés et d’un corps contenant différentes informations (texte, objets, etc.).
■■
Les connexions et les destinations. L’API fournit plusieurs fabriques permettant d’obtenir des fournisseurs et des destinations (files d’attente et sujets).
MDB
Les MDB sont des consommateurs de messages asynchrones qui s’exécutent dans un conteneur EJB. Comme on l’a vu aux Chapitres 6 à 9, le conteneur prend en charge plusieurs services (transaction, sécurité, concurrence, acquittement des messages, etc.) ; ce qui permet au MDB de se consacrer à la consommation des messages JMS. Les MDB étant sans état, le conteneur EJB peut avoir de nombreuses instances s’exécutant de façon concurrente pour traiter les messages provenant de différents producteurs JMS. Bien que les MDB ressemblent aux beans sans état, on ne peut pas y accéder directement par les applications : la seule façon de communiquer avec eux consiste à envoyer un message à la destination que le MDB surveille. En général, les MDB surveillent une destination (file d’attente ou sujet) et consomment et traitent les messages qui y arrivent. Ils peuvent également déléguer la logique métier à d’autres beans de session sans état de façon transactionnelle. Étant sans état, les MDB ne mémorisent pas l’état d’un message à l’autre. Ils répondent aux messages JMS reçus du conteneur, alors que les beans de session sans état répondent aux requêtes clientes via une interface appropriée (locale, distante ou sans interface).
Résumé de la spécification des messages En Java, les messages sont essentiellement représentés par JMS, qui peut être utilisée par les applications qui s’exécutent dans un environnement Java SE ou Java EE. MDB est l’extension entreprise d’un consommateur JMS et est lié à la spécification EJB.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
422
Java EE 6 et GlassFish 3
Bref historique des messages
Jusqu’à la fin des années 1980, les sociétés ne disposaient pas de moyens simples pour lier leurs différentes applications : les développeurs devaient écrire des adaptateurs logiciels pour que les systèmes traduisent les données des applications sources dans un format reconnu par les applications destinataires (et réciproquement). Pour gérer les différences de puissance et de disponibilité de serveurs, il fallait créer des tampons pour faire attendre les traitements. En outre, l’absence de protocoles de transport homogènes exigeait de créer des adaptateurs de protocoles de bas niveau. Le middleware a commencé à émerger à la fin des années 1980 afin de résoudre ces problèmes d’intégration. Les premiers MOM furent créés comme des composants logiciels séparés que l’on plaçait au milieu des applications pour "faire de la plomberie" entre les systèmes. Ils étaient capables de gérer des plates-formes, des langages de programmation, des matériels et des protocoles réseaux différents. JMS 1.1
La spécification JMS fut publiée en août 1998. Elle a été mise au point par les principaux éditeurs de middleware afin d’ajouter à Java les fonctionnalités des messages. La JSR 914 apporta quelques modifications mineures (JMS 1.0.1, 1.0.2 et 1.0.2b) pour finalement atteindre la version 1.1 en avril 2002. Depuis, cette spécification n’a pas changé. JMS 1.1 a été intégrée dans J2EE 1.2 et fait depuis partie de Java EE. Cependant, JMS et les MDB ne font pas partie de la spécification Web Profile que nous avons décrite au premier chapitre, ce qui signifie qu’ils ne seront disponibles que sur les serveurs d’applications qui implémentent l’intégralité de la plate-forme Java EE 6. Bien que JMS soit une API touffue et de bas niveau, elle fonctionne très bien. Malheureusement, elle n’a pas été modifiée ou améliorée dans Java EE 5 ni dans Java EE 6. EJB 3.1
Les MDB ont été introduits avec EJB 2.0 et améliorés avec EJB 3.0 et par le paradigme général de simplification de Java EE 5. Ils n’ont pas été modifiés en interne car ils continuent d’être des consommateurs de messages, mais l’introduction des annotations et de la configuration par exception facilite beaucoup leur écriture. La nouvelle spécification EJB 3.1 (JSR 318) n’apporte pas de modifications notables aux MDB.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 423
Comme on l’a vu au Chapitre 7, les appels asynchrones sont désormais possibles avec les beans de session sans état (en utilisant l’annotation @Asynchronous) et les threads ne sont pas autorisés dans les EJB. Dans les versions précédentes de Java EE, les appels asynchrones entre EJB étant impossibles, la seule solution consistait alors à passer par JMS et les MDB, ce qui était coûteux puisqu’il fallait utiliser de nombreuses ressources (destinations, connexions, fabriques JMS, etc.) simplement pour appeler une méthode de façon asynchrone. Avec EJB 3.1, les appels asynchrones étant devenus possibles entre les beans de session, nous pouvons utiliser les MDB pour réaliser ce pourquoi ils ont été initialement créés : intégrer des systèmes grâce aux messages. Implémentation de référence
L’implémentation de référence de JMS est OpenMQ (Open Message Queue), qui est open-source depuis 2006 et peut être utilisée dans des applications JMS autonomes ou intégrées dans un serveur d’applications. OpenMQ est le fournisseur de messages par défaut de GlassFish et, à la date où ce livre est écrit, en est à sa version 4.4. Elle respecte évidemment la spécification JMS et lui ajoute de nombreuses fonctionnalités supplémentaires comme UMS (Universal Message Service), les clusters et bien d’autres choses encore.
Envoi et réception d’un message Étudions un exemple simple pour nous faire une idée de JMS. Comme on l’a déjà évoqué, JMS utilise des producteurs, des consommateurs et des destinations : le producteur envoie un message à la destination, qui est surveillée par le consommateur qui attend qu’un message y arrive. Les destinations sont de deux types : les files d’attente (pour les communications point à point) et les sujets (pour les communications de type Publication-Abonnement). Dans le Listing 13.1, un producteur envoie un message de texte à une file surveillée par le consommateur. Listing 13.1 : La classe Sender produit un message dans une Queue public class Sender { public static void main(String[] args) { // Récupération du contexte JNDI Context jndiContext = new InitialContext();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
424
Java EE 6 et GlassFish 3
// Recherche des objets administrés ConnectionFactory connectionFactory = (ConnectionFactory) „ jndiContext.lookup("jms/javaee6/ConnectionFactory"); Queue queue = (Queue) jndiContext.lookup("jms/javaee6/Queue"); // Création des artéfacts nécessaires pour se connecter // à la file Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, „ Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(queue); // Envoi d’un message de texte à la file TextMessage message = session.createTextMessage(); message.setText("This is a text message"); producer.send(message); connection.close(); } }
Le code du Listing 13.1 représente une classe Sender qui ne dispose que d’une méthode main(). Celle-ci commence par instancier un contexte JNDI pour obtenir des objets ConnectionFactory et Queue. Les fabriques de connexions et les destinations (files d’attente et sujets) sont appelées objets administrés : ils doivent être créés et déclarés dans le fournisseur de messages (OpenMQ, ici). Tous les deux ont un nom JNDI (la file d’attente, par exemple, s’appelle jms/javaee6/Queue) et doivent être recherchés dans l’arborescence JNDI. Lorsque les deux objets administrés ont été obtenus, la classe Sender utilise l’objet ConnectionFactory pour créer une connexion (objet Connection) afin d’obtenir une session. Cette dernière est utilisée pour créer un producteur (objet MessageProducer) et un message (objet TextMessage) sur la file de destination (session. createProducer(queue)). Le producteur envoie ensuite ce message de type texte. Bien que ce code soit largement commenté, nous avons délibérément omis le traitement des exceptions JNDI et JMS. Le code pour recevoir le message est quasiment identique. En fait, les premières lignes de la classe Receiver du Listing 12.2 sont exactement les mêmes : on crée un contexte JNDI, on recherche la fabrique de connexion et la file d’attente, puis on se connecte. Les seules différences tiennent au fait que l’on utilise un MessageConsumer au lieu d’un MessageProducer et que le récepteur entre dans une boucle sans fin pour surveiller la file d’attente (nous verrons plus tard que l’on peut éviter cette boucle en utilisant un écouteur de messages, ce qui est une technique plus classique). Lorsque le message arrive, on le consomme et on affiche son contenu.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 425
Listing 13.2 : La classe Receiver consomme un message d’une file d’attente public class Receiver { public static void main(String[] args) { // Récupération du contexte JNDI Context jndiContext = new InitialContext(); // Recherche des objets administrés ConnectionFactory connectionFactory = (ConnectionFactory) „ jndiContext.lookup("jms/javaee6/ConnectionFactory"); Queue queue = (Queue) jndiContext.lookup("jms/javaee6/Queue"); // Création des artéfacts nécessaires pour se connecter // à la file Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, „ Session.AUTO_ACKNOWLEDGE); MessageConsumer consumer = session.createConsumer(queue); connection.start(); // Boucle pour recevoir les messages while (true) { TextMessage message = (TextMessage) consumer.receive(); System.out.println("Message received: " + message.getText()); } } }
Nous pouvons maintenant nous intéresser à l’API de JMS, définir les objets administrés et les classes nécessaires et voir comment traduire tout ceci dans un MDB.
Java Messaging Service Au niveau le plus haut, l’architecture de JMS est formée des composants suivants (voir Figure 13.2) : ■■
Un fournisseur. JMS n’est qu’une spécification et a donc besoin d’une implémentation sous-jacente pour router les messages, c’est-à-dire un fournisseur. Celui-ci gère le tampon et la livraison des messages en fournissant une implémentation de l’API JMS.
■■
Clients. Un client est une application ou un composant Java qui utilise l’API JMS pour consommer ou produire un message JMS. On parle de client JMS car c’est un client du fournisseur sous-jacent. "Client" est le terme générique pour
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
426
Java EE 6 et GlassFish 3
désigner un producteur, un émetteur, un éditeur, un consommateur, un récepteur ou un abonné. ■■
Messages. Ce sont les objets que les clients envoient et reçoivent du fournisseur JMS.
■■
Objets administrés. Pour qu’un fournisseur implémente totalement JMS, les objets administrés (fabriques de connexion et destinations) doivent être placés dans une arborescence JNDI et on doit pouvoir y accéder par des recherches JNDI.
Figure 13.2
produit un message
Architecture de JMS.
Fournisseur JMS
Consommateur
Producteur
recherche les objets administrés
consomme un message
Annuaire JNDI
recherche les objets administrés
Le fournisseur de messages permet de mettre en place une communication asynchrone en offrant une destination où seront stockés les messages en attendant de pouvoir être délivrés à un client. Il existe deux types de destinations correspondant chacun à un modèle d’architecture spécifique : ■■
Le modèle point à point (P2P). Dans ce modèle, la destination qui stocke les messages est appelée file d’attente. Lorsqu’on utilise des messages point à point, un client place un message dans une file et un autre client reçoit le message. Lorsque le message a été acquitté, le fournisseur le supprime de la file.
■■
Le modèle publication-abonnement. Ici, la destination s’appelle un sujet. Avec ce modèle, un client publie un message dans un sujet et tous les abonnés le reçoivent.
La spécification JMS fournit un ensemble d’interfaces pouvant être utilisées par les deux modèles. Le Tableau 13.1 énumère le nom générique de chaque interface (Session, par exemple) et son nom spécifique pour chaque modèle (QueueSession, TopicSession). Vous remarquerez également que les vocabulaires sont différents : un consommateur est appelé récepteur dans le modèle P2P et abonné dans le modèle publication/abonnement.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 427
Tableau 13.1 : Interfaces utilisées en fonction du type de destination
Générique
Point à Point
Publication-abonnement
Destination
Queue
Topic
ConnectionFactory
QueueConnectionFactory
TopicConnectionFactory
Connection
QueueConnection
TopicConnection
Session
QueueSession
TopicSession
MessageConsumer
QueueReceiver
TopicSubscriber
MessageProducer
QueueSender
TopicPublisher
Point à point
Dans le modèle P2P, un unique message va d’un unique producteur (le point A) à un unique consommateur (le point B). Ce modèle repose sur le concept de files de messages, d’émetteurs et de récepteurs (voir Figure 13.3). Une file stocke les messages envoyés par l’émetteur jusqu’à ce qu’ils soient consommés – les timings de l’émetteur et du récepteur sont indépendants, ce qui signifie que l’émetteur peut produire des messages et les envoyer dans la file lorsqu’il le désire et qu’un récepteur peut les consommer quand il le souhaite. Lorsqu’un récepteur est créé, il reçoit tous les messages envoyés dans la file, même ceux qui ont été envoyés avant sa création. Figure 13.3 Le modèle P2P.
Mess
Émetteur
envoie
Mess
reçoit
Récepteur
Chaque message est envoyé dans une file spécifique d’où le récepteur extrait les messages. Les files conservent tous les messages tant qu’ils n’ont pas été consommés ou jusqu’à ce qu’ils expirent. Le modèle P2P est utilisé lorsqu’il n’y a qu’un seul récepteur pour chaque message – une file peut avoir plusieurs consommateurs mais, une fois qu’un récepteur a consommé un message, ce dernier est supprimé de la file et aucun autre consommateur ne peut donc le recevoir. À la Figure 13.4, un seul émetteur produit trois messages et deux récepteurs consomment chacun un message qui ne sera pas disponible
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
428
Java EE 6 et GlassFish 3
pour l’autre. JMS garantit également qu’un message ne sera délivré qu’une seule fois. Figure 13.4 Plusieurs récepteurs.
Mess Mess Mess 1 2 3
Émetteur
Récepteur 1
Mess 3
Récepteur 2
Mess 1
Mess 2
Notez que P2P ne garantit pas que les messages seront délivrés dans un ordre particulier. En outre, s’il y a plusieurs récepteurs possibles pour un message, le choix du récepteur est aléatoire. Publication-abonnement
Dans ce modèle, un unique message envoyé par un seul producteur peut être reçu par plusieurs consommateurs. Ce modèle repose sur le concept de sujets, d’éditeurs et d’abonnés (voir Figure 13.5). Les consommateurs sont appelés abonnés car ils doivent d’abord s’abonner à un sujet. C’est le fournisseur qui gère le mécanisme d’abonnement/désabonnement, qui a lieu de façon dynamique. Figure 13.5 Modèle Publicationabonnement.
Mess
s'abonne
publie
reçoit
Abonné
Éditeur
Mess
Le sujet conserve les messages jusqu’à ce qu’ils aient été distribués à tous les abonnés. À la différence du modèle P2P, les timings entre éditeurs et abonnés sont liés : ces derniers ne reçoivent pas les messages envoyés avant leur abonnement et un abonné resté inactif pendant une période donnée ne recevra pas les messages publiés entre-temps lorsqu’il redeviendra actif. Comme nous le verrons plus tard, ceci peut être évité car l’API JMS dispose du concept d’abonné durable. Plusieurs abonnés peuvent consommer le même message. Ce modèle peut donc être utilisé par les applications de type "diffusion", dans lesquelles un même message est
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 429
délivré à plusieurs consommateurs. À la Figure 13.6, par exemple, l’éditeur envoie trois messages qui seront reçus par tous les abonnés. Figure 13.6 Plusieurs abonnés. Éditeur
Mess Mess Mess 3 2 1
Abonné 1
Mess 3
Mess 1
Mess 2
Abonné 2
Mess 1
Mess 3
Mess 2
API JMS
L’API JMS se trouve dans le paquetage javax.jms et fournit des classes et des interfaces aux applications qui ont besoin d’un système de messages (voir Figure 13.7). Elle autorise les communications asynchrones entre les clients en fournissant une connexion à un fournisseur et une session au cours de laquelle les messages peuvent être créés, envoyés et reçus. Ces messages peuvent contenir du texte ou différents types d’objets. Figure 13.7
ConnectionFactory
API JMS (inspirée de la Figure 2.1 de la spécification JMS 1.1).
crée Connection crée crée
MessageProducer
envoie à
Session
crée
Message
MessageConsumer
Destination
reçoit de
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
430
Java EE 6 et GlassFish 3
Objets administrés
Les objets administrés sont des objets qui ne sont pas configurés par programmation : le fournisseur de messages les rend disponibles dans l’espace de noms JNDI. Comme les sources de données JDBC, les objets administrés ne sont créés qu’une seule fois. Il en existe deux types pour JMS : ■■
Les fabriques de connexion. Ces objets sont utilisés par les clients pour créer une connexion vers une destination.
■■
Les destinations. Ces objets sont des points de distribution qui reçoivent, stockent et distribuent les messages. Les destinations peuvent être des files d’attente (P2P) ou des sujets (Publication-abonnement).
Les clients JMS accèdent à ces objets via des interfaces portables en les recherchant dans l’espace de noms JNDI. GlassFish fournit plusieurs moyens d’en créer : en utilisant la console d’administration, avec l’outil en ligne de commande asadmin ou avec l’interface REST. Fabriques de connexion
Ce sont les objets administrés qui permettent à une application de se connecter à un fournisseur en créant par programme un objet Connection. Une fabrique de connexion encapsule les paramètres de configuration définis par un administrateur. Il en existe trois types : est une interface pouvant être utilisée à la fois par les communications P2P et Publication-abonnement.
■■ javax.jms.ConnectionFactory
■■ javax.jms.QueueConnectionFactory est une interface qui hérite de Connection Factory
et qui ne sert qu’aux communications P2P.
■■ javax.jms.TopicConnectionFactory est une interface qui hérite de Connection Factory
et qui ne sert qu’aux communications Publication-abonnement.
Le programme doit d’abord obtenir une fabrique de connexion en effectuant une recherche JNDI. Le fragment de code suivant, par exemple, obtient un objet InitialContext et s’en sert pour rechercher un QueueConnectionFactory et un TopicConnectionFactory par leurs noms JNDI : Context ctx = new InitialContext(); QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory) ctx.lookup("QConnFactory"); TopicConnectionFactory topicConnectionFactory = (TopicConnectionFactory) ctx.lookup("TConnFactory");
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 431
Les fabriques de files d’attente/sujets sont nécessaires lorsque l’on a besoin d’accéder à des détails spécifiques de ces modèles. Dans le cas contraire, vous pouvez directement utiliser une ConnectionFactory générique pour les deux situations : Context ctx = new InitialContext(); ConnectionFactory ConnectionFactory = (QueueConnectionFactory) ctx.lookup("GenericConnFactory");
La seule méthode disponible dans ces trois interfaces est la méthode createConnection(), qui renvoie un objet Connection. Cette méthode est surchargée pour pouvoir créer une connexion en utilisant l’identité de l’utilisateur par défaut ou en précisant un nom d’utilisateur et un mot de passe (voir Listing 13.3). Listing 13.3 : Interface ConnectionFactory public interface ConnectionFactory { Connection createConnection() throws JMSException; Connection createConnection(String userName, String password) throws JMSException; }
Destinations
Une destination est un objet administré contenant des informations spécifiques à un fournisseur, comme l’adresse de destination. Cependant, ces informations sont cachées au client JMS par l’interface standard javax.jms.Destination. Il existe deux types de destinations, représentées par deux interfaces héritant de Destination : ■■ javax.jms.Queue,
utilisée pour les communications P2P ;
■■ javax.jms.Topic,
utilisée pour les communications Publication-abonnement.
Ces interfaces ne contiennent qu’une seule méthode, qui renvoie le nom de la destination. Comme pour les fabriques de connexion, ces objets s’obtiennent par une recherche JNDI. Injection
Les fabriques de connexion et les destinations sont des objets administrés qui résident dans un fournisseur de messages et qui doivent être déclarés dans l’espace de noms JNDI. Lorsque le code client s’exécute dans un conteneur (EJB, servlet, client d’application), vous pouvez utiliser l’injection des dépendances à la place des recherches JNDI grâce à l’annotation @Resource (voir la section "Injection de dépendances" du Chapitre 7), ce qui est bien plus simple.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
432
Java EE 6 et GlassFish 3
Le Tableau 13.2 énumère les éléments de cette annotation. Tableau 13.2 : API de l’annotation @javax.annotation.Resource
Élément
Description
name
Nom JNDI de la ressource.
type
Type Java de la ressource (javax.sql.DataSource ou javax. jms.Topic, par exemple).
authenticationType
Type d’authentification à utiliser pour la ressource (soit le conteneur, soit l’application
shareable
Indique si la ressource peut être ou non partagée.
mappedName
Nom spécifique auquel associer la ressource.
description
Description de la ressource.
Pour tester cette annotation, nous utiliserons une classe Receiver avec une méthode qui reçoit des messages textuels. Dans le Listing 13.2, la fabrique de connexions et la file d’attente étaient obtenues par des recherches JNDI ; dans le Listing 13.4, les noms JNDI sont indiqués dans les annotations @Resource : si cette classe Receiver s’exécute dans un conteneur, elle recevra donc par injection des références ConnectionFactory et Queue lors de son initialisation. main()
Listing 13.4 : La classe Receiver obtient par injection des références à des ressources JMS public class Receiver { @Resource(name private static @Resource(name private static
public static void main(String[] args) { // Crée les artéfacts nécessaires à la connexion à la file Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageConsumer consumer = session.createConsumer(queue); connection.start(); // Boucle pour recevoir les messages while (true) { TextMessage message = (TextMessage) consumer.receive();
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Pour simplifier, nous avons omis le traitement des exceptions. Notez que l’annotation @Resource porte sur des attributs privés : l’injection de ressources peut s’appliquer à différents niveaux de visibilité (privée, protégée, paquetage ou publique). Connexion
L’objet javax.jms.Connection que l’on crée avec la méthode createConnection() de la fabrique de connexion encapsule une connexion au fournisseur JMS. Les connexions sont thread-safe et conçues pour être partagées car l’ouverture d’une connexion est une opération coûteuse en terme de ressources. Une session (javax. jms.Session), par contre, fournit un contexte mono-thread pour envoyer et recevoir les messages ; une connexion permet de créer une ou plusieurs sessions. Comme les fabriques de connexion, les connexions peuvent être de trois types en utilisant l’interface générique Connection ou les interfaces QueueConnection et TopicConnection qui dérivent de celle-ci. La création de l’objet "connexion" dépend du type de la fabrique dont vous disposez : Connection connection = connFactory.createConnection(); QueueConnection connection = queueConnFactory.createQueueConnection(); TopicConnection connection = topicConnFactory.createTopicConnection();
Dans le Listing 13.4, le récepteur doit appeler la méthode start() avant de pouvoir consommer les messages. La méthode stop() permet d’arrêter temporairement d’en recevoir sans pour autant fermer la connexion. connection.start(); connection.stop();
Les connexions qui ont été créées doivent être fermées lorsque l’application se termine. La fermeture d’une connexion ferme également ses sessions, ses producteurs ou ses consommateurs. connection.close();
Session
On crée une session en appelant la méthode createSession() de la connexion. Une session fournit un contexte transactionnel qui permet de regrouper plusieurs
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
434
Java EE 6 et GlassFish 3
essages dans une unité de traitement atomique : si vous envoyez plusieurs mesm sages au cours de la même session, JMS garantira qu’ils arrivent tous à destination dans leur ordre d’émission, ou qu’aucun n’arrivera. Ce comportement est mis en place lors de la création de la session : Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Le premier paramètre de cette méthode indique si la session est transactionnelle ou non. Une valeur true précise que la requête d’envoi des messages ne sera pas réalisée tant que l’on n’a pas appelé la méthode commit() de la session ou que la session est fermée. Si ce paramètre vaut false, la session ne sera pas transactionnelle et les messages seront envoyés dès l’appel de la méthode send(). Le second paramètre indique que la session accuse automatiquement réception des messages lorsqu’ils ont été correctement reçus. Une session s’exécute dans un seul thread et permet de créer des messages, des producteurs et des consommateurs. Comme tous les objets que nous avons vus jusqu’à présent, il existe deux variantes de sessions : QueueSession et TopicSession. Un objet Session générique permet d’utiliser l’une ou l’autre via une interface unique. Messages Figure 13.8 Structure d’un message JMS.
Les clients échangent des messages pour communiquer ; un producteur envoie un message à une destination où un client le recevra. Les messages sont des objets qui encapsulent les informations et qui sont divisés en trois parties (voir Figure 13.8) : ■■
Un en-tête, qui contient les informations classiques pour identifier et acheminer le message.
■■
Les propriétés, qui sont des paires nom/valeur que l’application peut lire ou écrire. Elles permettent également aux destinations de filtrer les messages selon les valeurs de ces propriétés.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
■■
Envoi de messages 435
Un corps, qui contient le véritable message et peut utiliser plusieurs formats (texte, octets, objet, etc.).
En-tête
L’en-tête contient des paires nom/valeur prédéfinies, communes à tous les messages et que les clients et les fournisseurs utilisent pour identifier et acheminer les messages. Elles peuvent être considérées comme les métadonnées du message car elles fournissent des informations sur le message lui-même. Chaque champ a des méthodes getter et setter qui lui sont associées dans l’interface javax.jms. Message. Bien que certains champs de l’en-tête soient prévus pour être initialisés par un client, la plupart sont automatiquement fixés par la méthode send() ou publish(). Le Tableau 13.3 décrit tous les champs de l’en-tête d’un message JMS. Tableau 13.3 : Champs de l’en-tête1
Champ
Description
Initialisé par
JMSDestination
Destination à laquelle est envoyé le message.
send()
JMSDeliveryMode
JMS reconnaît deux modes de délivrance send() ou publish() des messages. Le mode PERSISTENT demande au fournisseur de garantir que le message ne sera pas perdu lors de son transfert à cause d’une erreur. Le mode NON_PERSISTENT est le mode de délivrance le moins coûteux car il n’exige pas d’enregistrer le message sur un support persistant.
JMSMessageID
Valeur identifiant de manière unique chaque message envoyé par un fournisseur.
JMSTimestamp
Horodatage indiquant l’instant où le send() ou publish() message a été passé à un fournisseur pour être envoyé.
send()
ou publish()
ou publish()
1 Spécification de JMS 1.1, "3.4: Message Header Fields" (http://jcp.org/en/jsr/detail?id=914).
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
436
Java EE 6 et GlassFish 3
Tableau 13.3 : Champs de l’en-tête (suite)
Champ
Description
Initialisé par
JMSCorrelationID
Un client peut utiliser ce champ pour lier un message à un autre : pour lier un message de réponse à son message de requête, par exemple.
Le client
JMSReplyTo
Contient la destination à laquelle renvoyer la réponse au message.
Le client
JMSRedelivered
Valeur booléenne initialisée par le fournisseur pour indiquer si un message a été à nouveau délivré.
Le fournisseur
JMSType
Identifiant du type du message.
Le client
JMSExpiration
Date d’expiration d’un message envoyé, send() ou publish() calculée et initialisée en fonction de la valeur time-to-live passée à la méthode send().
JMSPriority
JMS définit dix niveaux de priorités, 0 étant la plus faible et 9, la plus forte.
send()
ou publish()
Vous pouvez accéder à ces champs via l’interface Message : message.getJMSCorrelationID(); message.getJMSMessageID(); message.setJMSPriority(6);
Propriétés
Outre les champs d’en-tête, l’interface javax.jms.Message permet de gérer les valeurs des propriétés, qui sont exactement comme les en-têtes, mais créées explicitement par l’application au lieu d’être standard pour tous les messages. Elles permettent d’ajouter des champs d’en-têtes optionnels à un message, que le client peut choisir de recevoir ou non via des sélecteurs. Leurs valeurs peuvent être de type boolean, byte, short, int, long, float, double et String. Le code permettant de les initialiser et de les lire est de la forme : message.setFloatProperty("orderAmount", 1245.5f); message.getFloatProperty("or derAmount");
Corps
Le corps d’un message est facultatif et contient les données à envoyer ou à recevoir. Selon l’interface utilisée, il peut contenir des données dans différents formats, énumérés dans le Tableau 13.4.
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 437
Tableau 13.4 : Types de messages
Interface
Description
StreamMessage
Message contenant un flux de valeurs primitives Java. Il est rempli et lu séquentiellement.
MapMessage
Message contenant un ensemble de paires nom/valeur où les noms sont des chaînes et les valeurs sont d’un type primitif.
TextMessage
Message contenant une chaîne (du XML, par exemple).
ObjectMessage
Message contenant un objet sérialisable ou une collection d’objets sérialisables.
BytesMessage
Message contenant un flux d’octets.
En dérivant l’interface javax.jms.Message, vous pouvez créer votre propre format de message. Notez que lorsqu’un message est reçu, son corps est en lecture seule. Selon le type du message, des méthodes différentes permettent d’accéder à son contenu. Un message textuel disposera des méthodes getText() et setText(), un message d’objet disposera des méthodes getObject() et setObject(), etc. textMessage.setText("This is a text message"); textMessage.getText(); bytesMessage.readByte(); objectMessage.getObject();
MessageProducer
Un producteur de messages est un objet créé par une session pour envoyer des messages à une destination. L’interface générique javax.jms.MessageProducer peut être utilisée pour obtenir un producteur spécifique disposant d’une interface unique. Dans le cas du modèle P2P, un producteur de messages est appelé émetteur et implémente l’interface QueueSender. Avec le modèle Publication-abonnement, il s’appelle éditeur et implémente TopicPublisher. Selon l’interface utilisée, un message créé est envoyé (P2P) ou publié (Publication-abonnement) : messageProducer.send(message); queueSender.send(message); topicPublisher.publish(message);
Un producteur peut préciser un mode de livraison par défaut, une priorité et un délai d’expiration des messages qu’il envoie. Les étapes suivantes expliquent comment créer un éditeur qui publie un message dans un sujet (voir Listing 13.5) :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
438
Java EE 6 et GlassFish 3
1. Obtenir une fabrique de connexions et un sujet par injection (ou par une recherche JNDI). 2. Créer un objet Connection avec la fabrique de connexions. 3. Créer un objet Session avec la connexion. 4. Créer un MessageProducer (ici, nous aurions pu choisir un TopicPublisher) en utilisant l’objet Session. 5. Créer un ou plusieurs messages d’un type quelconque (ici, nous avons utilisé un TextMessage) en utilisant l’objet Session. Après sa création, remplir le message avec les données (nous nous sommes servis ici de la méthode setText()). 6. Envoyer un ou plusieurs messages dans le sujet à l’aide de la méthode MessageProducer.send() (ou TopicPublisher.publish()). Listing 13.5 : La classe Sender envoie un message dans un sujet public class Sender { @Resource(mappedName = "jms/javaee6/ConnectionFactory") private static ConnectionFactory connectionFactory; @Resource(mappedName = "jms/javaee6/Topic") private static Topic topic; public static void main(String[] args) { // Crée les artéfacts nécessaires à la connexion au sujet. Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(topic); // Envoie un message texte dans le sujet TextMessage message = session.createTextMessage(); message.setText("This is a text message"); producer.send(message); connection.close(); } }
MessageConsumer
Un client utilise un MessageConsumer pour recevoir les messages provenant d’une destination. Cet objet est créé en passant une Queue ou un Topic à la méthode createConsumer() de la session. Dans le cas du modèle P2P, un consommateur
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 439
de messages peut implémenter l’interface QueueReceiver et, pour le modèle Publi cation-abonnement, l’interface TopicSubscriber. Les messages sont intrinsèquement asynchrones car les timings des producteurs et des consommateurs sont totalement indépendants. Cependant, un client peut consommer les messages de deux façons : ■■
De façon synchrone. Un récepteur récupère explicitement le message de la destination en appelant la méthode receive(). Les exemples précédents, par exemple, utilisent une boucle sans fin qui se bloque jusqu’à ce que le message arrive.
■■
De façon asynchrone. Un récepteur s’enregistre auprès d’un événement qui est déclenché à l’arrivée du message. Il doit alors implémenter l’interface MessageListener : à chaque fois qu’un message arrive, le fournisseur lui délivrera en appelant la méthode onMessage().
Ces deux types de consommateurs sont illustrés à la Figure 13.9. Figure 13.9 Consommateurs synchrones et asynchrones.
Mess
Mess
envoie
collecte
Mess
enregistre
Émetteur
Émetteur
prévient envoie
Récepteur synchrone
Récepteur asynchrone
collecte Mess
Livraison synchrone
Un récepteur synchrone doit lancer une connexion, boucler en attendant qu’un message arrive et demander le message arrivé à l’aide de l’une de ses méthodes receive() : leurs différentes variantes permettent au client de demander ou d’attendre le prochain message. Les étapes suivantes expliquent comment créer un récepteur synchrone qui consomme un message d’un sujet (voir Listing 13.6) :
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
440
Java EE 6 et GlassFish 3
1. Obtenir une fabrique de connexions et un sujet en utilisant l’injection (ou une recherche JNDI). 2. Créer un objet Connection à l’aide de la fabrique. 3. Créer un objet Session à l’aide de la connexion. 4. Créer un MessageConsumer (ici, il pourrait également s’agir d’un scriber) à l’aide de l’objet Session.
TopicSub–
5. Lancer la connexion. 6. Boucler et appeler la méthode receive() sur l’objet consommateur. Cette méthode se bloque si la file est vide et attend qu’un message arrive. Ici, la boucle sans fin attend que d’autres messages arrivent. 7. Traiter le message renvoyé par receive() en utilisant la méthode TextMessage. getText() (si c’est un message texte). Listing 13.6 : Le Receiver consomme les messages de façon synchrone public class Receiver { @Resource(mappedName = "jms/javaee6/ConnectionFactory") private static ConnectionFactory connectionFactory; @Resource(mappedName = "jms/javaee6/Topic") private static Topic topic; public static void main(String[] args) { // Crée les artéfacts nécessaires à la connexion au sujet Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageConsumer consumer = session.createConsumer(topic); connection.start(); // Boucle pour recevoir les messages while (true) { TextMessage message = (TextMessage) consumer.receive(); System.out.println("Message received: " + message.getText()); } } }
Livraison asynchrone
La consommation asynchrone repose sur la gestion des événements. Un client peut enregistrer un objet (y compris lui-même) qui implémente l’interface
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 441
essageListener. Un écouteur de messages est un objet qui se comporte comme un M gestionnaire d’événements asynchrones pour les messages : lorsque les messages arrivent, le fournisseur les délivre en appelant la méthode onMessage() de l’écouteur, qui prend un seul paramètre de type Message. Grâce à ce modèle événementiel, le consommateur n’a pas besoin de boucler indéfiniment en attente d’un message : les MDB utilisent ce modèle. Les étapes suivantes décrivent la création d’un écouteur de messages asynchrone (voir Listing 13.7) : 1. La classe implémente l’interface javax.jms.MessageListener, qui définit une seule méthode nommée onMessage(). 2. Obtenir une fabrique de connexions et un sujet en utilisant l’injection (ou une recherche JNDI). 3. Créer un objet Connection à l’aide de la fabrique et un objet Session à l’aide de la connexion. Créer un MessageConsumer à l’aide de l’objet Session. 4. Appeler la méthode setMessageListener() en lui passant une instance de l’interface MessageListener (dans le Listing 13.7, la classe Listener implémente elle-même l’interface MessageListener). 5. Après l’enregistrement de l’écouteur de messages, appeler la méthode start() pour lancer la surveillance de l’arrivée d’un message – si l’on appelle start() avant d’enregistrer l’écouteur, on risque de perdre des messages. 6. Implémenter la méthode onMessage() et traiter le message reçu. À chaque fois qu’un message arrive, le fournisseur appellera cette méthode en lui passant le message. Listing 13.7 : Le consommateur est un écouteur de messages public class Listener implements MessageListener { @Resource(mappedName = "jms/javaee6/ConnectionFactory") private static ConnectionFactory connectionFactory; @Resource(mappedName = "jms/javaee6/Topic") private static Topic topic; public static void main(String[] args) { // Crée les artéfacts nécessaires à la connexion au sujet e Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Certaines applications doivent filtrer les messages qu’elles reçoivent. Lorsqu’un message est diffusé à de nombreux clients, par exemple, il peut être utile de fixer des critères afin qu’ils ne soient consommés que par certains récepteurs : ceci permet d’éviter que le fournisseur gaspille son temps et sa bande passante à transporter des messages à des clients qui n’en ont pas besoin. Nous avons vu que les messages étaient constitués de trois parties : un en-tête, des propriétés et un corps. L’en-tête contient un nombre déterminé de champs (qui forment les métadonnées du message) et les propriétés sont un ensemble de paires nom/valeur personnalisées que l’application peut utiliser pour fixer des valeurs dont elle a besoin. Ces deux parties peuvent servir à mettre en place une sélection : les émetteurs fixent une ou plusieurs propriétés ou des valeurs de champs dans l’en-tête, et le récepteur précise les critères de choix d’un message en utilisant des expressions de sélection (sélecteurs). Seuls les messages correspondant à ces sélecteurs seront alors délivrés. C’est le fournisseur JMS, plutôt que l’application, qui filtrera les messages. Un sélecteur de message est une chaîne contenant une expression dont la syntaxe s’inspire de celle des expressions conditionnelles de SQL92 : session.createConsumer(topic, "JMSPriority < 6"); session. createConsumer(topic, "JMSPriority < 6 „ AND orderAmount < 200"); session.createConsumer(topic, "orderAmount BETWEEN 1000 „ AND 2000");
Le code précédent crée un consommateur en lui passant un sélecteur. Cette chaîne peut utiliser les champs de l’en-tête (JMSPriority < 6) ou des propriétés propres à l’application (orderAmount < 200). De son côté, le producteur fixe ce champ d’entête et cette propriété dans le message de la façon suivante : message.setIntProperty("orderAmount", 1530); message.setJMSPriority(5);
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 13
Envoi de messages 443
Les expressions de sélections peuvent utiliser des opérateurs logiques (NOT, AND, OR), de comparaison (=, >, >=, Scifi IT book false1234-234 24124.0H2G2
Suppression d’un livre
La méthode deleteBook() a le même format que celui de getBookById() car elle utilise un sous-chemin et un identifiant de livre en paramètre – la seule différence est la requête HTTP utilisée. Ici, nous nous servons de la méthode DELETE, qui supprime le contenu associé à l’URL indiquée : @DELETE @Path("{id}/") public void deleteBook(@PathParam("id") Long id) { Book book = em.find(Book.class, id); em.remove(book); }
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
542
Java EE 6 et GlassFish 3
Avec le mode verbeux de cURL, on voit la requête DELETE qui est envoyée et le code d’état 204 No Content qui apparaît dans la réponse pour indiquer que la ressource n’existe plus : curl -X DELETE http://localhost:8080/chapter15-resource„ 1.0/rs/books/61 -v > DELETE /chapter15-resource-1.0/rs/books/61 HTTP/1.1 > User-Agent: curl/7.19.0 (i586-pc-mingw32msvc) „ libcurl/7.19.0 zlib/1.2.3 > Host: localhost:8080 > Accept: */* > < HTTP/1.1 204 No Content < X-Powered-By: Servlet/3.0 < Server: GlassFish/v3
Configuration avec web.xml
Avant de déployer la classe BookResource et l’entité Book, nous devons enregistrer Jersey dans le fichier web.xml (voir Listing 15.15) afin que les requêtes envoyées au chemin /rs soient interceptées par Jersey. Le paramètre com.sun.jersey.config. property.packages indiquera à ce dernier où rechercher les classes Java annotées par JAX-RS. Listing 15.15 : Fichier web.xml déclarant Jersey Jersey Web Application com.sun.jersey.spi.container.servlet.ServletContainer com.sun.jersey.config.property.packages com.apress.javaee6.chapter15 1
customer 27921 at Fri Mar 11 19:07:20 +0100 2011 Propriété de Albiri Sigue
Chapitre 15
Services web REST 543
Jersey Web Application /rs/*
Le descripteur de déploiement associe les requêtes au motif d’URL /rs/*, ce qui signifie que toutes les URL commençant par /rs/ seront traitées par Jersey. Nos exemples avec cURL ont, bien sûr, manipulé des URL commençant par /rs : curl -X GET -H "Accept: application/json" „ http://localhost:8080/chapter15-resource-1.0/rs/books curl -X DELETE http://localhost:8080/chapter15-resource„ 1.0/rs/books/61
Compilation et assemblage avec Maven
L’application web doit être compilée et déployée dans un fichier war (war). Le fichier pom.xml du Listing 15.16 déclare toutes les dépendances nécessaires à la compilation du code (jsr311-api, javax.ejb et javax.persistence). N’oubliez pas que JAXB fait partie de Java SE et qu’il n’y a donc pas besoin d’ajouter cette dépendance. Listing 15.16 : Le fichier pom.xml de Maven pour compiler et assembler l’application web