Programmation orientée aspect en Java avec AspectJ

Selim KEBIR

Cet article est consacré à la programmation orientée aspect. Il présente d'abord, d'une manière indépendante de l'implémentation, les principaux concepts de l'AOP puis, illustre ces concepts avec des mises en œuvre pratiques en langage AspectJ à travers un exemple d'application.

Commentez ♪

- Titre : Programmation orientée aspect en Java avec AspectJ

- Auteur : Selim KEBIR

- Parution : 2 février 2010

- Licence : Copyright ® 2009 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Préoccupations transversales

- I-A-1. Enchevêtrement du code (code tangling)

- I-A-2. Éparpillement du code (code scattering)

- I-B. Méthodologie de l'AOP

- II. Concepts

- II-A. Aspect

- II-B. Point de jonction (joinpoint)

- II-C. Coupe (pointcut)

- II-D. Code advice (advice code)

- II-E. Mécanisme d'introduction

- II-F. Tissage (weaving)

- III. AspectJ

- III-A. Historique et origine

- III-B. Téléchargement et installation de l'outil

- III-C. Présentation générale

- III-D. Point de jonction

- III-E. Coupe

- III-F. Advice

- III-G. Déclaration intertype

- III-H. Aspect

- IV. Exemple d'application

- IV-A. Introduction

- IV-B. Le système d'arbre d'expression

- IV-C. La préoccupation « cache »

- IV-D. Exécution sans cache

- IV-E. Exécution avec cache

- V. Conclusion

- VI. Téléchargement des sources

- VII. Liens connexes

- VIII. Remerciements

- IX. Commentaires des lecteurs

I - Introduction

Depuis une trentaine d'années, l'approche orientée objet a procuré des bénéfices indénombrables dans la communauté génie logiciel. En effet, elle permet de modulariser de larges systèmes d'une manière intuitive, car elle offre un modèle de programmation riche très proche du monde réel. Aujourd'hui, l'orienté objet bénéficie d'un excellent background notamment grâce à la popularisation d'UML. En plus, une multitude de langages de programmation (Java, C++, Smalltalk…) ont adopté ce paradigme de manière native.

Cependant, l'approche orientée objet montre ses limites et échoue face à la modularisation des préoccupations transversales au système. Parmi les préoccupations transversales plus courantes, on trouve la sécurité, la gestion transactionnelle de la persistance, la synchronisation, le logging (Figure). Ces préoccupations ont la particularité d'un côté, d'être dispersées à travers plusieurs modules, et d'un autre côté, d'être enchevêtrées avec les modules métier du système. Ces deux phénomènes dégradent considérablement le maintien, la compréhension et l'évolutivité du code.

Préoccupations transversales

C'est là que la programmation orientée aspect intervient en apportant des mécanismes à la fois simples à appréhender et puissants qui permettent de capturer des préoccupations transversales. En effet, l'orientée aspect procure une solution élégante aux problèmes d'enchevêtrement et d'éparpillement du code. Aujourd'hui, cette technique d'ingénierie logicielle s'affirme comme étant la prochaine étape pour le découpage des systèmes en offrant une nouvelle dimension pour la modularisation notamment avec la notion d'aspect. En effet, parallèlement aux classes qui sont un support idéal pour modulariser les préoccupations métier du système, les aspects sont un support pour capturer les préoccupations transversales. Dans une démarche orientée aspect, les préoccupations transversales peuvent évoluer indépendamment des préoccupations métier et vice-versa (Figure). Et afin, que l'application finale prenne en compte toutes les préoccupations, le système passe par une étape dite de tissage d'aspects. Durant cette étape, les préoccupations transversales encapsulées dans les aspects vont être tissées ou intégrées dans les préoccupations métier.

Séparation des préoccupations transversales et métier

Actuellement, une panoplie de langages et de plateformes orientées aspects existe (AspectJ, AspectWerkz, AspectC++…). Dans cet article, nous allons nous intéresser au langage AspectJ qui est à notre connaissance l'expérimentation la plus aboutie de langages orientés aspects. Ce n'est pas un langage à part entière, mais une extension du langage Java qui permet à ce dernier de supporter la programmation orientée aspect.

Dans cet article, nous allons tout d'abord nous intéresser dans la section, aux concepts de la programmation orientée aspect à savoir les points de jonction, les coupes et les codes advices. La présentation de ces concepts sera indépendante de toute implémentation afin de faciliter leur appréhension. Puis, nous nous intéresserons dans la section au langage AspectJ en présentant l'implémentation des principaux concepts de l'AOP à travers ce langage. Et enfin, nous terminerons dans la section par un exemple pratique d'une application orientée aspect. Mais avant, nous allons voir les problèmes engendrés par la non-modularisation des préoccupations transversales ainsi que la méthodologie générale d'une démarche orientée aspect.

I-A - Préoccupations transversales

Les préoccupations transversales (en anglais crosscutting concerns) sont les fonctionnalités dites non métier. Un développeur est souvent confronté à ce genre de fonctionnalités lorsqu'il développe une application de grande envergure. Dans ce cas, même si on applique une bonne modularisation verticale des préoccupations métier, on aura toujours un problème de préoccupations horizontales qui transversent l'ensemble des modules métier. Il existe en fait deux principaux symptômes liés aux préoccupations transversales : l'enchevêtrement du code et l'éparpillement du code

