La programmation par aspect (AOP) avec .NET et J2EE  par Thomas GIL (thomas.gil@valtech.fr)

Introduction

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).

Motivations et définition de la programmation orientée aspect (AOP)

A quoi aspire l'AOP ?

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.

Le type de problème visé par l'AOP

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 :

L'AOP se cantonne-t-elle aux langages orientés Objet ?

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.

Définition de l'AOP

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.

Implémentations concrètes de l'AOP

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.

Attributes .NET et intercepteurs

Exemple d'Attribute standard

[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.

Définition d'Attributes personnalisés

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 :

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 :

Figure B : Contenu d'une assembly dont les classes sont marquées par des Attributes

 

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 :

Figure C : Resultat obtenu par réflexion sur une classe marquée par PersistanceAutomatiqueAttribute

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.

Intercepteurs .NET

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 :

 

Figure C : Infrastructure d'interception .NET

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 :

  1. L'Attribute doit hériter de ContextAttribute au lieu de Attribute

  2. 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:

Une fois ces initialisations faites, nous nous retrouvons dans la méthode Main :

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.

Attributes et intercepteurs = Aspect ?

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 :

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.

Notre point de vue

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 :

Moralité, en l'état, cette technique est envisageable

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.

XDoclet Java

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...

Rappel sur le fonctionnement des Doclets

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 :

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.

Exemple de développement d'EJB avec XDoclet

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 :

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 :

L'idée était à notre avis suffisamment voisine pour mériter d'être mentionnée dans un article concernant l'AOP.

AspectJ

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.

Introduction

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 :
  • définitions de points d'interceptions nommés
  • associations de points d'interceptions (nommés ou anonymes) à des conseils
  • introduction d'attributs ou de méthodes dans les classes cibles (du code "de base")

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 :

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.

Mode de travail

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).

Figure D : AspectJ Browser

Mais le plus simple est certainement de se reposer sur le Plug-In AspectJ développé spécifiquement pour Eclipse. Celui-ci permet de :

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.

Exemple itératif

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.

1. Accès aux méthodes

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

Vous notez que ce code est complètement naïf : il ne se doute pas de ce qui va lui arriver. Pourtant, après application de l'aspect DNGCompteur, le résultat du lancement de la méthode Test.main donne le résultat suivant :

Figure E.1 : Compteur d'accès

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 :

Figure E.2 : Compteur d'accès

 

2. Accès aux attributs

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

Et sans surprise, le résultat comptabilise également l'accès aux attributs de l'objet Observable (en lecture uniquement, grâce à get()) :

Figure E.3 : Compteur d'accès

 

3. Ajout d'un attribut et d'un getter pour le compteur

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

On voit bien que l'aspect DNGCompteur est utilisée pour insérer les Conseils (nommés "DNGCOmpteur.ajc $ interELEMENT_SYNTAXIQUE $ CLASSE_DE_BASE $ METHODE_DE_BASE"), mais que l'attribut lui-même est bien situé dans la classe Observable.

4. Incrémentation du compteur et récupération de la valeur finale

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 org.dng.aop.base.Observable.compteurHits;
     
public int org.dng.aop.base.Observable.getCompteurHits(){
           
return compteurHits;
     
}
      pointcut sollicitationQuelconque(org.dng.aop.base.Observable o):

     
(call(* org.dng.aop..*.*(..)) ||
     
get(* org.dng.aop..*))
     
&& !within(DNGCompteur)
     
&& target (o);
     
before(org.dng.aop.base.Observable o): sollicitationQuelconque(o) {
           
o.compteurHits++;
           
System.out.println("Nombre hits : "+o.compteurHits);
     
}
}

DNGCompteur.java

Le résultat ne nous surprend plus :

Figure E.4 : Compteur d'accès

Il est toutefois intéressant de noter que dans le code "de base", l'attribut compteurHits et son accesseur deviennent accessibles ! Ce qui fait qu'il est légal dans la classe Test d'invoquer obs.getCompteurHits(), alors que cette méthode n'a pas été déclarée dans la classe Observable !

Faisons le point

AspectJ est un produit très simple à prendre en main, et à la fois très puissant. Après ce petit exemple d'utilisation, nous pouvons nous demander pour quels types  de besoins concrets nous pourrions l'utiliser :

Les exemples sont multiples, et vous trouverez certainement des applications novatrices de l'AOP sur vos propres projets !

AspectC#, Weave.NET, [AspectDNG ?]

Le problème

La question qui brûle les lèvres est bien sûr : "existe-t-il un équivalent à AspectJ dans le monde .NET " ?

