Principes et Implémentation de l'AOP dans Spring

L'Inversion de Contrôle (IoC) et la Programmation Orientée Aspect (AOP) constituent les deux piliers fondamentaux du framework Spring. Tandis que l'IoC gère la création et la liaison des objets, l'AOP permet de modulariser les préoccupations transversales (comme la journalisation, la sécurité ou la gestion des transactions) qui coupent à travers plusiuers modules d'une application. Sans l'AOP, intégrer ces fonctionnalités nécessiterait de modifier chaque méthode concernée, ce qui alourdit le code et en compromet la maintenabilité.

Pour comprendre comment Spring implémente l'AOP, examinons d'abord son mécanisme sous-jacent : la programmation dynamique en Java. Ce mécanisme repose sur l'interface java.lang.reflect.InvocationHandler. Chaque instance de proxy dynamique est associée à un handler. Lorsqu'une méthode est appelée sur l'objet proxy, cet appel est redirigé vers la méthode invoke du handler.


interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

Les paramètres représentent respectivement l'objet proxy, la méthode cible appelée et les arguments transmis à cette méthode.

Prenons un exemple concret avec une exigence d'audit des appels de méthode. Nous créons d'abord un entercepteur qui implémente InvocationHandler pour encapsuler l'ajout de logs avant et après l'exécution de la méthode déléguée.


public class AuditIntercepteur implements InvocationHandler {
    private final Object cible;

    public AuditIntercepteur(Object cible) {
        this.cible = cible;
    }

    private void avantAppel() {
        System.out.println("AUDIT: Début de l'appel.");
    }

    private void apresAppel() {
        System.out.println("AUDIT: Fin de l'appel.");
    }

    @Override
    public Object invoke(Object proxy, Method methode, Object[] arguments) throws Throwable {
        avantAppel();
        Object resultat = methode.invoke(cible, arguments);
        apresAppel();
        return resultat;
    }
}

L'utilisation consiste à créer une instance de notre intercepteur, à lui associer l'objet cible réel, puis à générer un proxy à l'aide de la classe utilitaire java.lang.reflect.Proxy.


// Supposons que 'serviceMetier' est une instance de notre classe de service métier.
AuditIntercepteur intercepteur = new AuditIntercepteur(serviceMetier);
InterfaceService proxyAudit = (InterfaceService) Proxy.newProxyInstance(
    serviceMetier.getClass().getClassLoader(),
    serviceMetier.getClass().getInterfaces(),
    intercepteur
);
// Tout appel sur 'proxyAudit' passera par l'intercepteur.
boolean operationResultante = proxyAudit.executerOperation();

Spring AOP encapsule cette complexité. Illustrons-le avec une configuration type. Définissons d'abord nos couches métier et de données.


// Interface DAO
public interface AccesDonneesUtilisateur {
    boolean creer();
    void supprimer();
}

// Implémentation DAO
public class AccesDonneesUtilisateurImpl implements AccesDonneesUtilisateur {
    public boolean creer() {
        System.out.println("Couche DAO: Enregistrement créé.");
        return true;
    }
    public void supprimer() {
        System.out.println("Couche DAO: Enregistrement supprimé.");
    }
}

// Interface Service
public interface ServiceUtilisateur {
    boolean ajouterUtilisateur();
    void retirerUtilisateur();
}

// Implémentation Service
public class ServiceUtilisateurImpl implements ServiceUtilisateur {
    private AccesDonneesUtilisateur daoUtilisateur;

    public void setDaoUtilisateur(AccesDonneesUtilisateur dao) {
        this.daoUtilisateur = dao;
    }

    public boolean ajouterUtilisateur() {
        System.out.println("Service: Début de l'ajout d'utilisateur.");
        boolean succes = daoUtilisateur.creer();
        System.out.println("Service: Fin de l'ajout d'utilisateur.");
        return succes;
    }

