| La programmation par aspect (AOP) avec .NET et J2EE par Thomas GIL (thomas.gil@valtech.fr) | ||
Programmation fonctionnelle, structurée, orientée objet... Ces dernières décennies ont été riches en rebondissements et en mutations des modes de conception et de programmation. Et pour être honnête, la dernière marche (vers l'Objet) s'inscrit dans la durée, tant le pas conceptuel à franchir est important.
Cela n'empêche pas les passionnés, les veilleurs technologiques, de se demander : "et après" ? Que reste-t-il à inventer, que peut-on encore améliorer dans notre mode de conception / programmation ? Eh bien pour nous, la prochaine grande mutation sera celle de la programmation orientée Aspect (AOP).
Cet article est tout d'abord une introduction aux idées et motivations qui ont mené à la programmation orientée aspect. Après avoir posé le contexte et introduit quelques définitions, nous pourrons nous enquérir des projets et des outils disponibles; cela passera, dans la tradition des comparatifs DotNetGuru, par un panorama sélectif des environnements C# / .NET et Java / J2EE.
A l'issue de ce dossier illustrés par de nombreux exemples, nous descendrons d'un niveau conceptuel pour nous intéresser aux outils élémentaires qui permettent de générer et d'analyser du code Java / C# . Ceci dans le double objectif de vous donner les moyens de mettre à profit ces outils sur vos propres projets, et, qui sait, peut-être de développer votre propre "tisseur d'aspects" (nous y reviendrons plus loin).
Tout est parti d'un constat simple : quel que soit le mode de développement que nous adoptions, nous ne parvenons jamais à ne pas écrire de code redondant. Vous pouvez être un développeur ou un concepteur hors pair, que ce soit en programmation procédurale, fonctionnelle, par contrainte, par prédicat, par règles, ou encore en programmation orientée objet, il existera toujours une situation dans laquelle il faudra écrire des choses répétitives pour gérer certains besoins récurrents.
Pour ne citer que des exemples tirés du monde Objet, demandons-nous comment nous pourrions gérer les besoins suivants sans écrire deux fois la même ligne de code :
L'objectif de la programmation orientée aspects est donc de nous fournir une technique apte à factoriser ce que les autres nous obligent à répéter à travers le code de nos projets.
L'exemple suivant, connu comme le loup blanc dans le monde de l'AOP Java, illustre bien le problème : dans le code du moteur de Servlets/JSP Tomcat, certains besoins techniques sont très localisés (lecture des fichiers de config, et donc parsing XML), d'autres sont répartis sur peu de classes (traitement des requêtes HTTP, et donc expressions régulières), mais il en est qui se retrouvent dans quasiment tout le code (appel au framework de log, pour diagnostiquer et journaliser les problèmes survenus en fonction de leur gravité, ainsi que pour rendre compte des requêtes traitées par le serveur).
|
Figure A : Répartition du code de gestion de certains besoins dans le serveur Web Tomcat |
Si par malheur (pour le projet Tomcat) le framework de log était amené à évoluer, il faudrait procéder à de nombreuses modifications, réparties sur la quasi-totalité du code. On peut imaginer deux solutions à cette situation de crise :
Non, car le problème que nous venons de soulever n'est pas nouveau. Il existait déjà lorsque nous développions en langage procédural; c'était même pire puisque nous n'avions même pas accès à l'héritage.
Il est donc tout à fait licite de songer à utiliser l'AOP en langage C, Cobol... Et certains l'ont fait : les équipes de développement du système d'exploitation libre FreeBSD; le système est bien entendu écrit en C, et l'AOP a été introduit pour gérer de manière uniforme le problème du chargement pro-actif des informations (pre-fetching). L'outil employé s'appelle bien entendu AspectC, il est lui-même en phase de ré-implémentation... par les équipes en question, emballées par les perspectives offertes et qui souhaitent enrichir l'outil afin d'implémenter d'autres aspect dans leur système.
Pour résumer, la programmation par aspects est une technique novatrice permettant de mettre en facteur certaines responsabilités dont la réalisation est a priori dispersée à travers un système, fût-il orienté objet.
A noter : un synonyme de AOP, que vous rencontrerez immanquablement lors de vos tribulations sur le Web : AOSD pour Aspect Oriented Software Developpement. Le site de référence dans ce domaine est d'ailleurs : http://aosd.net/. Et dès la première page, on retrouve une définition de l'AOP: Aspect-oriented software development is a new technology for separation of concerns (SOC) in software development. The techniques of AOSD make it possible to modularize crosscutting aspects of a system.
Il existe en réalité deux approches permettant d'implémenter le beau concept que représente l'AOP. La première consiste à dire qu'il faut absolument séparer le code des aspect du code sur lequel nous voulons les appliquer (appelé code de base). Un langage tiers permet d'établir les relations exactes qui existent entre le code de base et les aspects. Flexible et très puissante, cette approche est parfois jugée un peu trop "magique" : le développeur d'une classe particulière sera étonné de la voir se comporter différemment de ce qu'il y a véritablement écrit, et se posera donc sans cesse la question "quel aspect s'applique à ma classe ?", d'où la nécessité d'outils efficaces de gestion de la traçabilité. Cette orientation est suivie par les projets du type AspectJ, et comme ce projet est très moteur, elle a le vent en poupe.
L'autre option serait de dire qu'il faut tout d'abord développer les aspects, certes, mais qu'il est ensuite nécessaire de "marquer" le code en utilisant une construction syntaxique adaptée. L'avantage que procure cette technique est d'expliciter dans le code la relation avec les aspects : du coup, le développeur n'est pas sans cesse en train de se demander "dois-je implémenter cela, ou un aspect s'en chargera-t-il ?" : il voit les aspects auxquels il fait appel (ce qui n'ôte pas le besoin de comprendre leur sémantique). Cette seconde approche est suivie par Microsoft avec l'intégration en standard des Attributes .NET, et par certains outils de génération de code du type XDoclet en Java (mais est-ce toujours de l'AOP, me demanderez-vous... le débat fait déjà rage sur le Web).
Les sections suivantes vont s'efforcer de présenter ces deux techniques, en donnant des exemples d'implémentation dans les deux mondes, Java / J2EE et C# / .NET.
[Dans tout l'article, n'en déplaise à la loi Toubon, nous conserverons le mot anglais Attribute pour ne pas porter à confusion avec la notion d'attribut de classe ou d'instance, ces deux concepts n'ayant rien à voir.]
La quasi-totalité des développeurs .NET, quel que soit le langage choisi, a déjà manipulé les Attributes .NET : il s'agit de ces petits marqueurs que l'on place en en-tête d'une déclaration (de méthode, de classe, d'attribut, de namespace...) et qui permet de donner à cette déclaration une sémantique enrichie. L'exemple le plus simple est celui de la compilation conditionnelle : il suffit de préfixer la déclaration d'une méthode par l'Attribute [Conditional("VAR_DE_PRECOMPIL")] pour que le compilateur rende conditionnel l'appel à cette méthode, en fonction de la présence de la VAR_DE_PRECOMPIL. Un petit exemple pour nous rafraîchir la mémoire :
#define DEBUG
namespace AOP {
using
System;
using
System.Diagnostics;
public
class TestConditional {
[Conditional("DEBUG")]
public
static void
AfficherTrace(string msg)
{
Console.WriteLine(msg);
}
public
static void
Main()
{
AfficherTrace("Entrée dans la méthode Main()");
}
}
}
TestConditional.cs
Nous avons fait apparaître la définition de la variable DEBUG dans le code pour plus la clarté de l'exemple, mais bien entendu, c'est au niveau des propriétés du projet (ou des options de ligne de commande du compilateur) que nous le ferions sur un véritable projet. De cette manière, on pourrait choisir de manière globale le mode de compilation du projet et n'exécuter, en mode RELEASE, que le code qui soit absolument nécessaire.
On peut dire que ceci constitue notre premier Aspect : un comportement récurrent et cohérent (exécuter ou ne pas exécuter une méthode) est ainsi appliqué à l'ensemble des méthodes repérées par l'Attribute [Conditional].
Remarque : pour ceux qui découvrent l'Attribute [Conditional] il va de soi que les méthodes pouvant ainsi être marquées doivent renvoyer void, et n'avoir aucun effet de bord; sans cela, leur compilation conditionnelle aurait un impact sur le déroulement nominal du programme.
Enrichir la sémantique des éléments de syntaxe .NET est séduisant; il serait regrettable que cette faculté soit réservée au framework .NET lui-même ! Rassurez-vous, ce n'est pas le cas : nous pouvons tous développer nos propres Attributes, et les associer à tout type d'éléments du langage (quel que soit le langage .NET). Comment faire ? C'est très simple : un Attribute est ... une classe qui hérite de System.Attribute.
Par exemple, imaginons que nous souhaitions "marquer" certaines classes d'un modèle objet comme étant persistantes en base de données. Commençons par définir l'Attribute PersistanceAutomatique :
namespace AOP {
[System.AttributeUsage(
System.AttributeTargets.Class)]
public
class PersistanceAutomatiqueAttribute :
System.Attribute{
private bool
lazyLoading;
private string
nomMapping;
public
PersistanceAutomatiqueAttribute (string
nomMapping){this.nomMapping = nomMapping;}
public
string NomMapping{get{return
nomMapping;}}
public bool
LazyLoading{get{ return
lazyLoading;}set{ lazyLoading = value;}}
}
}
PersistanceAutomatiqueAttribute.cs
La règle veut que nous suffixions ce genre de classe par le mot Attribute de manière à ne pas confondre les classes d'Attributes et les classes "normales". Et les compilateurs .NET s'attendent eux aussi à ce que vos Attributes soient nommés comme cela.
D'autre part, vous aurez noté que la classe PersistanceAutomatiqueAttribute est elle-même marquée par un Attribute standard, AttributeUsage. Cela permet au compilateur de vérifier que notre Attribute ne sera utilisé que sur les éléments de langage pour lesquels il a été prévu; dans notre exemple, nous avons limité l'applicabilité de PersistanceAutomatiqueAttribute aux classes. Il sera donc illégal (et le compilateur nous le fera savoir) de l'appliquer à une méthode, une structure, ou un namespace.
Bien. Notre Attribute étant développé, appliquons-le à une classe métier (un CaddieVirtuel par exemple, pour ne pas perturber nos habitudes) :
namespace AOP {
[PersistanceAutomatique("CaddieVirtuel",
LazyLoading = true )]
public
class CaddieVirtuel
{
// ...}
}
CaddieVirtuel.cs
La syntaxe entre crochets ressemble étrangement à l'appel du constructeur de notre PersistanceAutomatiqueAttribute. C'est effectivement ce qui se passe, du moins pour le premier paramètre. Le second, vous l'aurez deviné, correspond à l'invocation du setter de la propriété LazyLoading.
Deux questions viennent dès lors à l'esprit :
Où sont stockées ces informations, que l'on passe en paramètre aux Attributes ?
A quel moment est invoqué le constructeur de notre Attribute ?
Répondre à la première question est trivial, il suffit de visualiser le contenu de l'Assembly portant la définition de notre classe CaddieVirtuel avec l'utilitaire ILDASM.exe :
|
|
La valeur tronquée, sur la droite de la figure B, correspond à la chaîne "CaddieVirtuel" placée entre crochets dans le code C#. Les informations passées en paramètre aux Attributes sont donc bien stockées dans l'Assembly qui fait appel à ces Attributes.
Et pour savoir à quel instant sera réellement invoqué le constructeur de PersistanceAutomatiqueAttribute, nous vous proposons d'ajouter un simple System.Console.WriteLine("Dans le constructeur de PersistanceAutomatiqueAttribute");.
Puis nous allons faire deux essais : le premier se contente d'instancier la classe CaddieVirtuel :
namespace AOP {
public
class GestionnairePersistance{
public static
void Main(string[]
args)
{
CaddieVirtuel cad = new
CaddieVirtuel();
}
}
}
GestionnairePersistance.cs
Résultat ? Rien, la console n'a absolument rien affiché. Le constructeur de notre PersistanceAutomatiqueAttribute n'a donc pas été appelé.
Deuxième essai : tentons de lire les méta-données de la classe CaddieVirtuel. Il suffit pour cela d'avoir recours à la réflexion .NET :
namespace AOP {
public class
GestionnairePersistance{
public
static void
Main(string[] args) {
CaddieVirtuel cad = new CaddieVirtuel();
foreach (System.Attribute a in
cad.GetType().GetCustomAttributes(true))
{
if (a is
PersistanceAutomatiqueAttribute){
PersistanceAutomatiqueAttribute paa = a as
PersistanceAutomatiqueAttribute;
System.Console.WriteLine(paa.NomMapping);
System.Console.WriteLine(paa.LazyLoading);
}
}
}
}
}
GestionnairePersistance.cs
Cette fois, le résultat est plus parlant :
|
|
Le constructeur d'un Attribute personnalisé est donc invoqué lors de la lecture de celui-ci.
Conclusion : les Attributes permettent de repérer des emplacements particuliers dans le code .NET et d'associer à ce repérage des méta-données (stockées dans l'Assembly). Mais le comportement par défaut d'un Attribute est ... de ne rien faire jusqu'au moment où l'on s'intéresse à lui. Ce mécanisme peut donc être utile (il permet aux classes utilisant la réflexion de prendre connaissance des méta-données) mais tout à fait insuffisant pour implémenter de véritables Aspects : en effet, comment installer du code avant et après l'invocation de méthodes sur une classe ? Comment ajouter une méthode à une classe ? Cela semble impossible... En tous cas, il nous manque un élément pour y parvenir.
Souvenez-vous des articles publiés par Sami Jaber concernant le framework .NET Remoting. Il était question de clients et de serveurs, de proxy et de skeletton, de channels, de messages et de MessageSink. Nous allons nous appuyer sur une partie de cette infrastructure pour placer un intercepteur entre notre objet métier et son client.
Sans entrer dans les détails (que vous trouverez en suivant ce lien), il nous suffit de comprendre que :
lorsqu'on invoque une méthode sur un objet "normal", l'invocation est directe, sans intermédiaire (et du coup très efficace, mais inadaptée à l'installation d'Aspects)
lorsqu'on invoque une méthode sur une objet héritant de System.ContextBoundObject, l'invocation est "réifiée en objet" (ou "marshallée"), c'est-à-dire qu'elle est transformée en un objet de type IMessage (ce marshalling est opéré par un couple d'objets du framework .NET : le proxy transparent et le proxy réél); ce message passe ensuite par un ou plusieurs MessageSink qui peuvent décider de déclencher du code avant le transfert du message au suivant, ou après avoir reçu le message de retour. Dans un langage de Patterns, nous dirions que les MessageSink forment une chaîne de responsabilités. Dans cette chaîne, chacun peut déclencher du code lors de l'interception des messages, avant de les transférer au suivant dans la chaîne, jusqu'à atteindre l'objet cible lui-même. Mais on peut également imaginer d'empêcher la propagation d'un message de manière à interdire l'invocation d'une méthode à certaines classes, ou encore à tout client qui n'aurait pas initialisé correctement un certain contexte de sécurité. Le schéma suivant tente de récapituler cette séquence d'actions de manière plus graphique :
|
|
Il suffirait donc d'installer un MessageSink personnalisé en amont de chaque objet métier de manière à pouvoir déclencher du code avant, après l'invocation de méthodes (ou même d'accès aux attributs des objets en question). Cette technique est très puissante... et nous vous proposons de vous la faire toucher du doigt sur un exemple simple : un intercepteur qui va compter le nombre d'accès (invocation de méthodes, lecture / écriture d'attributs) à une classe cible.
Commençons par développer une classe qui implémente l'interface IMessageSink : ce sera notre intercepteur, c'est lui qui appliquera notre aspect avant ou après l'invocation de méthodes ou l'accès aux attributs des objets cibles.
namespace AOP {
using System;
using
System.Runtime.Remoting.Activation;
using
System.Runtime.Remoting.Contexts;
using
System.Runtime.Remoting.Messaging;
// Notre intercepteur
public class
DNGAspectCompteur : IMessageSink{
private
int nbHits;
private
IMessageSink suivant;
public
DNGAspectCompteur(IMessageSink suivant){
this.suivant = suivant;
}
public
IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink msgSink) {
throw new
Exception("Pas implémenté");
}
public
IMessageSink NextSink {
get{ throw
new Exception("Pas implémenté");
}
}
public
IMessage SyncProcessMessage(IMessage msg) {
IMessage resultat = null;
nbHits++;
// On
pourrait placer du code avant invocationresultat =
suivant.SyncProcessMessage(msg);
// On
pourrait placer du code après invocationreturn resultat;
}
~DNGAspectCompteur() {
Console.WriteLine ("Nombre
total d'accès à l'objet : {0}", nbHits);
}
}
}
DNGAspectCompteur.cs
L'objet cible, quant à lui, n'a pas de complexité particulière à ceci près qu'il doit hériter de ContextBoundObject:
namespace AOP {
using
System;
[DNGCompteur]
public
class DNGObservable : ContextBoundObject{
public
int i = 5;
public
void Test(){
Console.WriteLine("dans
test");
}
public
static void
Main(){
DNGObservable o = new
DNGObservable();
o.i = 4;
o.Test();
o.i = 5;
}
}
}
DNGObservable.cs
Et comme vous l'avez deviné, c'est par l'application de l'Attribute [DNGCompteur] que l'on associe l'intercepteur DNGAspectCompteur à la classe DNGObservable. Justement, c'est la partie la plus épineuse; l'Attribute DNGCompteur est un peu particulier : il doit être déclenché automatiquement (pas question ici d'attendre que quelqu'un vienne lire l'Attribute par introspection !), ce qui est possible à deux conditions :
L'Attribute doit hériter de ContextAttribute au lieu de Attribute
L'objet visé doit hériter de ContextBoundObject, sans quoi notre Attribute ne serait pas sollicité automatiquement
Sans vous faire languir davantage, voici le code nécessaire à l'association de l'Attribute [DNGCompteur] à l'intercepteur DNGAspectCompteur :
namespace AOP {
using
System;
using
System.Runtime.Remoting.Activation;
using
System.Runtime.Remoting.Contexts;
using
System.Runtime.Remoting.Messaging;
//
Factory d'intercepteurs
public
class DNGAspectCompteurProperty :
IContextProperty, IContributeObjectSink{
public IMessageSink GetObjectSink
(System.MarshalByRefObject o, IMessageSink next) {
return new
DNGAspectCompteur(next);
}
public void
Freeze(Context ctx){}
public bool
IsNewContextOK(Context ctx){return true;}
public string
Name{get{return
"dngproperty";}}
}
// Installe la factory d'intercepteurs
dans l'infrastructure d'interception .NET
[AttributeUsage(AttributeTargets.Class)]
public
class DNGCompteurAttribute :
ContextAttribute {
public DNGCompteurAttribute() : base("dngcontext"){}
public override
void GetPropertiesForNewContext(IConstructionCallMessage
ccm) {
ccm.ContextProperties.Add(new
DNGAspectCompteurProperty ());
}
}
}
DNGObservable.cs
Pour mieux comprendre ce qui précède, mettons-nous à la place du CLR, et partons de la méthode Main:
Lors de l'instanciation de la classe métier DNGObservable, le CLR initialise un contexte dédié du fait que cet objet hérite de ContextBoundObject;
Il parcourt immédiatement la liste des Attributes de la classe DNGObservable, à la recherche de ceux qui héritent de ContextAttribute.
Il trouve notre [DNGCompteur], instancie la classe DNGCompteurAttribute associée, et invoque la méthode GetPropertiesForNewContext sur cette instance
La méthode GetPropertiesForNewContext instancie à son tour une propriété de type DNGAspectCompteurProperty et l'associe au contexte de l'objet DNGObservable. Le rôle de cette propriété sera celui de fabrique d'intercepteurs : c'est à cet objet que l'infrastructure .NET demandera d'instancier notre MessageSink.
Une fois ces initialisations faites, nous nous retrouvons dans la méthode Main :
L'instruction o.i = 4; invite l'infrastructure .NET (proxy transparent et proxy réel) à invoquer la méthode GetObjectSink de la propriété précédemment associée au contexte de o. Cette méthode instancie enfin notre intercepteur DNGAspectCompteur, et le place dans la chaîne de responsabilité des MessageSink. Toute la tuyauterie est dorénavant en place.
L'instruction o.i = 4; est traitée par la chaîne suivante : proxy transparent, proxy réel, DNGAspectCompteur, puis l'objet cible. Plus précisément, c'est la méthode SyncProcessMessage de DNGAspectCompteur qui sera invoquée dans cette chaîne, et c'est donc le bon endroit pour incrémenter le nombre de "hits" sur l'objet o.
Les instructions suivantes o.Test(); et o.i = 5; accèdent à l'objet cible via la même chaîne de responsabilité, et incrémentent donc elles aussi le nombre de hits
Avant de terminer notre application, le CLR invoque l'ensemble des finaliseurs, dont ~DNGAspectCompteur, dans lequel nous en avons profité pour imprimer le nombre de hits total sur l'objet. Sans surprise, le résultat est 3 dans notre exemple.
Par souci de simplicité , nous n'avons pas osé mentionner la possibilité d'invoquer les méthodes de l'objet cible de manière asynchrone... mais comme vous avez pu le lire dans le code, réagir à ces invocations ne serait pas différent de ce que nous avons présenté ci-dessus.
Ce que l'on appelle Aspect (et nous reviendrons précisément sur cette terminologie dans la présentation de AspectJ ci-dessous) correspond à l'application d'un ensemble de Conseils (enrichissements de code) à un ensemble d'éléments du code de base (typiquement à un ensemble de classes).
Adopter la technique des ContextAttributes pour installer les intercepteurs MessageSink permet de répondre à une partie du problème : l'affectation (dans le code) des aspects aux éléments de syntaxe à enrichir. Mais il faut encore à faire le lien entre les MessageSink et le code supplémentaire à déclencher avant ou après les accès aux objets cibles (les Conseils).
Sur ce point, plusieurs idées viennent à l'esprit :
Soit on développe les Conseils sous forme de MessageSink, ce qui n'est pas trop contraignant car il suffit de redéfinir la méthode SyncProcessMessage. Mais cette approche est assez fermée : pour ajouter de nouvelles fonctionnalités aux conseils, il nous faudra modifier le code du MessageSink, le recompiler, et relancer l'application pour qu'il soit rechargé.
Soit le MessageSink n'est finalement qu'un contrôleur, qui ira chercher dans un fichier de configuration (XML, bien entendu) le nom de la (ou des) classe associée à un Aspect particulier; il suffit ensuite de se mettre d'accord sur le nom de la méthode à déclencher sur les classes en question, et d'imposer cette règle en obligeant les classes à implémenter une interface commune. Cette approche est séduisante car elle permet d'ajouter des Conseils sans modifier le code du MessageSink contrôleur, et elle permet également de procéder à ces ajouts à chaud, en cours de fonctionnement de nos applications.
C'est de cette manière que procède l'outil CLAW, dont le développement est malheureusement interrompu. Vous trouverez les raisons de cet arrêt prématuré sur le weblog du développeur de l'outil, John Lam.
L'utilisation d'Attributes et de MessageSink est assez simple à mettre en oeuvre, séduisante, et très intégrée au framework .NET. Mais elle pose tout de même quelques problèmes, dont trois très sérieux :
l'insertion d'intermédiaires capables d'intercepter chaque invocation de méthode peut sembler lourde. En effet, les performances globales de l'application vont s'en ressentir. Pour se faire une idée, c'est un peu la manière de faire de la plupart des serveurs d'application EJB dans le monde Java / J2EE... Souple, robuste, évolutif, mais pas forcément très efficace. Mais ce paramètre n'est pas nécessairement primordial pour toutes les applications.
le second problème est plus gênant : les objets sur lesquels nous souhaitons installer un MessageSink doivent hériter de ContextBoundObject. Or cette classe hérite de ... MarshalByRefObject. Ce qui signifie que dans une application distribuée par .NET Remoting, tous les objets sur lesquels nous aurons apposé un Aspect par ce biais seront passés par référence, et non par valeur. Cela a un impact non négligeable sur l'architecture de ladite application, ainsi que sur ses performances globales.
le dernier souci que nous rencontrons est lié au précédent : nos objets métier doivent hériter de ContextBoundObject. Or .NET impose un héritage simple de classes... Nous voilà donc bloqués ? Pas tout à fait : pour limiter le problème, il faudrait que tous les objets de plus haut niveau dans la hiérarchie d'héritage des objets métier soient des enfants de ContextBoundObject. Mais conceptuellement, c'est très dérangeant car cela introduit une adhérence du modèle des objets métier vis-à-vis d'une infrastructure technique ! Un véritable sacrilège !
Moralité, en l'état, cette technique est envisageable
pour des applications "standalone"
pour associer des Aspects aux objets passés par référence dans une application .NET Remoting
à condition d'accepter la pollution du modèle objet par l'héritage de la classe ContextBoundObject
à supposer que l'ajout de [nombreux] intermédiaires ne dégrade pas trop les performances des l'applications visées
A notre avis, ce n'est donc, malheureusement, pas la meilleure technique pour implémenter l'AOP en .NET. Nous avons toutefois tenu à vous l'expliquer en détail à la fois par curiosité, et pour que vous perceviez bien les raisons de notre préférence pour la philosophie des outils du type AspectJ, présentée plus loin.
Rassurez-vous, l'objectif ici n'est certainement pas de faire une présentation de l'outil bien connu du monde Java et permettant de générer du code et des descripteurs de déploiement relatifs aux EJB, Servlets et JSP (entre autres). Pour cela, nous vous renvoyons sur le site officiel de XDoclet.
Non, nous voulions simplement attirer votre attention sur le fait que l'approche par Doclet en Java est voisine de l'utilisation d'Attributes en .NET, du moins dans la syntaxe. Sur la page d'accueil de XDoclet, on nous parle d'ailleurs de programmation orientée Attribute...
Pour ceux qui n'auraient pas vu de code Java depuis longtemps, souvenez-vous : il est possible de placer, en en-tête des déclaration de packages, classes, méthodes et attributs des commentaires un peu spéciaux appelés "commentaires JavaDoc". Comme leur nom l'indique, ces commentaires étaient initialement destinés à la génération automatique de documentation (au format HTML). Mais rien n'empêche de s'appuyer sur le même mécanisme pour générer d'autres types de fichiers, dont du code; il suffit pour cela de :
définir de nouveaux mots-clé (qui seront repérés par le préfixe @) à placer dans les commentaires JavaDoc
développer une classe Java (une Doclet) capable d'interpréter ces nouveaux mots-clé et de produire quelque chose d'utile
Le reste, c'est-à-dire le parcours du code Java à la recherche des commentaires JavaDoc, est complètement automatique grâce à l'outil éponyme : javadoc.exe. Et c'est une option de ligne de commande qui lui indique quelle Doclet utiliser pour faire l'interprétation des commentaires spéciaux.
Non outillé, le développement d'Enterprise JavaBeans est lourd et fastidieux. Il faut en effet produire de nombreux fichiers (classes Java et fichiers de configuration XML), à travers lesquelles on trouve beaucoup de redondances :
la classe du Bean lui-même (l'implémentation de l'objet métier)
l'interface métier distante du Bean (décrivant l'ensemble des méthodes et accesseurs invocables par RMI)
l'interface métier locale du Bean (décrivant l'ensemble des méthodes et accesseurs invocables au sein même du serveur d'applications)
l'interface locale de la fabrique du Bean (la Home dans la terminologie EJB)
l'interface distante de la fabrique du Bean
le descripteur de déploiement standard ejb-jar.xml
et souvent, un descripteur de déploiement propriétaire (jboss.xml, weblogic-jar.xml...)
la (ou les) classe DTO (Data Transfer Object, jadis appelée Value Object) qui permettra de passer des informations par valeur entre le serveur d'EJB et ses clients
L'époque héroïque où il fallait produire ces fichiers manuellement est définitivement révolue. Aujourd'hui, la majorité des développeurs d'EJB utilisent soit un outil de génération de code basé sur UML (Together Control Center par exemple), soit la XDoclet. Nous ne pouvons pas résister au plaisir de vous montrer un petit exemple de classe Java enrichie des tags JavaDoc nécessaire à la génération de tous les fichiers connexes; cet exemple est extrait d'un petit projet de gestion de cours dans une société de formation... :
package
beans;
import java.util.Collection;
import javax.ejb.*;
import javax.naming.InitialContext;
import util.XHome;
/**
* @ejb.bean type="CMP" view-type="local"
primkey-field = "id"
* schema="Course" name="Course" local-jndi-name="beans/Course"
* @ejb.finder signature = "java.util.Collection findAll()"
* @ejb.finder signature = "beans.CourseLocal findByCode(java.lang.String
code)"
* query = "SELECT OBJECT(c) FROM Course c WHERE c.code = ?1"
* @ejb.pk class = "java.lang.Integer"
*/
public abstract class
CourseBean implements EntityBean {/**
* @ejb.pk-field
* @ejb.persistent-field
* @ejb.interface-method
*/
public
abstract Integer getId();
public
abstract void
setId(Integer id);
/**
* @ejb.persistent-field
* @ejb.interface-method */
public
abstract String getCode();
/** @ejb.interface-method
*/
public
abstract void
setCode(String code);
/**
* @ejb.persistent-field
* @ejb.interface-method */
public
abstract int
getDuration();
/** @ejb.interface-method
*/
public
abstract void
setDuration(int duration);
/**
* @ejb.persistent-field
* @ejb.interface-method */
public
abstract String getDurationUnit();
/** @ejb.interface-method
*/
public
abstract void
setDurationUnit(String durationUnit);
/**
* @ejb.persistent-field
* @ejb.interface-method */
public
abstract String getLabRatio();
/** @ejb.interface-method
*/
public
abstract void
setLabRatio(String labRatio);
/**
* @ejb.persistent-field
* @ejb.interface-method */
public
abstract String getLanguage();
/** @ejb.interface-method
*/
public
abstract void
setLanguage(String language);
/**
* @ejb.persistent-field
* @ejb.interface-method */
public
abstract String getTitle();
/** @ejb.interface-method
*/
public
abstract void
setTitle(String title);
/**
@ejb.relation name="Course-Objectives"
* role-name="Course-Has-Objectives"
* target-ejb="Objective"
* @ejb.interface-method */
public
abstract Collection getObjectives();
/** @ejb.interface-method
*/
public
abstract void
setObjectives(Collection objectives);
/**
@ejb.create-method */
public
Integer ejbCreate() throws CreateException {
setId(XHome.getNewPK("Course"));
return
null;
}
public void ejbPostCreate()
{}
public void
setEntityContext(EntityContext context) {}
public void
unsetEntityContext() {}
public void
ejbRemove() {}
public void
ejbLoad() {}
public void
ejbStore() {}
public void
ejbActivate() {}
public void
ejbPassivate() {}
}
CourseBean.cs
Vous nous direz : "quel est le rapport avec la programmation orientée Aspects ?" A priori pas grand chose. Mais à y regarder de plus prêt, le principe est assez proche de l'utilisation des Attributes et des intercepteurs .NET :
On repère dans le code les méthodes et les classes à "enrichir"
L'interception des invocations et la réalisation de l'enrichissement est fait par les classes générées par la XDoclet, qui s'insèrent dans l'infrastructure d'interception des EJB : les interfaces générées transforment notre simple classe en EJB, et les descripteurs de déploiement recueillent toute la configuration technique (dans notre exemple, la requête de recherche d'un cours par son code, ainsi que la définition d'une relation entre le cours et ses objectifs pédagogiques, qui sont représentés par un autre EJB).
L'idée était à notre avis suffisamment voisine pour mériter d'être mentionnée dans un article concernant l'AOP.
Changeons de démarche : après avoir vu comment utiliser les Attributes ou les commentaires JavaDoc pour décorer du code en vue de l'enrichir grâce à des intercepteurs intelligents, nous allons nous intéresser à AspectJ dont la philosophie prône la séparation complète entre le code "de base" et les aspects.
AspectJ est un projet assez ambitieux, qui vise à définir et appliquer des aspects très précis sur des classes Java. Ce projet a longtemps existé de manière autonome, mais sont son support est aujourd'hui assuré par la communauté eclipse.org. C'est donc sur ce site que l'on pourra télécharger les dernières versions de AspectJ, ainsi qu'un plug-in permettant de manipuler cet outil depuis Eclipse.
AspectJ n'est pas le seul produit d'AOP disponible dans le monde Java, comme vous pouvez le voir sur ce recensement d'outils. Toutefois, c'est certainement le plus avancé, et celui qui remporte le plus grand nombre de suffrages; ses définitions sont bien acceptées par les acteurs du domaine de l'AOP, et sont reprises par les autres outils du même acabit (que nous verrons plus tard, dans le monde .NET). Aussi nous proposons-nous de vous offrir un petit glossaire :
| Terme anglais | Terme français retenu par DNG |
Définition |
| Advice | Conseil | Fragment de code (Java pour AspectJ) qui est voué à être inséré dans les classes du code "de base". Techniquement parlant, on peut associer un conseil à un ensemble des points d'une zone d'interception (cf définitions suivantes). |
| Joinpoint | Evénement Java | Identification d'un endroit dans le "code de base" où l'on pourra insérer des Conseils. En fait d'endroit, il vaut mieux se représenter les Joinpoints comme des moments, ou des événements particuliers; par exemple, le moment où un constructeur ou une méthode est appelé, un attribut accédé... |
| Pointcut | Points d'interception | Un point d'interception est un
enrichissement des événements Java : il permet par exemple
d'identifier l'invocation d'une méthode particulière, sur l'instance
d'une classe bien précise... AspectJ permet d'utiliser des caractères génériques (* et ..) pour identifier un ensemble de points d'interception de manière concise. Il est également possible de définir des "pointcuts utilisateur", c'est-à-dire de libeller un regroupement de points d'interception de manière à simplifier l'application d'un conseil à plusieurs endroits du code de base. |
| Aspect | Aspect | Un aspect est une unité de regroupement de :
Chaque aspect est stocké dans un fichier éponyme (d'extension .java), et se place dans un package standard. Cela ressemble donc étrangement à une classe, à ceci près que le mot clé est, bien entendu, aspect{}. |
| Weaver | Tisseur | Compilateur permettant d'appliquer les aspects au "code de base" |
Le principe de fonctionnement d'AspectJ est très simple : quasiment tout se passe à la compilation du projet. L'ordre de compilation est le suivant :
Le tisseur AspectJ recherche l'ensemble des définitions d'aspects à travers le projet, et les compile. Plus précisément, il pré-compile chaque aspect en une classe Java standard, qui est à son tour compilée en bytecode.
Si tous les aspects sont exempts d'erreur de compilation, ils sont appliqués aux classes de base. En pratique, cela revient à identifier tous les points d'interception dans le code source des classes ciblées, et à y insérer le code des conseils associés. A noter que l'on peut tout à fait associer plusieurs conseils à un même point d'interception. L'insertion de code ne se fait pas dans le source lui-même, heureusement, mais sur une copie temporaire. Il n'y a donc pas de pollution du code par les aspects ni de problème de suppression d'un aspect précédemment appliqué.
Une fois le code Java modifié, il est compilé en bytecode comme d'habitude.
Au runtime, on exécute donc le bytecode, compilé après application des aspects. A priori, rien n'est spécifique à AspectJ dans le code que nous exécutons et il est donc inutile de placer quoi que ce soit sur le CLASSPATH. En pratique, lorsque nous développerons des Conseils un peu subtils (qui ont recours à l'introspection pour savoir sur quel élément de syntaxe ils agissent), il faudra tout de même ajouter une toute petite bibliothèque, aspectjrt.jar, qui ne mesure que 29 Ko.
On peut tout à fait utiliser le tisseur AspectJ en ligne de commande, et développer les aspects avec n'importe quel éditeur de texte. Ce sera la technique préférée des afficionados de Vi, (X)Emacs, UltraEdit ou NEdit.
Autre technique : AspectJ est livré avec un navigateur graphique d'aspects, dont voici une petite copie d'écran (ne faites pas attention au code de l'aspect visualisé, nous allons y revenir par la suite).
|
|
Mais le plus simple est certainement de se reposer sur le Plug-In AspectJ développé spécifiquement pour Eclipse. Celui-ci permet de :
créer un projet AspectJ
ajouter un ensemble d'aspects au projet
générer un fichier de configuration (.lst) qui référence l'ensemble des aspects que le tisseur devra prendre en compte
et lancer la compilation par un simple clic droit sur le fichier .lst.
A l'usage, nous avons trouvé ce plug-in tout à fait stable, et très bien intégré aux convention de l'IDE Eclipse. Par exemple, une erreur de compilation d'un aspect apparaît sous forme d'une petite bulle rouge dans la marge et dans la liste des tâches, exactement comme une erreur de compilation Java traditionnelle.
Pour illustrer cette présentation d'AspectJ, nous vous proposons d'écrire l'aspect qui comptera le nombre d'accès à une instance de classe Java, comme nous l'avions fait dans la section Attributes et intercepteurs .NET.
Commençons par créer un aspect nommé DNGCompteur. Ajoutons à cet aspect un point d'interception nommé (pointcut) qui identifie l'ensemble des endroits où nous souhaitons déclencher du code.
package
org.dng.aop.aspects;
public aspect DNGCompteur {
pointcut invocationMethodes():
call(* org.dng.aop..*.*(..)) && !within(DNGCompteur);
before(): invocationMethodes() {
System.out.println("Avant l'acces
a une methode");
}
}
}
DNGCompteur.java
La traduction littérale de cet aspect est la suivante : juste avant (before()) chaque appel d'une méthode (call()) sur une instance de classe se trouvant dans le package org.dng.aop ou n'importe lequel de ses sous-packages (org.dng.aop..*.*), à l'exception d'invocations qui se produiraient dans l'aspect DNGComteur lui-même (!within(DNGCompteur)), nous voulons invoquer le Conseil System.out.println("Avant l'acces a une methode");.
Nous avons placé la contrainte !within(DNGCompteur) pour éviter toute invocation de méthode récursive...
L'encart suivant nous montre le code "de base" sur lequel nous allons appliquer l'aspect précédent :
package org.dng.aop.base;
public class Observable {
private int
valeur;
public
void test(){
System.out.println("Dans
la methode test de Observable");
}
public
int getValeur() {
return
valeur;
}
public
void setValeur(int
valeur) {
this.valeur
= valeur;
}
}
/******/
public class Test {
public
static void
main(String[] args) {
Observable
obs = new Observable();
obs.test();
System.out.println(
obs.getValeur() );
}
}
Observable.java
et Test.java
|
|
Le problème de notre Conseil est qu'il est incapable de nous dire quelle méthode va être appelée. Pour cela, il doit faire appel à une introspection un peu spéciale, qui passe par un objet "magique" fourni par AspectJ : la représentation du JoinPoint.
Nous modifions notre aspect :
package
org.dng.aop.aspects;
public aspect DNGCompteur {
pointcut invocationMethodes():
call(*
org.dng.aop..*.*(..)) && !within(DNGCompteur);
before():
invocationMethodes() {
System.out.println("Avant
l'acces a une methode\n\t" + thisJoinPoint);
}
}
}
DNGCompteur.java
Ce qui donne :
|
|
De la même manière, nous pourrions placer un Conseil sur l'accès aux attributs. Vous connaissez le principe :
package
org.dng.aop.aspects;
public aspect DNGCompteur {
pointcut
sollicitationQuelconque():
(call(* org.dng.aop..*.*(..)) ||
get(* org.dng.aop..*))
&& !within(DNGCompteur);
before(): sollicitationQuelconque() {
System.out.println ("Avant
l'acces a : \n\t " + thisJoinPoint);
}
}
}
DNGCompteur.java
|
|
Pour le moment, nous n'avons fait que réagir à l'invocation de méthodes et à l'accès aux attributs. Comment enrichir notre exemple pour comptabiliser le nombre d'accès opérés sur un objet particulier (ici l'objet Observable) ?
Vous me direz : "s'il y a un objet qui doit porter cette responsabilité, c'est bien l'objet Observable lui-même". Tout à fait d'accord. Mais il n'a pas été prévu pour cela... Qu'à cela ne tienne : nous allons utiliser une nouvelle facette de AspectJ, appelée l'Introduction, et qui va nous permettre d'ajouter un nouvel attribut à la classe Observable, ainsi qu'un getter pour lire et afficher la valeur de cet attribut en fin de programme.
package
org.dng.aop.aspects;
public aspect DNGCompteur {
private
int org.dng.aop.base.Observable.compteurHits;
public
int org.dng.aop.base.Observable.getCompteurHits(){
return compteurHits;
}
pointcut
sollicitationQuelconque():
(call(* org.dng.aop..*.*(..)) ||
get(*
org.dng.aop..*))
&& !within(DNGCompteur);
before(): sollicitationQuelconque() {
System.out.println ("Avant
l'acces a : \n\t " + thisJoinPoint);
}
}
}
DNGCompteur.java
Par pure curiosité, nous avons voulu jeter un oeil aux types de modifications opérées dans le code de la classe Observable. Pour cela, il suffit de la décompiler par jad. Le résultat parle de lui-même :
// Decompiled by Jad v1.5.7f. Copyright
2000 Pavel Kouznetsov.
// Source File Name: Observable.java
package org.dng.aop.base;
import java.io.PrintStream;
import org.aspectj.runtime.reflect.Factory;
import org.dng.aop.aspects.DNGCompteur;
public
class Observable{
public
Observable(){
DNGCompteur.ajc$interFieldInit$org_dng_aop_aspects_DNGCompteur$org_dng_aop_base_Observable$compteurHits(this);
}
public
void test(){
System.out.println("Dans
la methode test de Observable");
}
public
int getValeur(){
Observable observable = this;
Object
aobj[];
org.aspectj.lang.JoinPoint
joinpoint = Factory.makeJP(ajc$tjp_0, this,
observable, aobj = new Object[0]);
DNGCompteur.ajc$perSingletonInstance.ajc$before$org_dng_aop_aspects_DNGCompteur$154(joinpoint);
return
observable.valeur;
}
public
void setValeur(int
arg0){
valeur
= arg0;
}
public
int getCompteurHits(){
}
private
int valeur;
public
int ajc$interField$org_dng_aop_aspects_DNGCompteur$compteurHits;
public
static final
org.aspectj.lang.JoinPoint.StaticPart ajc$tjp_0;
static {
Factory
factory = new Factory("Observable.java",
Class.forName("org.dng.aop.base.Observable"));
ajc$tjp_0
= factory.makeSJP("field-get", factory.makeFieldSig("2-valeur-org.dng.aop.base.Observable-int-"),
11);
}
}
Observable.jad,
décompilée après application de l'aspect DNGCompteur
Il ne nous reste plus qu'à utiliser ce nouvel attribut inséré dans la classe Observable, et à l'incrémenter d'une unité dans notre Conseil. Il faut pour cela déclarer l'instance de la classe Observable sur laquelle nous voulons avoir accès à l'attribut compteurHits dans le pointcut, sans quoi cet élément serait inaccessible au conseil associé :
package
org.dng.aop.aspects;
public aspect DNGCompteur {
private
int<