La réponse, malheureusement, est négative. Quelques projets d'étude ont vu le jour, certes, tels que Weave.NET et AspectC#, mais ces projets sont à l'état d'ébauche.

Le plus avancé semble être AspectC#, qui est issu d'une thèse d'étudiant très intéressante à lire ici, et repris par Donal Lafferty; AspectC# est librement téléchargeable et se présente sous la forme d'un tisseur (AspectCSharp.exe) et d'un fichier de configuration XML pour associer les aspects au code de base. Mais ce produit est limité : il n'offre pas de Joinpoint sur les accès aux attributs par exemple, ni rien qui soit adapté aux delegates, aux structures, aux Attributes... Il est actuellement en version alpha, et sa page Web n'est guère confiante quant aux évolutions à venir ou au support de cet outil. Bref, nous sommes loin de la richesse, du support et de la pérennité de AspectJ.

Nous éprouvons donc une certaine frustration : à l'aube d'une ère nouvelle du développement logiciel, une technique séduisante, l'AOP, semble vouloir percer, et nous nous trouvons dans l'incapacité de l'expérimenter pour cause d'absence de l'outillage adéquat sur la plate-forme .NET.

La proposition

DotNetGuru souhaite négocier le virage de l'AOP sur .NET dès aujourd'hui, et vous faire profiter du mouvement. Sami et moi avons donc l'envie de développer notre propre tisseur d'aspects (appelons-le AspectDNG), et de placer son code sous la licence LGPL.

Mais comme vous le savez, cela n'a pas été le rôle de DotNetGuru jusqu'à présent de développer des outils sur .NET. Nous sommes donc à votre entière écoute, pour déterminer si les conditions suivantes sont réunies :

La question est posée, chers lecteurs... la balle est dans votre camp. Notre désir est de répondre au mieux à vos attentes, aussi est-ce à vous de nous dire si ce projet aurait pour vous plus ou moins de valeur ajoutée que les articles que nous pourrions écrire en lieu et place du développement d'Aspect.DNG.

La décision sera prise dans quelques temps, en fonction du sentiment recueilli dans les commentaires de cet article. C'est donc un mode de prise de décision démocratique que nous vous proposons, à l'image duquel nous aimerions voir le monde évoluer...

Outils de génération de code

Pour tous ceux qui seraient intéressées par ...

... nous vous proposons un panorama rapide des diverses techniques offertes par la plate-forme .NET dans ce domaine.

Emit

System.Reflection.Emit est une interface de programmation standard du framework .NET permettant de générer du code MS IL à la volée. C'est un outil très utile si vous souhaitez créer un compilateur (un compilateur PHP en MSIL par exemple...), mais il s'avère de bien trop bas niveau dans le cadre de la génération ou de la manipulation de code.

CodeDOM

System.CodeDom est une autre interface standard, de bien plus haut niveau, dont l'objectif est de représenter un arbre syntaxique abstrait (un AST) sous forme d'objets .NET. Comme son nom l'indique, CodeDom est au code .NET ce que le DOM est à un document XML.

Le gros avantage de CodeDom est d'être complètement abstrait, indépendant de toute syntaxe des langages de programmation. Cette représentation mémoire pourra ensuite être transformée en un fichier C#, VB.NET, ou tout autre langage .NET à condition de disposer du générateur de code correspondant.

Un petit exemple nous permettra de nous faire une idée du niveau d'abstraction de CodeDom :

namespace AOP {
     
using System;
     
using System.CodeDom;
     
using System.CodeDom.Compiler;
     
public class CodeDom {
           
public static void Main(string[] args){
            
     
// Unité de compilation ou de génération 
                 
codeCodeCompileUnit ccu = new CodeCompileUnit();
                  // Nouveau namespaceCodeNamespace 
                 
ns = new CodeNamespace("DngNamespace");
            
      ccu.Namespaces.Add(ns);
                  // Nouvelle classe
                 
CodeTypeDeclaration c = new CodeTypeDeclaration("DngClass");
            
      ns.Types.Add(c);
            
      c.IsClass = true;
                 
// Nouvelle méthode
                 
CodeEntryPointMethod m = new CodeEntryPointMethod();
            
      c.Members.Add(m);
                 
// Première manière d'ajouter une instruction
                 
CodeMethodInvokeExpression expr =
            
     
new CodeMethodInvokeExpression ( new CodeTypeReferenceExpression("System.Console"),
            
            "WriteLine", new CodePrimitiveExpression("Hello World!") );
            
      m.Statements.Add(new CodeExpressionStatement(expr));
                 
// Seconde manière, quasiment équivalents
                 
m.Statements.Add(new CodeSnippetStatement("System.Console.WriteLine(\"Hello\");")); 
            
      // Génération de codeCodeDomProvider comp;

                  // C#
                 
comp = new Microsoft.CSharp.CSharpCodeProvider();
            
      comp.CreateGenerator().GenerateCodeFromCompileUnit
            
            (ccu, Console.Out, new CodeGeneratorOptions());
                   // VB.NET
                 
comp = new Microsoft.VisualBasic.VBCodeProvider();
            
      comp.CreateGenerator().GenerateCodeFromCompileUnit
            
            (ccu, Console.Out, new CodeGeneratorOptions());
           
}
     
}
}

