Les beans sont des composants interconnectables utilisés au sein des couches d'accès aux données et métier d'une application. Ils permettent notamment d'initialiser facilement les composants d'une couche ainsi que d'assurer la communication entre composants de différentes couches. Nous réalisons ici une introduction à leur utilisation avec l'API EJB 3.1 de Java Enterprise Edition 6.
On pourra se référer à ce tutoriel d'Oracle pour plus d'informations sur l'utilisation des beans.
Que sont les beans ?
- Un bean est un composant se présentant sous la forme d'une classe Java.
- Le cycle de vie d'une instance de bean est géré par un container d'application : ce container se charge de l'instantiation, de l'initialisation et de la destruction du bean.
- Un bean permet au développeur de se décharger de certaines tâches confiées au container : gestion des problématiques d'authentification, d'accès concurrents, de transaction, de répartition de beans entre plusieurs machines (accès distant à un bean)...
- L'initialisation d'un bean est réalisée selon le principe d'injection de dépendances : les composants dont a besoin le bean pour fonctionner sont injectés automatiquement par le container lors de sa création.
-
L'usage de conventions par défaut permet d'éviter de trop configurer un bean
- L'approche convention plutôt que configuration était moins marquée sur les versions historiques de l'API EJB (ce qui rendait la création de beans plus fastidieuse)
-
La configuration d'un bean (mode de création, d'utilisation, dépendances injectées) utilise des annotations (possible depuis Java 1.5) du paquetage javax.ejb
- Il est également possible d'utiliser des fichiers de configuration XML (mode de configuration historique), ce qui reste pertinent dans certaines situations (par exemple pour indiquer des paramètres d'accès à une base de données)
Comment exécuter des beans ?
- Les beans ne peuvent pas fonctionner de manière autonome : ils nécessitent l'utilisation d'un serveur d'application
-
Un serveur d'application implante les APIs de Java EE (la couche vue généralement utilisée repose sur un serveur web) dont celles nécessaires à la gestion des beans (EJB 3.1)
-
Quelques serveurs d'application disponibles :
- Glassfish : implantation de référence développée par Oracle qui supporte la totalité de l'API Java EE 6 (et Java EE 7 depuis sa version 4.0)
- TomEE : implantation de la fondation Apache basée sur le container de servlets Tomcat (n'implante que le profile web de Java EE 6)
- Geronimo : sur-ensemble de TomEE implantant la totalité de l'API Java EE (utilisant Tomcat, OpenEJB, OpenJPA...)
- Wildfly : serveur d'application de RedHat (précédemment connu sous le nom de JBoss)
- Autres serveurs d'application : IBM WebSphere, Resin, Jonass...]
-
Quelques serveurs d'application disponibles :
-
Des implantations allégées (lite) de l'API Enterprise Java Beans (version 3.1 ou 3.2) peuvent être utilisées de façon autonome (si un serveur web n'est pas nécessaire)
- Les implantations lite ne comprennent pas les fonctionnalités d'invocation distante des beans, de la communication par message (MDB), les timers...
- L'utilisation d'une implantation lite est simple : il suffit de déclarer comme bibliothèque le fichier jar contenant l'implantation (déclaration dans le classpath)
-
Quelques implantations :
- OpenEJB : implantation utilisée par TomEE et Geronimo
- Embeddable EJB : paquetage EJB extrait de JBoss
Utilisation d'une implantation lite
- On récupère un container de beans à partir duquel on créé un contexte
- On peut ensuite demander à récupérer un bean selon son nom
public class BeanTester { public static void main(String[] args) { EJBContainer container = EJBContainer.createEJBContainer(); Context context = container.getContext(); TestBean testBean; try (EJBContainer container = EJBContainer.createEJBContainer();) { Context context = container.getContext(); TestBean testBean = (TestBean)context.lookup("java:app/bin/TestBean"); testBean.doSomeAction(); } catch (NamingException ex) { System.err.println("Problem while fetching the bean"); } } }
Utilisation d'un serveur applicatif (exemple de WildFly)
Nous pouvons utiliser par exemple Wildfly qui sera utilisé comme serveur applicatif fournissant l'implantation complète de l'API Java EE.
L'installation et la configuration basique de WildFly est assez simple :
- On télécharge l'archive contenant les fichiers binaires de la dernière version stable de WildFly sur le site wildfly.org (9.0.1 à l'écriture de ce
- On extrait l'archive dans un répertoire
- Le serveur WildFly peut être lancé par le script wildflyDirectory/standalone.sh ; on pourra rajouter l'option -Djboss.socket.binding.port-offset=N si l'on souhaite choisir le numéro de port d'attache du serveur web (par défaut 8080 auquel on peut rajouter l'offset défini N)
- Il faut ensuite se rendre avec un navigateur web sur http://localhost:8080 (8080 à remplacer par le port réel du serveur) et suivre les instructions qui demanderont de créer au moins un utilisateur avec le script addUser.sh pour pouvoir par la suite accéder à la console d'administration
- On pourra demander l'installation de greffons supportant JBoss/Wildfly depuis son IDE favori ; par exemple sous Eclipse, depuis le menu Help, on accède au marketplace et on installe la dernière version des JBoss Tools
Les beans entité
- Les beans de type entité sont marqué avec l'annotation @Entity
- Ils sont utilisés pour mettre en oeuvre des objets persistants au niveau modèle par exemple en utilisant l'API JPA 2.0 (dont Hibernate propose par exemple une implantation) ; il s'agit d'une approche de type Container Managed Persistence
- Les beans entités peuvent alternativement utiliser une approche Bean Managed Persistence : celle-ci demande plus de travail car le code de chargement et de sauvegarde du bean est directement codé dans la classe ; cela peut être rébarbatif et nécessite une réécriture des méthodes de chargement et sauvegarde lorsque le support de stockage change (fichiers, base de données...).
Les beans session
- Un bean session est généralement une classe gérant une problématique métier dont une seule instance est nécessaire pour une session de travail donnée
- Contrairement à un bean entité, un bean session n'est pas persistant ; il est donc reconstruit si nécessaire après un redémarrage de la JVM sans considération d'un état pré-existant
- Par convention, on suffixe le nom des beans session par Bean (exemple : CustomerManagerBean, ItemStoreBean...)
- Il est conseillé d'implanter une interface (nom sans le Bean) avec les méthodes à implanter pour le bean session. Cette pratique est même obligatoire pour mettre en place un pattern de type Proxy si le bean session est destiné à être accessible à distance (il faut ajouter l'annotation @Remote à l'interface).
- Il existe trois types de beans sessions chacun lié à des problématiques d'instantiation propres : Stateful, Stateless et Singleton ; le type du bean est indiqué par une annotation sur la classe
Bean session Stateful
- Lorsque les champs d'un bean session déterminent un état, le bean est dit Stateful : chaque instance du bean contient un état
- Chaque client communique avec une instance de bean qui lui est propre (1 client = 1 bean instantié)
Bean session Stateless
- Le bean session ne maintient pas d'état
- Une bean session Stateless peut être instantié plusieurs fois (avec une piscine de beans) mais l'instance utilisée par un client donné n'a pas d'importance : l'appel d'une méthode sur n'importe laquelle des instances aboutit normalement au même résultat
- Un bean Stateless peut être utilisé pour proposer un service web (étant donné que le protocole HTTP est lui-même sans état)
- Un bean Stateless pouvant être recyclé entre différentes invocations de client, celui-ci est préférable pour le passage à l'échelle à un bean avec état (lorsque la logique métier l'autorise)
Bean session Singleton
- Un bean session Singleton est instantié une unique fois pour une application
-
Différents clients peuvent partager la même instance : il faut donc s'assurer que les problématiques de concurrence sont bien traitées par le bean (quand plusieurs clients appelent simultanément des méthodes du bean pouvant potentiellement impacter les mêmes structures de données)
- Certaines annotations permettent de faciliter l'utilisation de modèles de concurrence : @ConcurrencyManagement, @Lock(READ), @Lock(Write), @AccessTimeout(durationInMillis)...
-
Un bean Singleton est normalement instantié de façon paresseuse lors de l'accès du premier client ; il est toutefois possible de forcer l'instantiation au démarrage avec l'annotation @Startup
- Certains beans Singleton peuvent dépendre de l'initialisation préalable d'autres beans Singleton : de telles dépendances peuvent être indiquées par l'annotation @DependsOn("DependentBean1", "DependentBean2", ...)
Voici un exemple d'un bean Singleton ABean initialisé au démarrage et qui entraîne l'instantiation prélable de CBean puis de BBean :
@Singleton @Startup @DependsOn("BBean") public class ABean { ... } @Singleton @DependsOn("CBean") poublic class BBean { ... } @Singleton public class CBean { ... }
Cycle de vie d'un bean session
Lorsqu'une nouvelle instance d'un bean session est nécessaire, celle-ci est créé par le container, cela requiert :
- d'appeler le constructeur par défaut sans argument
- d'injecter les dépendances du bean (chacune des dépendances est indiquée en annotant le champ par @EJB
- d'appeler les éventuelles méthodes de post-construction : des méthodes sont annotées par @PostConstruct (il s'agit d'une méthode sans argument)
Les beans session de type Stateful peuvent être mis en hibernation : il s'agit de les transférer de la mémoire centrale vers un espace de stockage secondaire. Avant cette mis en hibernation les méthodes annotées @PrePassivate sont appelées ; lorsque l'hibernation se termine et le bean replacé en mémoire centrale, les méthodes annotées @PostActivate sont appelées.
Un bean Stateful peut également être détruit par son client. Celui-ci appelle la méthode annotée @Remote pour ce travail. Suite à l'appel de cette méthode, le bean est supprimé.
Avant de détruire définitivement un bean, le container appelle ses méthodes (sans argument et de type de retour void) annotées @PreDestroy : on peut les utiliser pour libérer d'éventuelles ressources précédemment acquises.
L'injection de dépendances
- La problématique de l'injection de dépendances est traitée par la JSR 330 (Dependency Injection for Java) et la JSR 316 (Managed Beans Specification)
- Il existe différents frameworks s'intéressant à l'injection de dépendances développés avant la standardisation du concept dans JavaEE :
- L'injection de dépendances permet de gérer automatiquement le cycle de vie des managed beans (instantiation, destruction) et de mettre en relation les beans inter-dépendants
- Une grande variété de types d'objets peuvent être injectés : des instances d'une classe Java quelconque, des beans session, des EntityManager (contexte de persistance, des références de webservices, des références d'EJB distants, des ressources telles que des DataSource...
Des annotations peuvent être placées sur les champs nécessitant une injection de dépendances :
- @Inject permet d'injecter une instance de n'importe quel type de classe
- @EJB permet d'injecter un bean session
- @Resource permet d'injecter une ressource telle qu'une DataSource JDBC
- @PersistenceContext permet d'injecter un EntityManager dont la configuration a été réalisée dans un fichier persistence.xml
Utilisation d'@Inject avec annotations
Prenons pour exemple l'injection d'une instance de B dans une instance de A :
public class A { @Inject private B b; } public interface B { ... } public class BDefault implements B { ... }
Dans le cas présent, le container recherche pour injecter dans le champ b de A toutes les classes compatibles : il s'agit de B et BDefault. B étant une interface, elle ne peut être instantiée : seule BDefault peut être instantiée : le container l'instantie et l'injecte dans le champ b.
Si plusieurs possibilité d'injection existent, il est nécessaire d'utiliser une annotation supplémentaire pour lever l'amibiguïté ; cela peut être une annotation personnalisée ou alors l'annotation @Named.
Nous pouvons par exemple avoir deux implantations de B avec deux annotations @Named différentes :@Named("first") public class BDefault1 implements B { ... } @Named("second") public class BDefault2 implements B { ... }On peut alors lever l'ambiguïté sur la classe à utiliser pour le champ b de A en ajoutant une annotation @Named
public class A { @Inject @Named("second") private B b; }Ici, on utilisera BDefault2 qui correspond à l'annotation indiquée.
Quelquefois, il est souhaitable de sélectionner dynamiquement la dépendance à utiliser. Dans ce cas de configure, on utilise Instance :
public class A { @Inject private Instance<B> b; }A l'exécution, on pourra interroger Instance avec un itérateur pour récupérer la dépendance qui nous convient le mieux :
for (B impl: this.b) System.out.println("Here is an implementation of B: " + b);Il est possible de filtrer une Instance pour obtenir une Instance plus précise avec la méthode select en précisant des annotations :
Annotation a = Named("second"); Instance<B> refinedInstance = this.b.select(a);La méthode get() de Instance fonctionne pour récupérer l'unique dépendance satisfaisant les conditions. S'il en reste plusieurs une exception AmbiguousResolutionException est levée.
Il est également possible d'annoter des méthodes ou constructeurs avec l'annotation @Inject. Dans ce cas, le constructeur ou la méthoed est appelée automatiquement à la création de la classe et le résolveur d'injection essaie de trouver une instance valable pour chaque paramètre.
Pattern Observer
Le mécanisme d'injection peut être utilisé pour faciliter la mise en place d'un pattern Observer.
Prenons pour exemple une application web générant un événement à chaque page consultée. On écrit la classe PageAccess :
public class PageAccess { public InetAddress visitorAddress; public int statusCode; // 200 for success, 404 for page not found... public String visitedPage; public PageAccess(InetAddress visitorAddress, int statusCode, String page) { this.visitorAddress = visitorAddress; this.statusCode = statusCode; this.visitedPage = visitedPage; } @Override public String toString() { return String.format("[%s] %s: %d", visitorAddress, visitedPage, statusCode); } }
On peut écrire une classe écoutant les événements de type PageAccess et les enregistrant dans un fichier journal :
@Stateless public class PageAccessLogger { private String journalLocation; private String encoding; private Writer journalWriter; @PostConstruct public void init() { journalWriter = new OutputStreamWriter(FileOutputStream(journalLocation, true), encoding)); // we append data into the journal } public void log(PageAccess pa) { journalWriter.write(pa.toString()); } public void listenToPageAccess(@Observes PageAccess pa) { log(pa); } }
PageAccessLogger est automatiquement instantié et sa méthode listenToPageAccess est appelée par le container lorsqu'un événement de type PageAccess est émis. Il nous reste maintenant à écrire l'observé émettant les événements (cela peut être un servlet) :
@WebServlet(value="/HelloWorld") public class HelloWorldServlet { @Inject Event<PageAccess> pageAccessEvent; public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { pageAccessEvent.fire(new PageAccess(request.getRemoteAddress(), request.getServletPath(), response.getStatus()); } }
Il sera toujours possible d'écrire de nouveaux producteurs de l'événement PageAccess (observés) ainsi que de nouveaux consommateurs de l'événements (observateurs), le container se chargeant de les mettre en relation.
Les portées des objets
Chaque objet géré par le container a une portée spécifique ; on dénombre les portées suivantes (qui sont indiquées par annotations) :
- @Dependent : il s'agit de la portée par défaut (choix le plus conservatif), pour chaque injection une nouvelle instance est créé
- @ApplicationScoped : chaque instance a une portée globale pour l'application (singleton)
- @RequestScoped : la portée est limitée à la durée d'une requête HTTP
- @SessionScoped : la portée est limitée à une session HTTP (requêtes avec le même client)
- @ConversationScoped : la portée est fixée à une conversation (sorte de morceau de session ; l'application détermine elle-même lorsqu'une conversion débute et finit)
Il est possible de forcer l'instanciation d'un nouveau bean (sans prendre en compte sa portée de définition) en utilisant l'annotation @New sur le site de l'injection.
Descripteur de déploiement
- Les injections utilisées (injection de beans, de ressources...) peuvent être décrites avec un fichier XML de description de déploiement
- D'une manière générale, il est possible de surcharger l'usage des annotations par des entrées dans le descripteur de déploiement
- Différents descripteurs peuvent être utilisées selon le contexte d'utilisation : test avec des instances Mock, production... On peut ainsi "surcharger" certaines annotations (pour changer par exemple un composant injecté et le remplacer par un composant de test)
- On utilise le fichier META-INF/ejb-jar.xml pour indiquer des informations de déploiement
Le nommage de bean
Afin de pouvoir être facilement récupéré, un bean peut être associé à un nom (chaîne de caractères ou objet implantant l'interface Name). On utilise l'API Java Naming and Directory Interface (JNDI) à cet effet. Pour réaliser cette tâche JNDI peut s'aider de greffons se connectant à des services de nommage (service provider) utilisant différents protocoles (LDAP, NIS, DNS, CORBA, système de fichier...). En pratique, pour le nommage des beans, les espaces de nommage suivants peuvent être utilisés pour un nommage plus ou moins relatif :
- java:global/application/module/bean : nommage absolu
- java:app/module/bean : nommage dans le contexte de l'application courante
- java:module/bean : nommage dans le contexte du module courant
Quelques informations sur les noms utilisés :
- application désigne le nom de l'application (nom de l'archive ear de deploiement ou nom défini dans le fichier de configuration application.xml)
- module désigne le nom du module (nom de l'archive jar, war ou nom défini dans le fichier de configuration ejb-jar.xml)
- bean désigne le nom du bean : il peut être précisé par l'annotation du bean ou un fichier de configuration ; par défaut ce nom est le nom qualifié de la classe (nom préfixé par sa hiérarchie de paquetages)
Il est possible de rajouter au nom un suffixe !interface : celui-ci est uniquement utile si le bean implante plusieurs interfaces différentes (avec des annotations divergentes telles que @local ou @remote). Cela permet d'indiquer quelle interface on souhaite utiliser pour manipuler le bean.
L'accès aux interfaces métiers
- Un bean session peut être associé à une interface métier simplement en l'implantant
- Historiquement (avant EJB 2.0) tous les beans utilisaient une interface de type Remote pour permettre par défaut la communication entre différentes JVM
- Avec EJB 2.0, les interfaces de type Local ont été introduites pour limiter le surcoût lié aux appels distants (sérialisation des données, communication réseau)...
- On annote une interface avec @Local pour un usage local ou @Remote pour un usage distant
- Un bean peut implanter une interface déclarée @Local et en même temps une interface @Remote
-
Le mode de passage de paramètres diffère entre interfaces locale et distante :
- En local, les paramètres sont passés par référence : la méthode appelante et la méthode appelée du bean partagent donc la même instance pour le paramètre ; cela est possible car l'appelant et l'appelé résident dans la même JVM
- En distant, les paramètres sont passés par valeur : l'appelé manipule donc une copie des paramètres ; toute modification réalisée sur ces paramètres ne sera pas visible par l'appelé ; il faut en outre faire attention à ne pas communiquer des paramètres qui soient trop volumineux (car le container les sérialise avant de les transmettre)
- Il est possible d'annoter soit l'interface, ainsi les codes suivants sont équivalents :
@Local public interface Business {} @Stateless public class BusinessBean implements Business {}
public interface Business {} @Stateless @Local(Business.class) public class BusinessBean implements Business {}
Message Driven Bean (MDB)
Présentation
- Un MDB n'est jamais accédé par l'intermédiaire d'une interface
- Un MDB peut exister en plusieurs instances toutes équivalentes du point de vue des clients (ce qui est comparable au beans session Stateless) ; un MDB peut donc traiter les messages de plusieurs clients
- Un MDB traite de façon asynchrone les messages que des clients lui envoient ; il peut être automatiquement instantié à la réception d'un nouveau message et détruit après un laps de temps de non-réception de messages (leur fonctionnement est ainsi analogue aux MessageReceiver d'Android)
- Le principal critère qui prévaut pour le choix entre un bean session Stateless et un MDB est la nécessité du synchronisme : si celui-ci est absolument nécessaire (attente d'une valeur de retour suite à l'appel d'une méthode), on optera pour un bean session. Sinon un MDB est plus performant
- On utilise une API d'acheminement de messages qui est généralement Java Messaging System (JMS)
Un exemple de MDB
Nous souhaitons écrire un bean recevant des notes à ajouter pour un utilisateur donné. JMS supporte différents types de messages :
- BytesMessage pour transmettre un message contenant des données binaires
- MapMessage pour transmettre des couples de clé/valeur (writeX(String key, X value) permet d'ajouter un couple clé-valeur, les méthodes getX() permettent d'obtenir une valeur associée à une clé)
- ObjectMessage permet de transmettre un objet serialisable (qui doit implanter l'interface Serializable ou Externalizable)
- StreamMessage contient un flot de valeurs primitives (int, long, float, double, String...) qui doivent être lues dans le même ordre qu'elles ont été écrites (on utilise les méthodes writeX(X value) pour écrire une valeur et X readX() pour la lire)
- TextMessage permet de transmettre un message contenant une chaîne de caractères (la méthode setText(String s) place la chaîne s dans le message et la méthode String getText() permet de la relire)
La note est un objet sérializable et le propriétaire de la note est une chaîne de caractères que nous pouvons placer dans une MapMessage.
Nous écrivons le MDB chargé de traiter les notes reçues :
@MessageDriven(activationConfig = { @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/NoteQueue"), @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") }) public class NoteReceiverBean implements MessageListener { @Resource private MessageDrivenContext mdc; @EJB private NoteBackend noteBackend; // the note backend is automatically injected by the container static final Logger logger = Logger.getLogger("NoteReceiverBean"); public NoteReceiverBean() { } @Override public void onMessage(Message receivedMessage) { try { if (received instanceof TextMessage) { noteBackend.addNote(receivedMessage.getObject("note"), receivedMessage.getString("owner")); } else { logger.log(Level.WARNING, "Message of wrong type: {0}", receivedMessage.getClass().getName()); } } catch (JMSException e) { logger.log(Level.SEVERE, "SimpleMessageBean.onMessage: JMSException: {0}", e.toString()); mdc.setRollbackOnly(); } } }
Ce bean peut être utilisé par un client qui souhaiterait ajouter une nouvelle note pour un utilisateur donné. Les messages doivent être créés par un MessageProducer obtenu avec une ConnectionFactory. Les notes sont envoyées dans une queue de messages dédiée nommée jms/NoteQueue.
@Resource(lookup = "java:comp/DefaultJMSConnectionFactory") private static ConnectionFactory connectionFactory; @Resource(lookup = "jms/NoteQueue") private static Queue queue;
Voici maintenant comment envoyer un message :
public void sendNote(Note note, String owner) { try (Connection connection = connectionFactory.createConnection();) { Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer messageProducer = session.createProducer(queue); Message message = session.createMapMessage(); message.setObject("note", noteToSend); message.setString("owner", noteOwner); messageProducer.send(message); } }
Timers
- L'annotation @Schedule placé sur une méthode (sans argument) d'un bean sert à planifier son exécution automatique à certains horaires définis (il est possible d'utiliser l'annotation @Schedules avec un tableau de @Schedule pour indiquer plusieurs horaires)
- La syntaxe des valeurs pour les propriétés des annotations s'inspire de l'utilitaire Unix de planification Cron : * sert de joker, minute="*/10" indique que la tâche est réalisée toutes les 10 minutes, hour="1-10" indique que toutes les heures entre 1 heure et 10 heure sont concernées...
- Les méthodes planifiées sont généralement utiles pour réaliser des tâches de maintenance (nettoyage, sauvegarde de base de données par exemple), de génération périodique de rapports...
Par exemple, une méthode devant être appelé tous les samedis matin à 4h15 et tous les 1ers jours du mois à 7h00 :
@Schedules ({ @Schedule(dayOfWeek="Sat", hour="4", minute="15"), @Schedule(dayOfMonth="1", hour="7", minute="0") }) public void doSomeAction() { }
Il est possible aussi de planifier des tâches programmatiquement en utilisant un TimerService :
... @Resource TimerService timerService; // the timerService is automatically injected public void planAction() { timerService.createSingleActionTimer(10000, new TimerAction()); // we trigger the @timeout annotated method of the TimerAction class in 10 seconds }
Nous devons aussi définir l'action à déclencher dans le délai indiqué avec une classe disposant d'une méthode avec l'annotation Timeout :
class TimerAction { @Timeout public void doSomeAction() { ... } }
Intercepteurs
- Les intercepteurs permettent de déclencher automatiquement l'exécution de code à l'entrée et/ou la sortie de méthodes d'un bean (programmation orienté aspect)
- Ils peuvent être utilisés pour ajouter des fonctionnalités escamotables de sécurité, de test en contrôlant les paramètres d'appel ou la valeur de retour, de journalisation d'actions, de profiling du code...
- Les intercepteurs sur une méthode constituent une chaîne de responsabilité dont le maillon final est l'appel de la méthode elle-même ; il est tout à fait possible qu'un intercepteur décide de ne pas faire appel au maillon suivant de la chaîne
- Un intercepteur peut se placer sur une méthode donnée ou alors globalement sur une classe (dans ce cas toutes les méthodes sont interceptées)
- Un intercepteur dispose d'une méthode annotée @AroundInvoke avec un paramètre InvocationContext ; cette méthode a le pouvoir de modifier les paramètres d'appel, de continuer ou non la propagation dans la chaîne et de retourner une valeur éventuellement modifiée
À titre d'exemple, écrivons un intercepteur loggant les appels de méthodes :
public class LoggingInterceptor { Logger logger = Logger.getLogger(LoggingInterceptor.class.getName()); @AroundInvoke public Object interceptAndLog(InvocationContext ctx) { Object result = ctx.proceed(); // call the method itself or the next interceptor on the chain String message = String.format("Method %s called on instance %s with parameters %s, returning result: %s, ctx.getMethod(), ctx.getTarget(), Arrays.toString(ctx.getParameters()), result); logger.info(message); return result; } }
On peut ensuite l'utiliser pour n'importe quel bean avec une annotation :
@Interceptors({LoggingInterceptor.class, AnotherInterceptor.class}) @Stateless public class MyBean { ... }
On aurait pu alternativement indiquer l'intercepteur dans le fichier de déploiement ejb-jar.xml :
<interceptor-binding> <target-name>myapp.MyBean</target-name> <interceptor-class>myapp.LoggingInterceptor.class</interceptor-class> <interceptor-class>myapp.AnotherInterceptor.class</interceptor-class> </interceptor-binding>
Il est également possible de définir des intercepteurs pour des timers avec l'annotation @AroundTimeout.
Transactions
Avec Java EE, les transactions sur des bases de données pour manipuler des données persistantes peuvent être traitées de deux manières :
- en laissant la responsabilité de gestion aux beans eux-mêmes : c'est le développeur qui doit manuellement décider de commencer une transaction puis la finir par un commit ou un rollback (on utilise l'annotation @TransactionManagement(TransactionManagementType.BEAN))
- en laissant le soin de gérer cette problématique au container (aucune annotation n'est nécessaire car il s'agit du comportement par défaut ; on peut toutefois indiquer explicitement @TransactionManagement(TransactionManagementType.CONTAINER)
Mode bean
Si le bean gère lui-même les transactions, on peut insérer une dépendance pour récupérer un gestionnaire de transactions et ensuite appeler ses méthodes begin() pour démarrer la transaction puis commit() ou rollback() pour l'annuler.
@Resource private UserTransaction transaction;
Mode container
Pour chaque méthode, il faut ajouter éventuellement une annotation @TransactionAttribute(...) pour indiquer son comportement vis-à-vis des transactions :
- @TransactionAttribute(TransactionAttributeType.MANDATORY) : il est nécessaire que la méthode appelante ait déjà créé une transaction active (sinon une exception EJBTransactionRequiredException est levée)
- @TransactionAttribute(TransactionAttributeType.REQUIRED) : il s'agit du comportement par défaut si aucune annotation n'est présente ; il consiste à créer une transaction s'il n'en existait pas déjà une (ou sinon utiliser celle existante) puis à clore le transaction à la sortie de la méthode
- @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) : cette annotation créé une nouvelle transaction dans tous les cas à l'entrée de la méthode
- @TransactionAttribute(TransactionAttributeType.SUPPORTS) : on inclut l'appel de la méthode dans une éventuelle transaction de la méthode appelante (mais la présence de transaction n'est pas obligatoire)
- @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) : l'appel n'est pas inclus dans une transaction (une éventuelle transaction est suspendue)``
- @TransactionAttribute(TransactionAttributeType.NEVER) : l'usage de transaction est banni ; en cas de transaction déjà ouverte, une exception est levée
Le comportement par défaut REQUIRED est généralement acceptable dans la plupart des cas.
Le commit est automatiquement réalisé en fin de transaction. Touefois si une exception système survient un rollback est effectué. On peut également réaliser soi-même une opération rollback dans la méthode du bean comme le montre l'exemple suivant :
@Stateless public class ExempleBean { @Resource private EJBContext context; public void someTrxnMethod() throws Exception { try { // persistence actions } catch(Throwable t) { context.setRollbackOnly(); // trigger the rollback } } }