Exemple d'AOP en Java avec AspectJ


Description

La Programmation Orientée Aspect (Aspect Oriented Program) ou AOP, est un paradigme de programmation qui permet de rajouter du code avant ou après l'appel à une méthode.

Dans cette source nous utiliserons AspectJ, un tisseur d'aspect standard en Java, la dernière version est compatible avec Java 5 à 9.

L'exemple est un classique en AOP, il s'agit d'automatiser l'affichage de logs au début et à la fin de chaque méthode pour tracer les paramètres et les résultats.
Pour tester le code joint, exécuter les commandes mvn package puis java -jar target/aop-1.0.jar

Voici la classe principale, avec un main et une méthode récursive qui calcule la factorielle.

package ccm.kx.aop;

/**
 * @author KX
 */
public class MathUtil {

    public static long factorial(int n) {
        if (n < 0)
            throw new IllegalArgumentException("Can't compute " + n + "!");
        return n > 1 ? n * factorial(n - 1) : 1;
    }

    public static void main(String[] args) {
        System.out.println("3! = " + factorial(3));
        try {
            System.out.println("-2! = " + factorial(-2));
        } catch (RuntimeException e) {
            System.err.println("-2! throws an error");
        }
    }
}

Sans AOP, l'affichage de l'exécution devrait être :

3! = 6
-2! throws an error