DNGCompteur
.java

Le résultat de ce petit programme semble satisfaisant :

C#
namespace
AO
     
namespace DngNamespace {
           
public class DngClass {
            
      public static void Main() {
            
            System.Console.WriteLine("Hello World!");
            
            System.Console.WriteLine("Hello");
            
      }
           
}
     
}
VB


  
Option Strict Off

  
Option Explicit On
  
Namespace DngNamespace

       Public Class DngClass
    
       Public Shared Sub Main()
               System.Console.WriteLine("Hello World!")
               System.Console.WriteLine("Hello");
           End Sub
      
End Class
 
End Namespace

Code généré par System.CodeDom

Mais à y regarder de plus près, l'utilisation d'une ligne de code littérale dans notre programme de génération s'est avérée fatale à la génération de VB.NET. En effet, le point-virgule en fin d'instruction ne respecte pas la syntaxe VB.NET. Nous voilà mis en garde : si nous souhaitons véritablement générer du code multi-langage avec CodeDom, il faudra prendre soin de passer par les classes d'abstraction fournies plutôt que de générer directement du texte dans une "snippet".

CodeDom nous a paru séduisant pour produire du code. Aussi pensions-nous avoir trouvé l'outil adéquat au développement d'un tisseur d'aspects ! Nous avons donc cherché à faire l'opération inverse : lire du code (C# ou VB.NET) pour créer automatiquement un arbre CodeDom en mémoire. Il se trouve justement que l'objet CodeDomProvider dispose d'une méthode CreateParser(). Eh bien c'est un piège : cette méthode, dans le framework standard, renvoie toujours null (que ce soit pour C# ou pour VB.NET). Microsoft ne fournit en standard l'implémentation d'aucun parseur de code source. Qu'à cela ne tienne : quelqu'un d'autre doit l'avoir développé pour les besoins d'un projet... On trouve en effet une implémentation Open Source d'un parseur C# générant un arbre CodeDom : CS CODEDOM (disponible sur http://ivanz.webpark.cz/csparser.html et sur http://sourceforge.net/projects/cscodedomparser/). Cette implémentation s'appuie d'ailleurs sur MCS, le parseur C# du projet Mono...

Bref, CodeDom est prometteur, mais il lui manque un ensemble de parseurs pour les langages .NET usuels : C#, VB.NET, MC++, JScript.NET et J#.

Conclusion

La programmation orientée aspect est une technique orthogonale aux autres modes de programmation (objet, procédurale...) et s'utilise en complément. Notre sentiment est qu'elle est promise à un avenir radieux, car elle permet de résoudre de manière simple et élégante des problèmes insolubles en son absence et qui portent atteinte à la maintenabilité du code des projets importants.

Factoriser les besoins techniques récurrents et les appliquer de manière générique à un ensemble d'éléments d'un projet est une méthode naturelle. Aujourd'hui, elle est parfaitement outillée dans le monde Java, et en passe de l'être sur .NET.

Il ne reste plus qu'à acquérir le réflexe des aspects, de la même manière que nous avons acquis celui de l'objet. Nous sommes persuadés que le bon usage des aspects (pourtant si simples) prendra du temps. Il sera facilité, comme l'a été celui de l'objet, par la formalisation et l'apprentissage de nouveaux Design Patterns... 

Auteur : Thomas GIL

Copyright © Janvier 2003

Ressources

Dossiers du feu magazine Developpeur Référence sur l'AOP : http://www.devreference.net

Site de l'AOP : http://www.aosd.net

(Et surtout n'allez pas faire comme moi, http://www.aosd.org n'est PAS le site officiel de l'AOP, tout comme http://tomcat.com n'est pas le site officiel du serveur JSP : Jakarta Tomcat)