EJB 3.0 et AOP : l'industrialisation des projets Java EE

Auteur : Thomas GIL (thomas.gil@dotnetguru.org)

http://www.dotnetguru.fr

EJB 3.0 et AOP : l'industrialisation des projets Java EE

Thème :

Version 1.0

Table des matières

Introduction

La plate-forme Java EE offre plusieurs techniques pour développer une couche de services et d'accès aux données. La plus standard, entendez par là la plus indépendante des infrastructures d'hébergement (également appelées "conteneurs") est sans conteste les Enterprise Java Beans (EJB). Malheureusement, le développement d'EJB a longtemps été fastidieux, nécessitant même des outils de génération automatique de code basés sur des modèles UML ou sur du code Java agrémenté de commentaires spécifiques (cf. XDoclet).

L'avènement de la spécification EJB 3.0 a redistribué les cartes. En bref, cette nouvelle mouture élimine de nombreuses redondances tant au niveau du code des composants qu'au niveau de leur configuration, l'essentiel pouvant maintenant s'exprimer au sein du code source par le biais d'annotations (ou "méta-données"). Même le code client a été simplifié au passage, rendant caduque l'utilisation de la notion de Home une Abstract Factory de composants qui était implémentée par le conteneur jusqu'à la version EJB 2.1.

Même si cette spécification est encore incomplète, elle a le mérite de rendre le développement de composants enfin efficace et accessible à tout développeur Java. Dans cet article, nous partirons donc du principe qu'elle constitue une initiative constructive et élégante.

Toutefois, le développement par annotation n'est pas une technique très novatrice : nous avons déjà 5 ans de recul grâce à la plate-forme .NET qui offre ce type d'outil depuis sa première version. S'il est parfaitement adapté au paramétrage fin, on a pu s'apercevoir qu'il ne convient pas parfaitement aux paramétrages systématiques puisqu'il implique une redondance importante des annotations au niveau du code source.

Le but de cet article est de simplifier encore davantage le développement de composants EJB 3.0, d'éliminer 100% des redondances résiduelles dans le code des composants et dans leurs annotations. Pour atteindre cet objectif, nous nous appuierons sur les instruments offerts par la programmation orientée Objet, et quand cela ne suffit pas, sur ceux fournis par la programmation orientée Aspect.

Développement d'EJB 3.0: mode d'emploi

L'objet n'est pas ici de décrire en détail le développement de composants EJB mais de nous donner les principes et syntaxes de base qui, comme souvent, répondent à 70 / 80% des besoins. Par exemple, la plate-forme Java EE nous propose trois types de composants : EJB Session, EJB Entité et EJB Orienté Message (MDB), mais nous nous contenterons de décrire les deux premiers.

EJB Session, un composant orienté "Service"

Qu'ils soient invoqués localement (leurs clients étant situés dans la même machine virtuelle) ou à distance, les EJB Session représentent des services fonctionnels. Ils constituent les Contrôleurs des Cas d'Utilisation (sortes de Façades fonctionnelles) d'un projet, même si bien sûr un EJB Session représentant un service de très haut niveau peut reposer sur d'autres composants (d'autres EJB Session ou simplement d'autres classes Java locales) pour implémenter ses responsabilités.

Le diagramme suivant expose une architecture hybride dans laquelle les EJB Session sont invoqués :

  • soit par un client lourd : Java Swing/SWT si la connectivité s'établit via RMI, ou .NET Windows Forms/VB/Delphi/C++ si l'on invoque les EJB Session via les WebServices.
  • soit par un serveur de présentation Web (1.0 ou 2.0). Là aussi, selon la technologie du serveur Web, on peut utiliser plusieurs middleware : il peut s'agir à nouveau de RMI ou des WebServices, ou encore de technologies alternatives (Corba, Ice, Burlap, Hessian, Rest...) peut-être moins populaires et dont l'intégration demandera à coup sûr plus de travail.

Architecture EJB Session

Voilà pour le principe architectural. Prenons maintenant un exemple concret : un utilitaire de gestion de signets (bookmarks). Dans cette application, chaque utilisateur a besoin de créer des signets pour mémoriser ses sites préférés; il a également besoin d'organiser les signets par catégorie. Bien sûr, après création, il souhaitera également faire des recherches parmi ses signets, soit en parcourant les catégories, soit par mot-clé apparaissant dans le titre d'un signet.