Avec AspectJ nous allons rajouter des logs afin d'obtenir l'affichage suivant :

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsBefore
INFOS: void ccm.kx.aop.MathUtil.main(String[]) starts with [[Ljava.lang.String;@1234567] params

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsBefore
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [3] params

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsBefore
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [2] params

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsBefore
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [1] params

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAfterReturning
INFOS: long ccm.kx.aop.MathUtil.factorial(int) finishes with [1] params and returns 1

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAfterReturning
INFOS: long ccm.kx.aop.MathUtil.factorial(int) finishes with [2] params and returns 2

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAfterReturning
INFOS: long ccm.kx.aop.MathUtil.factorial(int) finishes with [3] params and returns 6

3! = 6

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsBefore
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [-2] params

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAfterThrowing
AVERTISSEMENT: long ccm.kx.aop.MathUtil.factorial(int) fails with [-2] params
java.lang.IllegalArgumentException: Can t compute -2!
        at ccm.kx.aop.MathUtil.factorial(MathUtil.java:10)
        at ccm.kx.aop.MathUtil.main(MathUtil.java:17)

-2! throws an error

déc. 15, 2018 1:23:46 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAfterReturning
INFOS: void ccm.kx.aop.MathUtil.main(String[]) finishes with [[Ljava.lang.String;@1234567] params and returns null

Pour faire cela nous allons ajouter une classe avec l'annotation @Aspect qui permettra au tisseur d'aspect de prendre en compte les méthodes @Before @AfterReturning et @AfterThrowing qu'elle contient.

package ccm.kx.aop;

import java.util.*;
import java.util.logging.*;
import org.aspectj.lang.*;
import org.aspectj.lang.annotation.*;

/**
 * @author KX
 */
@Aspect
public class AutomaticLogsAspect {

    private static final Level LEVEL_BEFORE = Level.INFO;
    private static final Level LEVEL_AFTER_RETURNING = Level.INFO;
    private static final Level LEVEL_AFTER_THROWING = Level.WARNING;

    @Pointcut("execution(* *(..))") // all methods
    public void allMethodsPointcut() {
    }

    @Before("allMethodsPointcut()")
    public void automaticLogsAllMethodsBefore(JoinPoint joinPoint) throws Throwable {
        Logger logger = Logger.getLogger(joinPoint.getSourceLocation().getWithinType().getName());
        if (logger.isLoggable(LEVEL_BEFORE)) {
            logger.log(LEVEL_BEFORE, joinPoint.getSignature() + " starts with " + Arrays.toString(joinPoint.getArgs()) + " params");
        }
    }

    @AfterReturning(pointcut = "allMethodsPointcut()", returning = "result")
    public void automaticLogsAllMethodsAfterReturning(JoinPoint joinPoint, Object result) throws Throwable {
        Logger logger = Logger.getLogger(joinPoint.getSourceLocation().getWithinType().getName());
        if (logger.isLoggable(LEVEL_AFTER_RETURNING)) {
            logger.log(LEVEL_AFTER_RETURNING, joinPoint.getSignature() + " finishes with " + Arrays.toString(joinPoint.getArgs()) + " params and returns " + result);
        }
    }

    @AfterThrowing(pointcut = "allMethodsPointcut()", throwing = "exception")
    public void automaticLogsAllMethodsAfterThrowing(JoinPoint joinPoint, Throwable exception) throws Throwable {
        Logger logger = Logger.getLogger(joinPoint.getSourceLocation().getWithinType().getName());
        if (logger.isLoggable(LEVEL_AFTER_THROWING)) {
            logger.log(LEVEL_AFTER_THROWING, joinPoint.getSignature() + " fails with " + Arrays.toString(joinPoint.getArgs()) + " params", exception);
        }
    }

}

Remarque : il n'y a rien à modifier dans la classe MathUtil, le code va venir s'ajouter automatiquement à la compilation grâce à la configuration Maven ci-dessous :

<properties>
   <java.source.version>1.5</java.source.version>
   <aspectj.version>1.9.2</aspectj.version>
</properties>

<dependencies>
   <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>${aspectj.version}</version>
   </dependency>
</dependencies>

<build>
   <plugins>
      <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>aspectj-maven-plugin</artifactId>
         <version>1.11</version>
         <dependencies>
            <dependency>
               <groupId>org.aspectj</groupId>
               <artifactId>aspectjtools</artifactId>
               <version>${aspectj.version}</version>
            </dependency>
         </dependencies>
         <configuration>
            <source>${java.source.version}</source>
            <target>${java.source.version}</target>
            <complianceLevel>${java.source.version}</complianceLevel>
         </configuration>
         <executions>
            <execution>
               <goals>
                  <goal>compile</goal>
               </goals>
            </execution>
         </executions>
      </plugin>
   </plugins>
</build>

Ci-dessous un code obtenu par décompilation après le tissage des aspects, cela permet de comprendre comment va se comporter le programme à l'exécution.

package ccm.kx.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.JoinPoint.StaticPart;
import org.aspectj.lang.Signature;
import org.aspectj.runtime.internal.Conversions;
import org.aspectj.runtime.reflect.Factory;

public class MathUtil {
    private static StaticPart ajc$tjp_0;
    private static StaticPart ajc$tjp_1;

    static {
        ajc$preClinit();
    }

    private static void ajc$preClinit() {
        final Factory factory = new Factory("MathUtil.java", MathUtil.class);
        ajc$tjp_0 = factory.makeSJP("method-execution", factory.makeMethodSig("9", "factorial", "ccm.kx.aop.MathUtil", "int", "n", "", "long"), 8);
        ajc$tjp_1 = factory.makeSJP("method-execution", factory.makeMethodSig("9", "main", "ccm.kx.aop.MathUtil", "[Ljava.lang.String;", "args", "", "void"), 14);
    }

    public static long factorial(final int n) {
        final JoinPoint jp = Factory.makeJP(MathUtil.ajc$tjp_0, null, null, Conversions.intObject(n));
        try {
            AutomaticLogsAspect.aspectOf().automaticLogsAllMethodsBefore(jp);
            if (n < 0) {
                throw new IllegalArgumentException("Can't compute " + n + "!");
            }
            final long n2 = (n > 1) ? (n * factorial(n - 1)) : 1L;
            AutomaticLogsAspect.aspectOf().automaticLogsAllMethodsAfterReturning(jp, Conversions.longObject(n2));
            return n2;
        } catch (Throwable exception) {
            AutomaticLogsAspect.aspectOf().automaticLogsAllMethodsAfterThrowing(jp, exception);
            throw exception;
        }
    }

    public static void main(String args[]) {
        final JoinPoint joinpoint = Factory.makeJP(ajc$tjp_1, null, null, args);
        try {
            AutomaticLogsAspect.aspectOf().automaticLogsAllMethodsBefore(joinpoint);
            System.out.println((new StringBuilder("3! = ")).append(factorial(3)).toString());
            try {
                System.out.println((new StringBuilder("-2! = ")).append(factorial(-2)).toString());
            } catch (RuntimeException _ex) {
                System.err.println("-2! throws an error");
            }
            AutomaticLogsAspect.aspectOf().automaticLogsAllMethodsAfterReturning(joinpoint, null);
            return;
        } catch (Throwable throwable) {
            AutomaticLogsAspect.aspectOf().automaticLogsAllMethodsAfterThrowing(joinpoint, throwable);
            throw throwable;
        }
    }
}

Remarque : plutôt que d'utiliser les 3 annotations @Begin @AfterReturning et @AfterThrowing nous pourrions utiliser l'annotation @Around qui est plus puissante mais génère un bytecode plus complexe (avec des classes intermédiaires), de plus elle est incompatible avec les annotations @Before et @After.

@Around("allMethodsPointcut()") // Do not use with @Before or @After
public Object automaticLogsAllMethodsAround(ProceedingJoinPoint joinPoint) throws Throwable {
    Logger logger = Logger.getLogger(joinPoint.getSourceLocation().getWithinType().getName());
    if (logger.isLoggable(LEVEL_BEFORE)) {
        logger.log(LEVEL_BEFORE, joinPoint.getSignature() + " starts with " + Arrays.toString(joinPoint.getArgs()) + " params");
    }
    Object result = null;
    Throwable throwable = null;
    double time = System.nanoTime();
    try {
        result = joinPoint.proceed();
    } catch (Throwable t) {
        throwable = t;
    } finally {
        time = System.nanoTime() - time;
    }
    if (throwable != null) {
        if (logger.isLoggable(LEVEL_AFTER_THROWING)) {
            logger.log(LEVEL_AFTER_THROWING, joinPoint.getSignature() + " fails with " + Arrays.toString(joinPoint.getArgs()) + " params after " + time / 1000000 + "ms", throwable);
        }
        throw throwable;
    } else {
        if (logger.isLoggable(LEVEL_AFTER_RETURNING)) {
            logger.log(LEVEL_AFTER_RETURNING, joinPoint.getSignature() + " finishes with " + Arrays.toString(joinPoint.getArgs()) + " params after " + time / 1000000 + "ms and returns " + result);
        }
        return result;
    }
}

L'affichage résultat serait alors :

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: void ccm.kx.aop.MathUtil.main(String[]) starts with [[Ljava.lang.String;@1234567] params

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [3] params

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [2] params

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [1] params

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) finishes with [1] params after 0.04ms and returns 1

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) finishes with [2] params after 8.55ms and returns 2

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) finishes with [3] params after 17.00ms and returns 6

