La quête du Graal ou "L'AOP selon Lancelot du Lac"

Le but de ce tutorial est de présenter l'utilisation de l'AOP jshadow. Pour cela nous allons modéliser un problème complétement inspiré de l'excellent livre Spring in Action.

Dans notre application, de vaillants chevaliers partiront dans de nombreuses quêtes tout plus périlleuses les unes que les autres ! Des dragons seront pourfendus et des princesses sauvées ! Les hauts faîts de ces nobles héros seront chantés par leurs fidèles ménestrels.

La version sans AOP

Cette implémentation est très basique, mais cependant rapide à mettre en oeuvre. Nous analyserons par la suite les défaults de conception liés à cette approche que les anglicistes traduiraient par quelque chose se rapprochant de "pretty straighforward".

L'implémentation

On commence tout d'abord par définir un chevalier en tant que POJO :

public class Chevalier
{
    /**
     * Le nom du chevalier
     */
    private String nom;

    public Chevalier(String nom)
    {
        this.nom = nom;
    }

    public String getNom()
    {
        return nom;
    }

    //d'autre propriétés diverses et variées (avec les getters et setters qui vont bien)
    ....
}

Vient ensuite une quête abstraite, avec un logger commons-logging (le ménéstrel). Bon on imaginera que l'implémentation de l'exception QueteException est connue, car sinon on ne s'en sortira pas..


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public abstract class Quete
{
    /**
     * commons-logging
     */
    protected final static Log menestrel = LogFactory.get(Quete.class);

    /**
     * La difficulté de la quête
     */
    private int difficulte;

    /**
     * Le nom de la quête
     */
    private String nom;

    public Quete(String nom, int difficulte)
    {
        this.nom = nom;
        this.difficulte = difficule;
    }

    //d'autre propriétés diverses et variées (avec les getters et setters qui vont bien)
    ....

    /**
     * Un chevalier part en quête
     */
    public abstract void partEnQuete(Chevalier chevalier) throws QueteException;
}

Réalisiont maitenant une implémentation concrété de cette abstraction de quête. Comme nos chevaliers n'ont pas froid aux yeux on les fait partir à la quête du Saint Graal(1)

public class QueteDuSaintGraal extends Quete
{
    public QueteDuSaintGraal()
    {
        //c'est super dur comme quête....
        super("la quête du Saint Graal", Integer.MAX_VALUE);
    }

    public void partEnQuete(Chevalier chevalier) throws QueteException;
    {
        //le menestrel chante le depart de la quête
        if (menestrel.isInfoEnabled()) menestrel.info (String.format("Le chevalier %s part à %s", chevalier.getNom(), getNom());

        //du code métier relatif à la quête
        .....

    }
}

Notre main ressemble alors à :

import org.apache.log4j.BasicConfigurator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

    /**
     * commons-logging
     */
    protected final static Log log = LogFactory.get(MainClass.class);

    public static void main(String[] args)
    {
        //configuration de log4j
         BasicConfigurator.configure();

        Quete queteSaintGraal = new QueteDuSaintGraal();

        Chevalier lancelot = new Chevalier("Lancelot du lac");

        try
        {
             queteSaintGraal.partEnQuete(lancelot);
        }
        catch(QueteException qe)
        {
            log.error(String.format("Le chevalier %s échoue dans %s", lancelot.getNom(), queteSaintGraal.getNom ()),e);
            //... traitement de l'exception
        }
    }

Debriefing

En observant attentivement le code précédent on peut voir que

La version avec l'AOP jshadow

On utilisera dans cette version le Chevalier de la version précédente, car il est plutôt pas trop mal.

On commence par définir l'interface d'une quête :

public interface Quete
{
    /**
     * Un chevalier part en quête
     */
    public void partEnQuete(Chevalier chevalier) throws QueteException;
}
On note immédiatement que l'objet Quete ne dépend PAS de commons logging.

On définit ensuite la quête du Saint Graal
public class QueteDuSaintGraal implements Quete
{
    public void partEnQuete(Chevalier chevalier) throws QueteException;
    {
        //du code métier relatif à la quête
        .....
    }