I-A-1 - Enchevêtrement du code (code tangling)

L'enchevêtrement du code est provoqué quand un module est implémenté pour traiter plusieurs préoccupations en même temps. Un développeur a souvent affaire, pendant qu'il développe un module, à des préoccupations telles que la logique métier, la gestion transactionnelle de la persistance, le logging, la sécurité, etc. (Figure). Cela conduit à la présence simultanée d'éléments issus de chaque préoccupation et il en résulte en un enchevêtrement du code.

Enchevêtrement du code

Un autre exemple pour illustrer l'enchevêtrement du code est la notion d'espace multidimensionnel de préoccupations. Imaginons que les besoins sont projetés sur un espace multidimensionnel où chaque préoccupation représente une dimension. Dans cet espace, chaque préoccupation est indépendante et peut évoluer sans affecter les autres préoccupations. Par exemple, le changement du schéma de sécurité n'affecte pas la logique métier. Cependant, un espace multidimensionnel de préoccupation est réduit dans un espace unidimensionnel d'implémentation (Figure).

Espace des préoccupations et espace d'implémentation

Tant que l'espace d'implémentation est unidimensionnel, on se concentre souvent sur l'implémentation de la préoccupation qui a un rôle dominant ; les autres préoccupations sont alors enchevêtrées avec la préoccupation dominante.

I-A-2 - Éparpillement du code (code scattering)

L'éparpillement du code survient quand une préoccupation est implémentée dans plusieurs modules. Les préoccupations transversales sont, par définition, dispersées à travers plusieurs modules. Par exemple, dans un système utilisant une base de données, le logging est une préoccupation implémentée dans tous les modules qui accèdent à la base (Figure).

Éparpillement du code de la préoccupation logging

I-B - Méthodologie de l'AOP

Le développement de logiciels en utilisant l'approche orientée aspect est similaire au développement de logiciels avec d'autres méthodologies (voir figure) : identification des préoccupations, leur implémentation, et leur combinaison pour former le système final. La communauté des chercheurs de l'AOP définit ces trois étapes comme suit :

Étapes de développement dans une méthodologie AOP

1. Décomposition aspectuelle : les besoins sont ici décomposés pour identifier les préoccupations fonctionnelles et transversales. Par exemple, un développeur pourra identifier les préoccupations suivantes : logique métier, logging, cache, gestion transactionnelle de la persistance, authentification, etc. Ensuite, il pourra décider que seule la logique métier est une préoccupation fonctionnelle. Toutes les autres préoccupations sont des préoccupations transversales au système et qui vont être utilisées par plusieurs modules.

2. Implémentation des préoccupations : chaque préoccupation est implémentée indépendamment des autres. Comme pour l'exemple du paragraphe précédent, le développeur aura à implémenter la logique métier, le logging, le cache, la gestion transactionnelle de la persistance, l'authentification, etc.

3. Recomposition aspectuelle : des règles de recomposition sont spécifiées en créant des unités appelées aspects. Le processus de recomposition, aussi connu sous le nom de tissage ou d'intégration, utilise ces informations pour composer le système final. Comme pour l'exemple précédent, le développeur peut créer une règle qui s'assure que chaque opération effectuée par la logique métier doit d'abord être mise en journal (loggée).

II - Concepts

II-A - Aspect

Un aspect est une entité logicielle qui capture une fonctionnalité transversale à une application.

Les trois éléments principaux définis dans un aspect sont les coupes (pointcuts), les codes advice et le mécanisme d'introduction. Les coupes définissent où l'aspect doit être intégré dans une application et les codes advice définissent ce que fait l'aspect (le quoi). Le mécanisme d'introduction permet d'ajouter du contenu structurel dans une application.

Nous allons définir ces concepts avec plus de détails dans les paragraphes suivants.

II-B - Point de jonction (joinpoint)

Un point de jonction est un point dans le flot de contrôle d'un programme dans lequel un ou plusieurs aspects peuvent être appliqués.