3! = 6

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: long ccm.kx.aop.MathUtil.factorial(int) starts with [-2] params

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
AVERTISSEMENT: long ccm.kx.aop.MathUtil.factorial(int) fails with [-2] params after 0.08ms
java.lang.IllegalArgumentException: Can t compute -2!
        at ccm.kx.aop.MathUtil.factorial_aroundBody0(MathUtil.java:10)
        at ccm.kx.aop.MathUtil$AjcClosure1.run(MathUtil.java:1)
        at org.aspectj.runtime.reflect.JoinPointImpl.proceed(JoinPointImpl.java:149)
        at ccm.kx.aop.AutomaticLogsAspect.automaticLogsAllMethodsAround(AutomaticLogsAspect.java:56)
        at ccm.kx.aop.MathUtil.factorial(MathUtil.java:8)
        at ccm.kx.aop.MathUtil.main_aroundBody2(MathUtil.java:17)
        at ccm.kx.aop.MathUtil$AjcClosure3.run(MathUtil.java:1)
        at org.aspectj.runtime.reflect.JoinPointImpl.proceed(JoinPointImpl.java:149)
        at ccm.kx.aop.AutomaticLogsAspect.automaticLogsAllMethodsAround(AutomaticLogsAspect.java:56)
        at ccm.kx.aop.MathUtil.main(MathUtil.java:14)

-2! throws an error

déc. 15, 2018 1:23:45 PM ccm.kx.aop.AutomaticLogsAspect automaticLogsAllMethodsAround
INFOS: void ccm.kx.aop.MathUtil.main(String[]) finishes with [[Ljava.lang.String;@1234567] params after 49.98ms and returns null

NB. La stack de l'exception est un exemple de la complexité introduite par @Around

Remarque : pour les applications Spring on pourra utiliser le framework Spring AOP qui reprends en partie la syntaxe d'AspectJ, mais le tissage des aspects ne serait plus fait à la compilation mais à l'exécution via des proxy dédiés.

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.