    public void retirerUtilisateur() {
        System.out.println("Service: Début de la suppression d'utilisateur.");
        daoUtilisateur.supprimer();
        System.out.println("Service: Fin de la suppression d'utilisateur.");
    }
}

Créons ensuite une classe d'aspect qui contiendra notre logique de journalisation du temps d'exécution. Cet aspect sera activé via la configuration XML de Spring.


public class AspectTemporisation {
    private static final DateFormat FORMATEUR_DATE = new SimpleDateFormat("HH:mm:ss.SSS");

    public void avantConseil() {
        System.out.println("ASPECT: Exécution à " + FORMATEUR_DATE.format(new Date()));
    }

    public Object autourConseil(ProceedingJoinPoint pointJonction) throws Throwable {
        System.out.println("AVANT autour");
        Object resultat = pointJonction.proceed(); // Poursuite de l'exécution de la méthode cible
        System.out.println("APRÈS autour");
        return resultat;
    }

    public void apresConseil() {
        System.out.println("ASPECT: Fin à " + FORMATEUR_DATE.format(new Date()));
    }

    public void apresRetourConseil() {
        System.out.println("ASPECT: Méthode terminée avec succès.");
    }

    public void apresLancerConseil() {
        System.out.println("ASPECT: La méthode a levé une exception.");
    }
}

La configuration XML ci-dessous déclare les beans, définit un point de coupure (pointcut) ciblant toutes les méthodes du package service, et associe les différents conseils (advices) de notre aspect à ce point de coupure.


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="daoUtilisateur" class="com.exemple.dao.AccesDonneesUtilisateurImpl"/>
    <bean id="serviceUtilisateur" class="com.exemple.service.ServiceUtilisateurImpl">
        <property name="daoUtilisateur" ref="daoUtilisateur"/>
    </bean>

    <!-- Bean de l'aspect -->
    <bean id="aspectTemporisation" class="com.exemple.aspect.AspectTemporisation"/>

    <!-- Configuration de l'AOP -->
    <aop:config>
        <aop:pointcut id="coupureService" expression="execution(* com.exemple.service.*.*(..))"/>
        <aop:aspect id="temporisationAspect" ref="aspectTemporisation">
            <aop:before method="avantConseil" pointcut-ref="coupureService"/>
            <aop:around method="autourConseil" pointcut-ref="coupureService"/>
            <aop:after-returning method="apresRetourConseil" pointcut-ref="coupureService"/>
            <aop:after method="apresConseil" pointcut-ref="coupureService"/>
            <aop:after-throwing method="apresLancerConseil" pointcut-ref="coupureService"/>
        </aop:aspect>
    </aop:config>
</beans>

L'approche annotationnelle offre une alternative plus concise. Le bean de l'aspect est détecté par un scan des composants, et le proxy AOP est activé automatiquement.


<!-- Configuration simplifiée avec annotations -->
<context:component-scan base-package="com.exemple"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>

L'aspect est alors entièrement décrit en annotations.


@Aspect
@Component
public class AspectTemporisationAnnote {
    private static final DateFormat FORMATEUR_DATE = new SimpleDateFormat("HH:mm:ss.SSS");

    @Pointcut("execution(* com.exemple.service.*.*(..))")
    public void coupureService() {}

    @Before("coupureService()")
    public void avantConseil() {
        System.out.println("ASPECT ANN: Début à " + FORMATEUR_DATE.format(new Date()));
    }

    @Around("coupureService()")
    public Object autourConseil(ProceedingJoinPoint pointJonction) throws Throwable {
        System.out.println("AVANT autour");
        Object resultat = pointJonction.proceed();
        System.out.println("APRÈS autour");
        return resultat;
    }
}

L'utilisation de @Pointcut permet de définir une expression réutilisable par plusieurs conseils au sein de la même classe, évitant ainsi la duplication de la définition du point de coupure.

Étiquettes: Spring AOP

Publié le 6 juillet à 05h16