    ///on imagine des getters et des setters pour diverses propriétées
    ...
}
On note rapidement que la QueteDuSaintGraal ne dépend toujours pas de commons logging et encore moins de jshadow. Une oeil attentif aura même vu qu'elle ne contient QUE du code métier relatif à la quête.

On définit maintenant un aspect dont le rôle sera de tracer les appels à la fonction partEnQuete. Les aspects dans jshadow sont des POJOs avec des méthodes qui possèdent une signature particulière ainsi que des annotations spécifiques.

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.jshadow.aop.Pointcut;
import org.jshadow.aop.Intercept;
import org.jshadow.aop.When ;
import org.jshadow.aop.MethodCall;

public class Menestrel
{
    /**
     * commons-logging
     */
    protected final static Log log = LogFactory.get(Menestrel.class);

    //avec cette annotation on vise la méthode partEnQuete de la classe Quete
     @Pointcut(target = Quete.class, name = "partEnQuete")
    public void changeLouangeAuDepartDeLaQuete(MethodCall call)
    {
        //on récupére l'instance sur laquelle est invoquée la méthode
        Quete quete = (Quete) call.getTarget();

        //on récupére les objets passé en paramêtre
        Chevalier chevalier = (Chevalier) call.getArgs()[0];
        if (log.isInfoEnabled()) String.format ("Le chevalier %s part à %s", chevalier.getNom(), quete.getNom());
    }

    //en rajoutant l'annotation Intercept on indique que l'on se greffe à la fonction cible après l'émission d'une exception
    @Intercept(when=When.EXCEPTION_THROWED)
    @Pointcut(target = Quete.class, name = "partEnQuete")
    public void changeLouangeLorsDeLEchec(MethodCall call)
    {
        //on récupére l'instance sur laquelle est invoquée la méthode
        Quete quete = (Quete) call.getTarget();

        //on récupére les objets passé en paramêtre
        Chevalier chevalier = (Chevalier) call.getArgs()[0];

        //l'object MethodCall contient l'exception qui à été lancer
        log.error(String.format("Le chevalier %s échoue dans %s", chevalier.getNom(), quete.getNom()), call.getThrowable());
    }
}
On voit qu'il n'est pas nécessaire d'appeler une fonction "proceed()" sur l'objet MethodCall. L'appel à l'objet tissé est géré par le framework.
Le nom de la fonction aspect n'a absolument aucune importance. Il suffit qu'elle renvoie void et qu'elle prenne un unique argument de type MethodCall pour être valide. Il est cependant de bon goût de lui donner un nom avec un rapport avec son code...

Tout cela est bien joli, mais doit certainement vous laissez dubitatif. Nous allons donc maintenant voir comment s'articule notre main.

import org.apache.log4j.BasicConfigurator ;

import org.jshadow.aop.Weaver;

    public static void main(String[] args) throws QueteException
    {
        //configuration de log4j
        BasicConfigurator.configure();

        //instanciation d'un tisseur qui contiendra les aspects
        Weaver tisseur = new Weaver();

        //on tisse la toile d'aspect à partir d'une instance de notre aspect de log
        tisseur.weave(new Menestrel());

        //creation d'un objet proxy tissé qui encapsule notre implémentation de la quête du saint graal
        Quete queteSaintGraal = weaver.createObject(Quete.class, new QueteDuSaintGraal());
        Chevalier lancelot = new Chevalier("Lancelot du lac");

        queteSaintGraal.partEnQuete(lancelot);
    }

Debriefing

On note tout de suite que le main dépend :

De plus :

En conclusion

Il est relativement aisé de séparer ainsi les différents métiers de notre application, rendant ainsi la conception plus propre. Avec jshadow la déclaration des points de greffes se fait de façon explicite dans le code source java, ainsi on évite un mapping XML plus qu'ennuyeux (certains oseront dire lourdingue) à mettre en place.

Les limitations de l'AOP avec jshadow sont : Ces limitations ne sont cependant pas forcément génantes car :

La suite au prochain épisode...

Nous n'avons vu qu'une partie du support de l'AOP avec jshadow, mais sachez qu'il est possible de définir 4 points de greffes (joinpoint) (avant, aprés, après en erreur, à chaque étape), d'ordonner l'exécution de différents aspects au même point de greffe.

De plus l'objet MethodCall permet de : Il va de soit que toute ces fonctionnalités sont à utiliser avec précautions, car il devient possible de changer complétement le comportement d'une application.

(1) : plus de détails à http://fr.wikipedia.org/wiki/Graal