Bien que la notion de point de jonction soit générale (potentiellement, chaque instruction d'un programme peut être un point de jonction), tous les points dans le flot de contrôle ne sont pas considérés comme utiles pour l'AOP. Les points de jonction sont groupés en fonction de leur type, et seulement un sous-ensemble de tous les types possibles de points de jonction est supporté par les langages orientés aspects.

Bien que la définition d'un point de jonction désigne un moment de l'exécution, cette définition est basée sur la structure d'un programme (ses classes, méthodes, attributs, etc.). Les catégories suivantes décrivent les types de points de jonction communément rencontrés et qui sont indépendants de toute implémentation.

- Les méthodes : dans les langages orientés objet, l'exécution d'un programme peut être considérée comme une séquence d'appels et d'exécution de méthodes. Les différents scénarios d'exécution d'une application peuvent être exprimés en termes de séquences de messages qui déclenchent l'exécution de méthodes. Les appels et les exécutions de méthodes sont donc deux types de points de jonction couramment utilisés.

- Les constructeurs : les constructeurs sont principalement utilisés pour créer les instances des classes d'une application. Comme pour les méthodes, les appels et les exécutions d'un constructeur correspondent à des types de points de jonction.

- Les attributs : les langages orientés aspect considèrent les opérations de lecture et d'écriture sur les attributs comme des types de points de jonction. Un exemple concret d'utilisation de ce type de points de jonction est la gestion transactionnelle de la persistance.

- Les exceptions : les exceptions sont levées (throw) pour signaler une situation d'exécution anormale et elles sont capturées (catch}) pour exécuter un traitement particulier. Ces deux événements (le throw et le catch) sont des points d'exécution très importants dans une application. Ils sont tous les deux considérés par la plupart des langages orientés aspect comme des types de points de jonction.

Les appels et les exécutions de méthodes sont incontestablement les types de points de jonction les plus utilisés dans l'AOP. D'autres éléments de code, comme les boucles FOR et les instructions conditionnelles IF, qui définissent la structure d'un programme sont considérés de très faible granularité pour être utilisés pour définir des points de jonction.

En conclusion, tous les programmes, même les plus simples, contiennent plusieurs points de jonction différents. La tâche du développeur est de sélectionner les points de jonction qui sont utiles pour implémenter un aspect donné. Cette sélection est réalisée grâce à la notion de coupe (pointcut), qui est présentée dans le paragraphe suivant.

II-C - Coupe (pointcut)

Dans le paragraphe précédent, nous avons dit que les points de jonction sont des points dans le flot de contrôle d'un programme dans lequel un ou plusieurs aspects peuvent être appliqués. La notion de point de jonction n'est pas suffisante à elle seule pour définir quels points de jonction sont pertinents pour un aspect donné. On a besoin d'une autre notion pour décrire les points de jonction. Cette notion est la coupe.

Une coupe sélectionne un ensemble de points de jonction.

Un langage de programmation orienté aspect se doit de fournir au développeur une structure syntaxique permettant de déclarer une coupe. Cependant chaque langage définit sa propre syntaxe.

Généralement, une coupe est définie avec un langage de patterns qui permet d'indiquer où l'aspect doit être intégré dans l'application en utilisant des quantificateurs, des opérateurs booléens et des caractères joker ou \textit{wildcards} (comme le caractère *).

II-D - Code advice (advice code)

Un code advice est un bloc de code définissant le comportement d'un aspect.

Concrètement, un code advice est un bloc d'instructions qui spécifie le comportement de l'aspect. Un code advice est toujours associé à une coupe ou plus exactement aux points de jonction sélectionnés par cette coupe. En effet, un code advice n'est jamais appelé manuellement, mais il est invoqué chaque fois qu'un point de jonction, sélectionné par la coupe à laquelle il est associé, survient.

Un code advice peut être exécuté selon trois modes : avant, après, ou autour d'un point de jonction. Lorsqu'il est exécuté autour du point de jonction, il peut carrément remplacer l'exécution de ce dernier, ou bien lui redonner le contrôle.

II-E - Mécanisme d'introduction

Le mécanisme d'introduction est un mécanisme d'extension permettant d'introduire de nouveaux éléments structuraux au code d'une application.

Le mécanisme d'introduction permet d'étendre la structure d'une application et non pas le comportement de cette dernière. En effet, le mécanisme d'introduction ne s'appuie pas sur la notion de coupe, mais va opérer sur des emplacements bien définis dans le programme. On peut dire que le mécanisme d'introduction est pour l'orientée aspect ce que l'héritage est pour l'orientée objet puisque ces deux derniers permettent d'étendre la structure et non pas le comportement de l'application.

II-F - Tissage (weaving)

Le tissage (weaving) est le processus qui prend en entrée un ensemble d'aspects et une application de base et fournit en sortie une application dont le comportement et la structure sont étendus par les aspects.

Une application orientée aspect contient des classes et des aspects. L'opération qui prend en entrée les classes et les aspects et produit une application qui intègre les fonctionnalités des classes et des aspects est connue sous le nom de tissage d'aspects (aspect weaving). Le programme qui réalise cette opération est appelé tisseur d'aspects (aspect weaver) ou bien tisseur (weaver) tout court. (Figure)

Tissage des aspects

III - AspectJ

III-A - Historique et origine

L'histoire d'AspectJ est étroitement liée à celle de la programmation orientée aspect. En effet, ce langage a été développé par la même équipe à l'origine de l'AOP. Un premier prototype d'AspectJ a été réalisé en 1998. Et depuis, plusieurs versions d'AspectJ ont vu le jour, et chacune apportait de nouvelles fonctionnalités et corrigeait les bogues de la précédente. La première version officielle d'AspectJ, désigné AspectJ 1.0, a été réalisée en novembre 2001. Durant cette année, l'AOP a été complètement reconnue par la communauté informatique mondiale, et une édition spéciale du journal Communications of the ACM a été dédiée à l'AOP.

En décembre 2002, le projet AspectJ a quitté XEROX PARC et a rejoint la communauté open source Eclipse. Et depuis, le plugin Eclipse AspectJ Developpement Tools (AJDT) est développé. Ce plugin intègre AspectJ et permet d'écrire, de compiler et d'exécuter des programmes orientés aspects dans l'environnement de développement Eclipse.

III-B - Téléchargement et installation de l'outil

AspectJ peut être téléchargé à partir de cette adresse http://www.eclipse.org/aspectj/downloads.php, il se présente sous forme d'un compilateur (ajc) et d'un ensemble de bibliothèques. Pour installer AspectJ il suffit d'exécuter l'installation préalablement téléchargée depuis le lien indiqué précédemment.

Installation d'AspectJ

À la fin de l'installation, il est recommandé de mettre le répertoire %aspectj_folder%/bin dans la variable d'environnement path afin de pouvoir l'utiliser dans la ligne de commande à partir de n'importe quel chemin.

III-C - Présentation générale

AspectJ est aujourd'hui une implémentation orientée aspect phare qui fournit un excellent support pour appréhender les concepts de la programmation orientée aspect. Comme indiqué dans le paragraphe précédent, sa plus grande force réside dans le fait qu'il est issu des travaux de la même équipe à l'origine de l'orientée aspect. AspectJ est donc une extension orientée aspect du langage de programmation Java. Il permet de déclarer des aspects, des coupes, des codes advices et des introductions. Il offre aussi un tisseur d'aspects appelé ajc (pour AspectJCompiler) qui prend en entrée des classes java et des aspects, et produit en sortie des classes dont le comportement est augmenté par les aspects. Aujourd'hui, AspectJ bénéficie d'une multitude d'outils de débogage, d'environnement de développements et de visualisateurs d'aspects. AspectJ permet de définir deux types de transversalités avec les classes de base : transversalité statique (static crosscutting) et transversalité dynamique (dynamic crosscutting).

- Transversalité statique : la transversalité statique consiste à augmenter la structure des classes. Pour cela AspectJ offre la notion de mécanisme d'introduction qui permet entre autres d'ajouter des éléments structuraux comme des attributs ou des méthodes aux classes. Le mécanisme d'introduction d'AspectJ permet aussi d'ajouter des liens entre des classes comme l'héritage et l'implémentation d'interfaces. Concrètement, AspectJ offre un support simple et facile à appréhender pour implémenter ce genre de transversalité.

- Transversalité dynamique : la transversalité dynamique consiste à augmenter le comportement des classes. Pour cela AspectJ offre les notions de coupes et de code advices. Les coupes servent à sélectionner des points précis dans les classes. Et les advices iront se greffer avant, après ou autour de ces points afin d'étendre leur comportement.

Dans ce qui suit, nous allons aborder les principaux concepts de ce langage et leur mise en œuvre pratique. Pour cela, tous les exemples qui seront présentés reposeront sur le diagramme de classes de la figure suivante.

Diagramme de classes d'un système de figures géométriques

III-D - Point de jonction

En réalité, un point de jonction est n'importe quel point d'exécution dans un système. Parmi tous les points de jonction possibles dans un système, on cite de façon non exhaustive : l’appel à une méthode ; l’exécution d'une méthode ; l’affectation de variable ; l’appel au constructeur d'une classe ; une instruction conditionnelle (i.e. IF/THEN/ELSE) ; le traitement d'une exception ; les boucles (i.e. FOR, WHILE, DO/WHILE) ; etc.

Mais en pratique, et par souci de prévenir la dépendance de l'implémentation et la transversalité instable, le modèle de points de jonction adopté par AspectJ n'offre qu'un sous-ensemble de points de jonction possibles. Par exemple, il ne considère pas les boucles comme des points de jonction. L'ensemble des points de jonction offerts par AspectJ est résumé dans le tableau suivant :

Points de jonction disponibles dans AspectJ

Point de jonction

Description

Method call

Quand une méthode est appelée

Method execution

Quand le corps d'une méthode est exécuté

Constructor call

Quand un constructeur est appelé

Constructor execution

Quand le corps d'un constructeur est exécuté

Static initializer execution

Quand l'initialisation statique d'une classe est exécutée

Object pre-initialization

Avant l'initialisation de l'objet

Object initialization

Quand l'initialisation d'un objet est exécutée

Field reference

Quand un attribut non constant d'une classe est référencé

Field set

Quand un attribut d'une classe est modifié

Handler execution

Quand un traitement d'une exception est exécuté

Advice execution

Quand le code d'un advice est exécuté

Dans AspectJ, tous les points de jonction ont un contexte associé à eux. Par exemple, le contexte d'un point de jonction correspondant à un appel de méthode contient l'objet appelant, l'objet appelé, et les arguments de la méthode. De la même manière, le contexte d'un point de jonction correspondant au traitement d'une exception contient l'objet courant, et l'exception levée.

III-E - Coupe

Dans AspectJ, les coupes correspondent à plusieurs points de jonction dans le flot d'un programme. Par exemple, la coupe :

call(void Point.setX(int))

capture chaque point de jonction correspondant à un appel à la méthode setX() de la classe Point qui ne retourne aucune valeur void et qui a comme paramètre un entier (int).

Une coupe peut être construite à partir d'autres coupes en utilisant les opérateurs and, or et not (respectivement &&, || et !). Par exemple, la coupe :

call(void Point.setX(int)) || call(void Point.setY(int))

désigne les points de jonction correspondant à un appel à la méthode Point.setX() ou un appel à la méthode Point.setY().

Les coupes peuvent identifier des points de jonction de différentes classes, en d'autres termes, elles peuvent être transverses aux classes. Par exemple, la coupe :

call(void FigureElement.incrXY(int,int)) ||
call(void Point.setX(int))               ||
call(void Point.setY(int))               ||
call(void Line.setP1(Point))             ||
call(void Line.setP2(Point))

capture chaque point de jonction qui est un appel à une des cinq méthodes (la première méthode est une méthode d'interface).

Dans le dernier exemple, la coupe capture tous les points de jonction correspondant au mouvement d'un objet de type FigureElement. AspectJ permet de déclarer des coupes nommées avec le mot-clé pointcut, afin qu'elles puissent être réutilisées sans avoir à les redéfinir. Les instructions suivantes déclarent une coupe nommée :

pointcut move():
call(void FigureElement.incrXY(int,int)) ||
call(void Point.setX(int))               ||
call(void Point.setY(int))               ||
call(void Line.setP1(Point))             ||
call(void Line.setP2(Point));

Ainsi, on peut appeler à n'importe quel moment la coupe nommée move().

Jusque là, les coupes que nous avons données étaient basées sur une énumération explicite d'un ensemble de signatures de méthodes. AspectJ offre aussi un mécanisme qui permet de spécifier des coupes en termes de propriétés de méthodes autres que leur nom exact. La façon la plus simple de le faire est d'utiliser les expressions régulières pour exprimer les champs de la signature des méthodes. Par exemple, la coupe :

call(void Point.set*(..))

capture chaque point de jonction correspondant à un appel d'une méthode ne retournant aucun résultat (void) et appartenant à la classe Point et commençant par la chaine set, quels que soient le type et le nombre de ses paramètres.

La coupe :

call(public * Line.* (..))

capture tous les appels aux méthodes publiques (public) de la classe Line.

Les exemples précédents ne font appel qu'à un seul type de coupe qui est l'appel de méthode (i.e. call). Il existe d'autres types de coupes dans AspectJ comme : l'exécution de méthode (ex. : execution(void Point.setX(..))), ou l'accès aux attributs (ex. : get(Point.x)), etc.

III-F - Advice

Les coupes capturent les points de jonction, mais elles ne font rien de plus. Pour implémenter un comportement transversal, on utilise les advices. En effet, un advice fait correspondre une coupe (i.e. un ensemble de points de jonction) à un bout de code exécuté à chaque point de jonction de cette coupe.

Un code advice est un bloc d'instructions associé à une coupe. Il est exécuté avant, après ou autour des points de jonction sélectionnés par la coupe qui lui est associée. AspectJ offre trois types de codes advice : before, after et around. Les codes advice de type before (respectivement after) permettent d'introduire un comportement avant (respectivement après) un point de jonction. Cependant les codes advice de type after se déclinent en deux variantes : after returning et after throwing qui signifient respectivement après le retour d'une méthode sans exception et avec exception. Un advice de type around quant à lui définit un bloc d'instructions qui s'exécute autour d'un point de jonction. Il permet éventuellement de remplacer carrément l'exécution d'une méthode. AspectJ fournit la méthode proceed() qui permet de rendre le contrôle de l'exécution au point de jonction dans un code advice de type around. Le code suivant montre un exemple d'advice de type before, qui utilise la coupe move() défini précédemment :

before() : move() { System.out.println("Figure sur le point d'être déplacée");}

Le code suivant montre un exemple d'advice de type around :

around() : call(Display.update()) { if (! Display.disabled()) proceed();}

Nous avons dit précédemment qu'à chaque point de jonction est associé un contexte d'exécution (cf. section ) contenant par exemple les arguments de la méthode si le point de jonction en est une. Dans AspectJ, ce contexte est accessible via les trois coupes primitives : this, target, args. Ce contexte s'avère très utile lorsqu'on veut par exemple, accéder aux paramètres d'une méthode dans un code advice. L'advice suivant :

after(int x,int y) returning : call(void FigureElement.incrXY(int,int) && args(x,y) {
System.out.println("la figure a été déplacée de +"x"+","+y);
}

récupère les arguments de la méthode incrXY(int,int) dans les variables x et y et peut faire n'importe quel traitement avec ces valeurs.

Les coupes primitives this et target permettent respectivement de récupérer l'objet courant et l'objet cible du contexte d'exécution pour le point de jonction. Le tableau suivant résume la signification de l'objet courant, de l'objet cible et des arguments pour chaque type de points de jonction.

Contexte des points de jonction

Point de jonction

Objet courant

Objet cible

Arguments

Method Call

L'objet appelant

L'objet appelé

Les arguments de la méthode

Method Execution

L'objet appelant

L'objet appelé

Les arguments de la méthode

Constructor Call

L'objet appelant

Néant

Les arguments du constructeur

Constructor Execution

L'objet appelant

L'objet appelant

Les arguments du constructeur

Static initializer execution

Néant

Néant

Néant

Object pre-initialization

Néant

Néant

Les arguments du constructeur

Object initialization

L'objet appelant

L'objet appelant

Les arguments du constructeur

Field reference

L'objet appelant

L'objet appelé

Néant

Field assignment

L'objet appelant

L'objet appelé

La valeur assignée

Handler execution

L'objet appelant

L'objet appelant

L'exception levée

Advice execution

L'aspect appelant

L'aspect appelant

Les arguments de l'advice

III-G - Déclaration intertype

Les déclarations intertypes, dans AspectJ, correspondent au mécanisme d'introduction vu précédemment (cf. section II). Elles permettent de déclarer des membres dans des classes, ou de changer la relation d'héritages entre classes. Le code suivant montre quelques exemples de déclaration intertypes. Il s'agit d'ajouter un attribut name de type String et deux méthodes setName() et getName() à la classe Point :

public String Point.name ;
public void Point.setName ( String name ) { this.name = name ; }
public String Point.getName ( ) { return name ; }

L'instruction suivante permet de déclarer que les classes Point et Line héritent de la classe GeometricObject :

declare parents : (Point || Line) extends GeometricObject ;

III-H - Aspect

Dans AspectJ. Un aspect contient tous les ingrédients nécessaires pour la définition d'une préoccupation transversale à savoir : les définitions de coupes, les codes advice et les déclarations intertypes. Il peut aussi éventuellement contenir des attributs et des méthodes qui lui sont propres. Le code suivant montre un exemple d'aspect réalisant la préoccupation de mise à jour d'affichage :

aspect UpdateDisplay {
pointcut move(FigureElement elem) : target (elem) &&
( call ( void Line.setP1 (Point) ) ||
call ( void Line.setP2 (Point) ) ||
call ( void Point.setX (int) )   ||
call ( void Point.setY (int) )   ||
call ( void FigureElement.incrXY(int, int) ) ) ;

after(FigureElement elem) returning : move ( elem ) {
Display.update(elem);
}
}

IV - Exemple d'application

IV-A - Introduction

Dans cette section, nous allons nous intéresser à un exemple d'implémentation d'une préoccupation transversale sur un système existant. D'abord, nous allons commencer par décrire le système sur lequel va porter l'exemple. Il s'agit d'un système d'arbre d'expression syntaxique. Nous allons le décrire à l'aide de diagrammes UML accompagnés du code source Java correspondant. Ensuite, nous allons décrire la préoccupation qu'on veut implémenter dans le système. Cette préoccupation consiste en l'implémentation d'un cache qui permettra d'optimiser l'évaluation des arbres syntaxiques en évitant les parcours en profondeur. Enfin, nous allons terminer par comparer l'exécution de l'exemple avec et sans la préoccupation cache.

IV-B - Le système d'arbre d'expression

Le système permet de représenter des expressions syntaxiques sous forme d'arbre d'expression syntaxique. Par souci de simplicité, nous avons décidé de ne représenter qu'une seule opération qui est l'opération Plus. Donc, le système est composé de trois classes : Expression, Number et Plus et il est décrit par le diagramme de classe de la figure suivante :

Système d'arbre d'expression

Le code source correspondant aux trois classes du système est très simple et est présenté ci-dessous.

Expression.java

package org.sdf;

public abstract class Expression {
public abstract int eval();
}

Plus.java

package org.sdf;

public class Plus extends Expression {
private Expression leftExpression;
private Expression rightExpression;

public Plus(Expression leftExpression, Expression rightExpression) {
super();
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}

public Expression getLeftExpression() {
return leftExpression;
}

public void setLeftExpression(Expression leftExpression) {
this.leftExpression = leftExpression;
}

public Expression getRightExpression() {
return rightExpression;
}

public void setRightExpression(Expression rightExpression) {
this.rightExpression = rightExpression;
}

@Override
public int eval() {
System.out.println("Evaluation d'un noeud");
return leftExpression.eval() + rightExpression.eval();
}
}

Number.java

package org.sdf;

public class Number extends Expression {
private int value;

public Number(int value) {
this.value = value;
}

public void setValue(int value) {
this.value = value;
}

@Override
public int eval() {
System.out.println("Evaluation d'un noeud");
return value;
}
}

Si on exécute le programme suivant

Main.java

package org.sdf;

public class Main {

/**
* @param args
*/
public static void main(String[] args) {
Number dix = new Number(10);
Number neuf = new Number(9);
Number cinq = new Number(5);
Plus plus1 = new Plus(dix,neuf);
Plus plus2 = new Plus(plus1,cinq);
// au début, defaut de cache obligatoire
System.out.println("premier appel");
System.out.println(plus2.eval());
// on a un cache valide, donc on le consulte au lieu de parcourir l'arbre
System.out.println("deuxième appel");
System.out.println(plus2.eval());
// on va altérer la valeur du nœud 5
// qui va conduire à invalider son cache ainsi que celui de operation2
cinq.setValue(4);
System.out.println("troisième appel");
System.out.println(plus2.eval());
}

}

on aura comme résultat l'arbre de la figure suivante :

Arbre représentant l'expression ((10+9)+5)

IV-C - La préoccupation « cache »

Supposons que nous voulons étendre le système pour qu'il supporte un mécanisme de cache. Le principe du cache dans l'arbre d'expression est simple : on ajoute une variable cache au niveau de chaque nœud, et à chaque appel de eval(), on vérifie si la valeur du cache est valide. Si c'est le cas, on n'aura pas besoin de parcourir les sous-arbres gauches et droits.

Apporter un mécanisme de cache au système est certes très bénéfique côté performance. Mais malheureusement, le code source sera moins lisible. Car, d'une part, il y aura un enchevêtrement du code source métier (i.e. code source original du système d'arbre d'expression) et du code source de la préoccupation cache et d'autre part, la préoccupation cache sera dispersée à travers les classes du système. En effet, pour implémenter une telle préoccupation, on aura à déclarer une variable cache dans la classe Expression qui détiendra la valeur courante du cache, et des méthodes pour accéder a cette variable. De plus, on devra implémenter un mécanisme d'invalidation du cache dans toutes les sous-classes de la classe Expression au cas où la valeur du cache serait invalide. Et puis, on devra changer l'implémentation des méthodes eval() dans toutes les classes de façon à ce qu'elles renvoient la valeur du cache s'il est valide.

C'est là où l'AOP intervient, en apportant une solution efficace et élégante aux problèmes cités ci-dessus. La préoccupation cache sera implémentée dans un seul module et non éparpillée à travers plusieurs classes.

Comme précisé ci-dessus, pour gérer le cache, on devra déclarer une variable cache qui détient la valeur courante du cache, ainsi qu'une variable booléenne qui indique si le cache est valide ou pas. Ceci est réalisé grâce au mécanisme de déclaration intertypes (cf. section ) comme suit :

private int Expression.cache;
private boolean Expression.cacheValid = false;
public int Expression.getCache() {
return cache;
}

public void Expression.setCache(int cache) {
this.cache = cache;
}

public boolean Expression.isCacheValid() {
return cacheValid;
}

public void Expression.validateCache() {
this.cacheValid = true;
}

En plus de ces deux variables, chaque nœud devra connaitre l'identité de son père afin qu'il puisse lui signaliser que son cache n'est plus valide, ce qui va impliquer aussi l'invalidation du cache du père et vice-versa jusqu'à la racine. Ceci est fait en déclarant une variable appelée ancestor qui pointera vers le père du nœud, et une méthode qui permet d'invalider le cache des ascendants (s'ils existent) en cascade, comme suit :

private Expression Expression.ancestor = null;
public void Expression.invalidateCache() {
cacheValid = false;
if (this.getAncestor()!=null) this.getAncestor().invalidateCache();
}
public Expression Expression.getAncestor() {
return ancestor;
}

public void Expression.setAncestor(Expression ancestor) {
this.ancestor = ancestor;
}

L'invalidation du cache devra survenir après chaque changement de valeur d'un nœud. Le changement de valeur d'un nœud consiste en l'appel d'une des méthodes :

- Number.setValue();

- Plus.setLeftExpression();

- Plus.setRightExpression();

En AspectJ, cette coupe s'exprime comme suit :

pointcut changeValue(Expression exp):
target(exp) &&
(
call(public void Number.setValue(int)) ||
call(public void Plus.setLeftExpression(Expression)) ||
call(public void Plus.setRightExpression(Expression))
);

Et l'advice correspondant à cette coupe devra appeler la méthode invalidateCache() sur l'objet exp (objet sur lequel la méthode a été appelée et récupérée avec la coupe primitive target) après chaque occurrence de l'un des points de jonction de la coupe. Il est déclaré comme suit :

after(Expression exp):changeValue(exp) {
exp.invalidateCache();
}

Afin de pouvoir consulter le cache avant d'évaluer les fils gauches et droits, on devra capturer chaque point de jonction correspondant à un appel à la méthode eval(). Ceci est effectué grâce à la coupe suivante :

pointcut evaluation(Expression exp):
target(exp) && call(public int Expression.eval());

Et l'advice correspondant à cette coupe doit d'abord voir si le cache est valide. Si ce n'est pas le cas, il appelle la méthode originale avec proceed() pour évaluer la valeur des sous-arbres, puis il affecte cette valeur au cache, le valide et retourne sa valeur. Il est déclaré comme suit :

int around(Expression exp):evaluation(exp) {
if (!exp.isCacheValid()) {
int result = proceed(exp);
exp.setCache(result);
exp.validateCache();
}
return exp.getCache();
}

La dernière chose à faire consiste à créer un lien entre un nœud et son père. Pour cela, on devra capturer tous les points de jonction correspondant au constructeur de la classe Plus. Ceci est fait avec la coupe suivante :

pointcut PlusCreation(Plus exp):
this(exp) && execution(Plus.new(Expression,Expression));

Et l'advice correspondant devra lier le père aux fils, en utilisant la méthode setAncestor() déclarée précédemment, comme suit :

after(Plus exp) : PlusCreation(exp) {
exp.getLeftExpression().setAncestor(exp);
exp.getRightExpression().setAncestor(exp);
}

Voilà, il ne reste plus qu'à mettre tous ces petits bouts de code dans un même module (ou aspect). Ce qui va résulter en ceci :

Caching.aj

package org.sdf;

public aspect Caching {
private int Expression.cache;
private boolean Expression.cacheValid = false;
private Expression Expression.ancestor = null;

public int Expression.getCache() {
return cache;
}

public void Expression.setCache(int cache) {
this.cache = cache;
}

public boolean Expression.isCacheValid() {
return cacheValid;
}

public void Expression.validateCache() {
this.cacheValid = true;
}

public void Expression.invalidateCache() {
cacheValid = false;
if (this.getAncestor()!=null) this.getAncestor().invalidateCache();
}

public Expression Expression.getAncestor() {
return ancestor;
}

public void Expression.setAncestor(Expression ancestor) {
this.ancestor = ancestor;
}

pointcut changeValue(Expression exp):
target(exp) &&
(
call(public void Number.setValue(int)) ||
call(public void Plus.setLeftExpression(Expression)) ||
call(public void Plus.setRightExpression(Expression))
);

after(Expression exp):changeValue(exp) {
exp.invalidateCache();
}

pointcut evaluation(Expression exp):
target(exp) && call(public int Expression.eval());

int around(Expression exp):evaluation(exp) {
if (!exp.isCacheValid()) {
int result = proceed(exp);
exp.setCache(result);
exp.validateCache();
}
return exp.getCache();
}

pointcut PlusCreation(Plus exp):
this(exp) && execution(Plus.new(Expression,Expression));

after(Plus exp) : PlusCreation(exp) {
exp.getLeftExpression().setAncestor(exp);
exp.getRightExpression().setAncestor(exp);
}
}

IV-D - Exécution sans cache

Pour ne pas prendre en compte l'aspect Caching, le fichier Caching.aj ne doit pas être indiqué au compilateur ajc lors de la compilation. Les commandes suivantes servent à compiler les classes sans la prise en compte de l'aspect Caching et à exécuter l'application :

ajc -cp c:\aspectj1.6\lib\aspectjrt.jar -source 1.6 org/sdf/*.java
java org.sdf.Main

Ce qui donne le résultat suivant après l'exécution :

premier appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
24
deuxième appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
24
troisième appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
23

On remarque qu'à chaque appel, on devra visiter les cinq nœuds de l'arbre.

IV-E - Exécution avec cache

Pour prendre en compte l'aspect Caching, le fichier Caching.aj doit être indiqué au compilateur ajc lors de la compilation. Les commandes suivantes servent à compiler les classes avec la prise en compte du tissage de l'aspect Caching et à exécuter l'application :

ajc -cp c:\aspectj1.6\lib\aspectjrt.jar -source 1.6 org/sdf/*.java org/sdf/*.aj
java -cp c:\aspectj1.6\lib\aspectjrt.jar;. org.sdf.Main

Ce qui donne le résultat suivant après l'exécution :

premier appel
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
Evaluation d'un noeud
24
deuxième appel
24
troisième appel
Evaluation d'un noeud
Evaluation d'un noeud
23

On remarque qu'au premier appel de eval(), on devra créer 5 défauts de cache obligatoires. Mais au deuxième appel aucun nœud n'est évalué, la valeur 24 est extraite du cache et aucun parcours de l'arbre n'est effectué. Au troisième appel la valeur du nœud 5 est altérée donc la valeur de son cache et aussi celle du cache de son père (le nœud plus 2) sont invalidées, ce qui résulte en la réévaluation de seulement deux nœuds dans l'arbre.

V - Conclusion

Dans cet article, nous avons fait un survol sur la programmation orientée aspect. Après avoir expliqué les problèmes auxquels l'AOP apporte des solutions efficaces dans la section, nous avons abordé les principaux concepts de ce paradigme dans la section en nous basant sur des références plus ou moins récentes. Ensuite, nous avons présenté dans la section le langage AspectJ, ses origines, les notions qu'il apporte au langage de programmation Java. Nous avons vu la syntaxe de ses principales composantes accompagnée de plusieurs exemples illustratifs.

La section présente un exemple concret d'implémentation d'une préoccupation transversale dans un système existant. Il s'agit d'implémenter un schéma de cache dans un système d'arbre d'expression, afin d'éviter des parcours en profondeur. Pour cela, le système a subi une évolution sans perdre le moindre degré de maintenabilité ou de compréhensibilité. La préoccupation transversale Cache a été exprimée dans le langage AspectJ. Elle a été encapsulée dans un seul module et le programme principal n'a subi aucune modification. Les résultats empiriques ont montré la faisabilité de cette approche.

VI - Téléchargement des sources

Télécharger

VII - Liens connexes

- Critique du livre Refactoring des applications Java/J2EE

- Critique du livre Programmation Orienté Aspect pour Java / J2EE

- Introduction à AOP (Aspect-Oriented Programming) avec le framework Spring

- Framework Programmation Orientée Aspect sous Delphi

- AspectJ

- AspectJ Developpement tools

VIII - Remerciements

Je remercie La Zélie et jacques_jean pour l'excellent travail qu'ils ont effectué pour la relecture orthographique ainsi que Ricky81 et Baptiste Wicht pour leurs conseils et encouragements.

IX - Commentaires des lecteurs

Vos commentaires sont les bienvenus dans cette discussion Commentez