Nous ne détaillerons pas cette application, mais à vue de nez, on peut tout de même identifier au moins deux cas d'utilisation :

  • La gestion des signets et des catégories (création, suppression, modification, déplacement des signets dans les catégories)
  • Le moteur de recherche
  • La gestion des utilisateurs

Chacun de ces cas d'utilisation sera géré par un EJB Session différent; il représente un service fonctionnel de haut niveau qui sera sollicité par les utilisateurs. De plus, nous pouvons organiser notre application de telle sorte que chaque service puisse répondre aux utilisateurs sans avoir besoin de conserver leur contexte applicatif. Si cette hypothèse est avérée, il ne sera plus nécessaire d'avoir un exemplaire de composant par utilisateur : quelques instances de chaque type de composant répondront quasi-simultanément à tous les utilisateurs qui le sollicitent et ces instances pourront être recyclées pour les traiter les requêtes ultérieures. Dans la terminologie des EJB, on qualifie ce type de composant "sans mémoire" de EJB Session Stateless (par opposition à "composant à mémoire" ou EJB Session Stateful ). Une architecture technique basée essentiellement sur les Stateless aura nettement moins de problèmes de montée en charge et de répartition de charge.

Après avoir déterminé quels composants constitueraient notre application, passons à l'implémentation de l'un d'entre eux : le moteur de recherche. Voici son interface :

package service;



import java.util.List;

import business.Signet;



public interface MoteurRecherche {

    List<Signet> rechercherTousSignets();

    String rechercherURL(Signet s);

    List<Signet> rechercherSignets(String categorie);

    List<Signet> rechercherSignetsParTitre(String partieTitre);

    List<Signet> rechercherSignetsParURL(String partieURL);

}

Son implémentation fonctionnelle ne pose donc aucun problème :

package service.implementation;



import java.util.List;

import business.Signet;

import business.Categorie;



public class MoteurRechercheBean implements Catalog {

    public List<Signet> rechercherTousSignets(){

        // ...

    }

    public String rechercherURL(Signet s){

        // ...

    }

   

    public List<Signet> rechercherSignets(String categorie){

        // ...

    }

   

    public List<Signet> rechercherSignetsParTitre(String partieTitre){

        // ...

    }

   

    public List<Signet> rechercherSignetsParURL(String partieURL){

        // ...

    }



}

La bonne nouvelle avec les EJB 3.0, c'est que pour rendre ce composant à la fois distribué (via RMI et via WebServices) et transactionnel (car il se connectera directement ou indirectement à une base de données), il suffit de spécifier nos besoins techniques sous forme d'annotations complémentaires dans le code du Bean :

package service.implementation;



import java.util.List;

import javax.ejb.Remote;

import javax.ejb.Stateless;

import javax.ejb.TransactionAttribute;

import javax.ejb.TransactionAttributeType;

import javax.jws.WebMethod;

import javax.jws.WebService;

import javax.jws.soap.SOAPBinding;

import javax.persistence.EntityManager;

import javax.persistence.PersistenceContext;

import business.Signet;

import business.Categorie;



@Remote



@Stateless // Sous entendu : session stateless



@SOAPBinding

    (style = SOAPBinding.Style.DOCUMENT,

    use = SOAPBinding.Use.LITERAL,

    parameterStyle = SOAPBinding.ParameterStyle.WRAPPED)

   

@WebService

    (name = "EndpointInterface",

    targetNamespace = "http://dotnetguru.org/GestionSignets",

    serviceName = "MoteurRecherche")

   

