Il y a 14 ans -
Temps de lecture 5 minutes
Stateful Aspects
L’AOP (Programmation Orientée Aspect) permet au sein d’un programme d’implémenter facilement des problématiques transversales, comme la gestion de transaction, les mécanismes de cache ou la sécurité. Généralement, le traitement de ces opérations est soit local à une méthode, soit sans état. Je vous propose dans cet article de vous montrer comment déclarer des aspects dont le cycle de vie n’est plus celui de la JVM (static) mais fonction d’un pointcut.
L’application témoin
Voici une magnifique application ayant la structure suivante : Application -> Manager -> DAO avec un modèle simple : Caddy -> * Item. Chaque run de l’application ajoute 10 items au Caddy et effectue un paiement. Le test case effectue 5 run().
package fr.xebia.aop.app; import java.util.Random; public class Application implements Runnable{ static Random r = new Random(); private CaddyManager manager = new CaddyManager(); public void run() { for (int i =0 ; i < 10; i++) { manager.addItem(new Item(i,"Item#"+i,r.nextInt(100))); } manager.purchase(); manager.clearCaddy(); } }
Dans la suite de l'article nous allons écrire 2 aspects qui vont nous permettre de :
- Connaître le nombre d'appels au DAO lors de l'appel à la méthode 'purchase'.
- Calculer la moyenne du prix des Item du Caddy.
Nombre d'appels au DAO
Utilisation d'un aspect 'classique'
Le premier exemple va permettre de connaître le nombre d'appels au DAO lors de l'appel à la méthode 'purchase'. Voici le code de la classe CaddyManager
.
package fr.xebia.aop.app; public class CaddyManager { private CaddyDAO dao = new CaddyDAO(); private Caddy caddy = new Caddy(); public void addItem(Item i) { caddy.add(i); } void purchase() { for (Item item : caddy.getItems()) { dao.persist(item); } } public void clearCaddy() { caddy.clear(); } }
Dans une vision sans état, l'aspect peut être écrit comme ceci :
package fr.xebia.aop.aspects; public aspect MonitorManager { pointcut onManager() : execution( fr.xebia.aop.app.CaddyManager.purchase(..)); pointcut onDao() : execution(* fr.xebia.aop.app.CaddyDAO.persist(..)); int managerCalls = 0; int daoCalls = 0; Object around() : onManager() { managerCalls++; try { return proceed(); } finally { System.out .println(thisJoinPointStaticPart.getSignature() + " managerCalls/daoCalls " + managerCalls + "/" + daoCalls); } } before() : onDao() { daoCalls++; } }
Lors de l'exécution du test, on obtient le nombre total des appels à purchase
et à persist
: l'effet 'Aspect Sans Etat'
void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 2/20 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 3/30 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 4/40 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 5/50
Cet aspect ne répond donc pas à notre problématique : obtenir un nombre d'appels 'hiérarchisé' par appel à la méthode de plus haut niveau.
La solution : percflow
Il suffit de modifier légèrement l'aspect avec percflow
pour modifier ses conditions de création:
public aspect MonitorManager percflow(onManager()) { // code identique }
Si un aspect A est défini avec l'instruction percflow(pointcut)
, alors une nouvelle instance de l'aspect est créée dès que le déroulement du code correspond au pointcut passé en paramètre. Dans notre exemple, avec l'ajout de percflow(onManager())
, la JVM va instancier l'aspect à chaque fois que le code exécutera CaddyManager.purchase()
L'exécution du test donne maintenant 10 appels au DAO par appel de la méthode purchase
void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10
L'exemple est ici trivial mais ce type d'aspect placé sur une application J2EE qui aurait mal vieillie peut permettre d'en extraire les principaux chemins d'exécution.
Prix Moyen des articles
Utilisation d'un aspect 'classique'
Le second exemple va permettre de calculer la moyenne du prix des Item du Caddy. Voici le code de l'aspect sans état: à chaque appel à la méthode Cadd.add()
, le prix total et le nombre d'articles est incrémenté. L'exécution de la méthode Caddy.clear()
affiche le résultat et remet à zéro les variables.
public aspect MonitorItems { private int total = 0; private int nb = 0; after(Item i) returning() : execution(* Caddy.add(Item)) && args(i) { total +=i.getPrice(); nb ++; } before(): execution(* Caddy.clear()) { System.out.println(" Caddy items("+nb+"), average price ("+total/nb+")"); total = 0; nb = 0; } }
L'exécution de l'application donne un résultat correct dans sa version mono-threadée. Dans une version multi-threadée (qui se rappoche d'un serveur d'application J2EE), l'application échoue avec un message erroné (50 items au lieu de 10) suivie d'une exception:
Caddy items(50), average price (47) Caused by: java.lang.ArithmeticException: / by zero at fr.xebia.aop.aspects.MonitorItems.ajc$before$fr_xebia_aop_aspects_MonitorItems$2$6dfa1e74(MonitorItems.aj:17) at fr.xebia.aop.app.Caddy.clear(Caddy.java:20) at fr.xebia.aop.app.CaddyManager.clearCaddy(CaddyManager.java:21) at fr.xebia.aop.app.Application.run(Application.java:16)
L'ensemble des threads partagent le même aspect, et donc les mêmes attributs. Un des thread effectue la mise à zéro l'attribut de nb
pendant l'exécution du pointcut before()
et lorsqu'un second thread veut effectuer la moyenne, une division par zéro arrive et provoque l'exception.
La solution : perthis
La solution est d'indiquer qu'il faut créer un nouvelle instance de l'aspect à chaque nouvelle instance de la classe Caddy
. Si un aspect A est défini avec perthis(pointcut)
, alors une nouvelle instance de l'aspect est créée pour chaque objet qui exécute (this
), le pointcut passé en paramètre. L'instruction pertarget(pointcut)
existe également.
public aspect MonitorItems perthis (execution(Caddy.new(..))) { // code identique }
L'exécution du test donne maintenant un résultat correct (le prix moyen des 10 objets) en mode mono-thread et multi-thread.
ApplicationTest.testSameInstanceMT() void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 Caddy items(10), average price (66) void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 Caddy items(10), average price (50) void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 Caddy items(10), average price (49) void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 Caddy items(10), average price (51) void fr.xebia.aop.app.CaddyManager.purchase() managerCalls/daoCalls 1/10 Caddy items(10), average price (51)
Vous retrouverez l'ensemble des sources présentées dans ce billet dans le repository SVN de Xebia France.
Référence :
Commentaire