public class MoteurRechercheBean implements Catalog {

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherTousSignets(){

        // ...

    }



    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public String rechercherURL(Signet s){

        // ...

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherSignets(String categorie){

        // ...

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherSignetsParTitre(String partieTitre){

        // ...

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherSignetsParURL(String partieURL){

        // ...

    }



}

Ce dernier code mérite quelques explications:

  • L'annotation @Remote exprime le fait que notre composant est invocable par RMI
  • @Stateless permettra au conteneur de composants d'optimiser le recyclage des instances de composants, comme indiqué précédemment
  • @SoapBinding et @WebService gèrent le paramétrage protocolaire et l'encodage XML à employer lorsque le composant est sollicité par SOAP
  • @WebMethod permet d'indiquer quelles méthodes peuvent être invoquées à distance par le protocole SOAP. Dans notre exemple, nous avons exposé toutes les méthodes; l'interface WSDL sera déduite de ce code par le conteneur d'EJB et reflétera ce choix. N.B: Dans la pratique, on n'expose pas systématiquement les méthodes de services dont les types sont complexes et qui nécessitent une adaptation; dans ce cas, il est possible d'ajouter des méthodes dédiées aux invocations via WebServices mais qui prennent en paramètre ou renvoient des types simplifiés (tableaux, objets restreints, types primitifs). Ces méthodes procèdent à l'adaptation de type et invoquent à leur tour les méthodes naturelles du service.
  • Enfin @TransactionAttribute permet de régler finement la démarcation transactionnelle des composants : quelle méthode nécessite/requiert/ne supporte pas d'être invoquée dans une transaction. Cette exigence sera interprétée par le conteneur de composants comme la nécessité d'initier une nouvelle transaction, d'enrôler le service dans une transaction existante ou au contraire de lever une exception (ou de suspendre la transaction) si une transaction est en cours mais que le service ne peut pas s'y intégrer sans risque.
Supposons que les autres services soient déclarés puis implémentés de façon similaire, et passons tout de suite à la conception et au paramétrage des objets métier et de leur lien avec une base de donnée relationnelle.

EJB Entité et JPA, le mapping Objet/Relationnel

Tous les objets métiers, aussi appelés "objets du domaine", ne sont bien entendu pas persistants (comprenez par là "à enregistrer dans un système de stockage, tel qu'une base de données par exemple"). Mais dans notre application, il se trouve que c'est le cas : les Utilisateurs, leurs Signets et les Catégories qui les structurent sont autant d'informations à conserver de manière durable. Nous choisissons ici de les stocker dans une base de données relationnelle (qui peut être HypersonicSQL, PostgreSQL, MySQL, Oracle, SQL Server, etc...)

Il existe de nombreuses techniques pour établir une connexion entre nos services (EJB Session) et une base de données relationnelle :

  • Utiliser JDBC directement. En général, on ne le fait pas dans le code d'implémentation de l'EJB Session lui-même, qui est censé se concentrer sur le code fonctionnel, mais on délègue l'accès aux données à une couche dédiée, la couche de Persistance. Il peut s'agir d'un simple package supplémentaire dans lequel plusieurs classes se chargent d'envoyer les requêtes à la base de données, de lire les résultats et si besoin de transformer ces résultats en objets structurés. On qualifie souvent ces objets de DAO (Data Access Object).
  • Déclencher depuis les EJB Session une bibliothèque technique appelée "framework de persistance" telle que Hibernate, IBatis, une implémentation de JDO, etc...
  • Enfin, les EJB Session peuvent s'appuyer sur des EJB Entité. Comme les EJB Session, il s'agit de classes particulières déployées et gérées par un conteneur de composants. Ce dernier permet de paramétrer la manière dont chaque EJB Entité sera mappé sur la structure relationnelle d'une base de données (dans une table ou plusieurs); il permet également de faire des choix parmi plusieurs stratégies de mapping des relations, qu'il s'agisse de l'héritage ou des associations entre composants.

Grosso-modo, les deux dernières techniques d'accès à la base sont identiques. Leurs avantages comparés : l'avant-dernière permet de choisir et de maîtriser 100% du paramétrage du framework de persistance alors que les EJB Entité offrent une garantie d'indépendance et donc de portabilité vis-à-vis du conteneur (et donc du framework de persistance sous-jacent), ainsi qu'une meilleure intégration dans le contexte du serveur d'application (démarcation transactionnelle automatique et intégrée à celle des EJB Session, idem pour la sécurité...).

Dans cet article, nous allons tâcher de ne reposer que sur la plate-forme Java EE standard donc optons pour les EJB Entité. Pour être plus précis, les dernières évolutions des spécifications EJB ont clairement séparé des autres parties ce qui s'appelait hier encore encore EJB Entité. Dorénavant, nous parlerons de "composants Java Persistants", suivant le nom de la spécification JPA (Java Persistence API). Ce détail est important car il signifie que les composants persistants sont utilisables non seulement dans le contexte d'un conteneur de composants EJB mais qu'ils peuvent également être exploités dans un simple conteneur de Servlets, dans une application console ou graphique côté client (une application autonome intégrant une petite base de données par exemple), etc... Il suffit d'intégrer un gestionnaire de persistance compatible JPA (en clair : quelques bibliothèques packagées dans des fichiers JAR) pour bénéficier du support de ces composants.

Voyons maintenant comment implémenter nos composants persistants. Dans l'application de gestion des signets, nous en avions identifié 3 : Utilisateur, Signet et Catégorie. Voici les relations qui les unissent :

Modèle métier

Et à titre d'exemple, voici le code de la classe Signet (les deux autres sont similaires). Notez l'utilisation des annotations pour exprimer l'essentiel du mapping Objet/Relationnel :

package business;



import java.io.Serializable;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.Id;

import javax.persistence.ManyToOne;



@Entity

public class Signet implements Serializable {   

    @Id

    @GeneratedValue

    private long id;

    public long getId {

        return id;

    }

    public void setId(long value){

        id = value;

    }

   

    private String titre;

    public String getTitre {

        return titre;

    }

    public void setTitre(String value){

        titre = value;

    }

   

    private String url;

    public String getUrl {

        return url;

    }

    public void setUrl(String value){

        url = value;

    }

   

    @ManyToOne

    private Categorie categorie;

    public Categorie getCategorie {

        return categorie;

    }

    public void setCategorie(Categorie value){

        categorie = value;

    }

}

Vous l'aurez probablement deviné :

  • @Entity spécifie que l'infrastructure d'accès aux données doit rendre cette classe persistante
  • @Id identifie l'attribut qui sera mappé sur la clé primaire (ici, une clé primaire technique)
  • @GeneratedValue délègue au framework la génération et l'affection d'une clé primaire lors du premier enregistrement d'un objet en base. La génération de la clé se fait généralement par le biais d'une séquence en base, d'un attribut entier auto-incrémenté, ou encore par la production d'un GUID.
  • @ManyToOne exprime une partie de l'association entre Catégorie et Signet qui est de type 1-n. Dans la classe Catégorie, on trouve logiquement un autre attribut de type List<Signet> qualifié de @OneToMany.
Pour ceux qui ont développé des composants EJB Entité avec les versions précédentes de la spécification (2.1 ou antérieures) et qui découvrent cette nouvelle syntaxe, on peut dire que les choses se sont nettement simplifiées. Bien sûr on sent une très nette inspiration des frameworks de persistance tels que Hibernate dans cette nouvelle spécification JPA. Ma foi, ce n'est pas une mauvaise idée de s'inspirer de ce qui fonctionne et qui séduit les développeurs.

Bien sûr, en tant que tel, le paramétrage de la persistance ne suffit pas. Il nous faut également voir comment les EJB Session interagissent avec le gestionnaire de persistance pour sauvegarder ou restaurer des composants persistants vis-à-vis de la base de données. Reprenons le moteur de recherche, mais cette fois, examinons le code complet :

package service.implementation;



import java.util.List;

import javax.ejb.Remote;

import javax.ejb.Stateless;

import javax.ejb.TransactionAttribute;

import javax.ejb.TransactionAttributeType;

import javax.jws.WebMethod;

import javax.jws.WebService;

import javax.jws.soap.SOAPBinding;

import javax.persistence.EntityManager;

import javax.persistence.PersistenceContext;

import business.Signet;

import business.Categorie;



@Remote



@Stateless // Sous entendu : session stateless



@SOAPBinding

    (style = SOAPBinding.Style.DOCUMENT,

    use = SOAPBinding.Use.LITERAL,

    parameterStyle = SOAPBinding.ParameterStyle.WRAPPED)

   

@WebService

    (name = "EndpointInterface",

    targetNamespace = "http://dotnetguru.org/GestionSignets",

    serviceName = "MoteurRecherche")

   

public class MoteurRechercheBean implements Catalog {

    @PersistenceContext

    private EntityManager mgr;



    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherTousSignets(){

        return mgr

            .createQuery("select s from Signet s")

            .getResultList();

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public String rechercherURL(Signet s){

        return mgr

            .createQuery("select s.url from Signet s where s.id = :id")

            .setParameter("id", s.getId())

            .getUniqueResult();

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherSignets(String categorie){

        return mgr

            .createQuery("select c.signets from Category c where c.nom = :categorie")

            .setParameter("categorie", categorie)

            .getResultList();

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    @WebMethod

    public List<Signet> rechercherSignetsParTitre(String partieTitre){

        return mgr

            .createQuery("select s from Signet s where s.titre like :partieTitre")

            .setParameter("partieTitre", partieTitre)

            .getResultList();

    }

   

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)

    public List<Signet> rechercherSignetsParURL(String partieURL){

        return mgr

            .createQuery("select s from Signet s where s.url like :partieURL")

            .setParameter("partieURL", partieURL)

            .getResultList();

    }



}

A la lecture du code, on se rend compte de deux choses :

  • Il suffit de déclarer un attribut de type EntityManager (annoté @PersistenceContext) pour pouvoir interagir avec le gestionnaire de persistance. L'annotation permet au conteneur de composant d'injecter automatiquement la dépendance (la référence) vers le bon objet sans que nous ayons à nous en soucier.
  • Le langage utilisé n'est pas tout à fait du SQL mais un langage de plus haut niveau appelé Java Persistance Query Language. L'avantage notable est qu'il nous évite toute dépendance vis-à-vis des dialectes SQL des multiples bases relationnelles, tout en offrant une syntaxe cohérente pour interroger des graphes d'objets pouvant tirer parti de relations d'héritage complexes. L'inconvénient découle du premier avantage : le développeur a moins de maîtrise du code SQL qui sera finalement envoyé à la base de données, ce qui peut avoir un impact sur les performances, la robustesse et plus tard la maintenance de l'application.

Tentative d'industrialisation

Cette brève introduction aux EJB 3.0 et à JPA montre combien il est devenu simple de construire des applications distribuées (par plusieurs protocoles), transactionnelles, persistantes... tout en restant standard et portable non seulement sur plusieurs systèmes d'exploitation (c'est la force de Java) mais aussi sur n'importe quel conteneur d'EJB 3.0 (c'est la force de Java EE). Il faut avouer que la programmation par méta-données a redistribué les cartes.

Malgré toutes ces simplifications, nous pouvons nous poser les questions habituelles : que donneraient ces technologies si elles étaient utilisées sur un projet important (disons quelques centaines de composants persistants, quelques dizaines de services)? L'utilisation des annotations élimine-t-elle toutes les redondances de code? Est-il aisé de changer d'avis, par exemple sur la technique de génération de clés primaires, et de propager les modifications sur l'ensemble des composants? Bref, en une phrase : l'approche par annotation est-elle suffisante ou peut-on mieux faire?

Tout d'abord, nous pouvons considérer les clés primaires en base représentées par les identifiants uniques dans nos classes. Si l'on fait un choix technique et cohérent, il s'agit de déclarer un attribut private int id; sur toutes les classes persistantes, puis d'y apposer les annotations @Id et éventuellement @GeneratedValue comme ici. Cette redondance doit bien pouvoir être mise en facteur! Par exemple, si nous créions une classe abstraite PersistentObject, elle pourrait porter cet attribut @Id private int id.

Malheureusement, ou plutôt heureusement, la notion d'héritage entre objets persistants est supportée par JPA, ce qui signifie qu'en cas d'héritage, l'union des attributs d'une classe et de toutes ses parentes doit être enregistré pour chaque objet. Cela peut se faire dans une table unique (une table par graphe d'héritage) ou dans une table par classe persistante par exemple. Mais dans ce dernier cas, il devient évident que l'identifiant de la classe mère doit être différent de celui déclaré dans la classe fille sans quoi il serait impossible d'exprimer des corrélations et des jointures entre les tables qui mappent un graphe d'héritage. Ainsi, nous sommes bel et bien contraints de déclarer un attribut id identique mais différent dans chaque classe persistante.

Un raisonnement similaire nous amènerait conclure qu'il faut également déclarer chaque classe persistante en la préfixant de @Entity. De même, si les objets métier jouent le rôle de DTO (Data Transfer Object), c'est-à-dire si nos EJB Session les renvoient par valeur à des clients distants, il faut également les déclarer Serializable, en les faisant implémenter l'interface idoine. Le langage Java aurait pu nous éviter cette contrainte si la classe mère de toutes les autres, Object, avait été elle-même sérializable. Mais ce n'est pas le cas, donc il nous faut l'indiquer sur chaque classe (excepté sur les classes dérivant d'une autre classe déjà sérialisable, ce qui sera assez rare dans un modèle d'objets métier).

On sent bien que la programmation orientée Objet enrichie des annotations est insuffisante pour généraliser un choix d'implémentation sur un ensemble de composants. Qu'à cela ne tienne, tentons notre chance du côté des Aspects

AOP en renfort des EJB 3.0

Dans cette section, nous utiliserons la syntaxe proposée par le tisseur AspectJ pour exprimer nos aspects. Nous supposerons également que le lecteur est au fait des principaux mécanismes de la Programmation Orientée Aspect. Si ce n'est pas le cas, consultez la rubrique AOP de DotNetGuru.org ou (si vous avez un peu de temps devant vous) lisez l'ebook que nous avons mis en ligne sur le sujet à l'adresse suivante : DNG-Book-ConceptionOrienteeAspects-Gil-2006.pdf.

L'utilisation d'un tisseur d'aspects statique est assez logique pour pallier les limitations énoncées dans la section précédente. Nous allons donc créer un aspect qui aura les responsabilités suivantes :

  • Rendre toutes les classes du package "metier" persistantes en leur adjoignant une annotation @Entity.
  • Greffer un attribut private int id sur chacune de ces mêmes classes
  • Annoter ces attributs de @Id et de @GeneratedValue
  • Enfin, pour que les EJB Session puissent interagir facilement avec le gestionnaire de persistance, nous ferons en sorte que la simple déclaration d'un attribut de type EntityManager suffise. C'est du luxe, mais cela permettra aux composants de choisir le nom de cet attribut sans même être obligé de le préfixer de @PersistenceContext (tout oubli devient donc impossible). Bien sûr, si un EJB Session n'a pas besoin d'accéder au gestionnaire de persistance, il ne déclarera pas d'attribut; dans ce cas, notre aspect n'aura tout simplement aucun effet (cela ne posera aucun problème de compilation ni de tissage).

Voyons comment implémenter ces exigences avec la syntaxe AspectJ :

package aspects;



import java.io.Serializable;

import javax.persistence.GeneratedValue;

import javax.persistence.Id;

import javax.persistence.Entity;

import javax.persistence.EntityManager;



@SuppressWarnings("serial")

public aspect Persistence {

    interface Persistent extends Serializable{}

    declare parents : metier.* implements Persistent;

   

    @Id

    @GeneratedValue

    private int Persistent.id;

    public int Persistent.getId {

        return id;

    }

    public void Persistent.setId(int value){

        id = value;

    }

   

    declare @type : metier.* : @Entity;



    declare @field : EntityManager *.* : @javax.persistence.PersistenceContext;

}

Pour les moins familiers de cette syntaxe biscornue, voici un décodage de l'exemple précédent : AspectJ ne permet pas d'introduire (ajouter, greffer) un attribut sur un ensemble de classes. C'est une limitation syntaxique et non conceptuelle : les déclarations de greffe doivent avoir la forme de déclarations d'attributs ou de méthode, au nom près. Par exemple ici, nous greffons l'attribut id grâce à : private int Persistent.id, ce qui signifie littéralement qu'on ajoute l'attribut id au type Persistent.

Nous utilisons donc un moyen détourné, en deux temps, qui consiste à marquer tous les types "cible" (sur lesquels nous souhaitons greffer l'attribut id) par une interface, ici Persistent. Concrètement, declare parents : metier.* implements Persistent spécifie que tous les types du package "metier" implémenteront le type Persistant (le tissage modifie donc les relations d'implémentation d'interface). Notez qu'au passage nous en avons profité pour faire hériter Persistent de Serializable. Dans un second temps, AspectJ nous permet de greffer l'attribut id à l'interface Persistent... ce qui n'a pas vraiment de sens; la signification réelle est pourtant assez proche : nous greffons l'attribut id à toutes les classes qui implémentent l'interface Persistent directement, ce sont donc bien celles que nous avons marquées dans un premier temps. L'approche est la même pour greffer les accesseurs getId() et setId() aux mêmes classes.

Les deux dernières syntaxes sont des greffes d'attributs :

  • declare @type : metier.* : @Entity; enrichit toutes les classes du package metier de l'annotation @Entity
  • declare @field : EntityManager *.* : @PersistenceContext; est similaire, mais opère sur tous les attributs de toutes les classes (*.*) à condition qu'ils soient de type EntityManager, et leur greffe l'annotation @PersistenceContext.

Maintenant que nous avons acquiert la technique, nous pouvons la généraliser et simplifier le développement de composants EJB Session à condition de faire quelques hypothèses :

  • Tous les composants situés dans le package "service.implementation" sont des EJB Session, locaux ou distants (les exposer sur les deux modes en même temps ne poserait aucun problème)
  • Toutes les méthodes publiques peuvent également être invoquées via WebServices (ce choix est un brin simpliste comme nous l'avons dit précédemment)
  • Il existe une règle de nommage forte sur le projet:
    • les méthodes nommées "rechercherXXX" supportent les transactions mais n'en ont pas expressément besoin
    • les autres méthodes requièrent une transaction (à nouveau, ce choix est à affiner selon les projets. Nous vous donnons ici le principe général).
package aspects;



import javax.ejb.Remote;

import javax.ejb.Stateless;



public aspect GenericService {

    // Remoting

    declare @type : service.impl.* : @Stateless;

    declare @type : service.impl.* : @Remote;



    // WebServices

    declare @type : service.impl.* : @SOAPBinding

        (style = SOAPBinding.Style.DOCUMENT,

        use = SOAPBinding.Use.LITERAL,

        parameterStyle = SOAPBinding.ParameterStyle.WRAPPED)   

    declare @type : service.impl.* : @WebService

        (name = "EndpointInterface",

        targetNamespace = "http://dotnetguru.org/GestionSignets")

       

    declare @method : public * service.impl.*.*(..) :

        @WebMethod;



    // Transaction management

    declare @method : public * service.impl.*.rechercher*(..) :

        @TransactionAttribute(TransactionAttributeType.SUPPORTS);

    declare @method : public * service.impl.*.*(..)

                                    && ! public * service.impl.*.rechercher*(..):

        @TransactionAttribute(TransactionAttributeType.REQUIRES);

}

Conclusion

Dans cet article, nous avons vu que les spécifications EJB 3.0 et JPA simplifient énormément le développement de composants d'entreprise (distribués, transactionnels, persistants) par rapport aux spécifications précédentes. Elles s'appuient pour cela sur les nouveautés du langage Java 5.0, en particulier sur les annotations. Mais si la simplification est flagrante sur un composant, il reste malgré tout une redondance importante dans les annotations elles-mêmes lorsqu'on s'attaque à de gros projets.

Heureusement, la programmation par aspects permet de dépasser ces limitations par le biais d'un tissage statique. Le prix à payer est d'avoir une structure d'application extrêmement lisible (organisation des composants et des couches par packages) ainsi que des règles de nommage très fines (en particulier pour distinguer les méthodes nécessitant un contexte transactionnel). En acceptant de payer ce prix, on peut compléter les EJB 3.0, JPA par l'AOP et simplifier d'un cran supplémentaire le développement d'applications complexes sur la plate-forme Java EE.

Notez que la démarche suivie dans cet article est appliquable à tous les contextes dans lesquels vous serez amenés à rencontrer des redondances dans la spécification d'annotations. La programmation par annotation est un outil de spécification fin, il permet de repérer directement dans le code les choses qui sortent du cas général; mais ce n'est pas la technique la plus adaptée pour opérer des réglages ensemblistes. Pour cela, le mieux serait que les spécifications permettent une configuration globale dotée d'un langage ensembliste (comme ici : "toutes les classes du package XXX sont Persistantes et utilisent un attribut @Id id"). Mais à défaut, nous venons de voir que la Programmation Orientée Aspect s'avère être le complément idéal à la programmation par annotation car elle apporte à la programmation Objet un langage de composition ensembliste, transverse et cohérent. Et pour ne rien gâcher, puisqu'il n'existe aucune adhérence forte entre les classes du projet et les aspects, il n'est pas nécessaire pour les développeurs des couches métier et service d'apprendre à programmer en Aspects : au contraire, il leur suffit de respecter les règles de nommage sur lesquelles reposent les aspects.

N.B. Nous ne mettons pas de projet "prêt à l'emploi" à disposition avec cet article, pour éviter toute confusion : les Aspects que nous avons imaginés ici sont didactiques plus que complets ou robustes. Sur un projet d'envergure, ils sont à adapter voire à éviter dans certaines situations (en particulier celui rendant toutes les méthodes des EJB Session invoquables via les WebServices). Par contre, nous vous invitons à vous inspirer des exemples de code de cet article pour concevoir vos propres aspects. Bons tissages!

Auteur :Thomas GIL