lundi 6 juin 2011

Comment tracer l'appel à une méthode en utilisant l'AOP, Guice et Slf4j

 Je trouve l'annotation @Log de Tapestry, très pratique. Et il est possible de faire la même chose avec Guice.

Il y a un article qui explique les bases de l'AOP avec Guice ici.

Il faut ces dépendances :

  • Guice, pour le moteur d'injection, et qui permet également de faire de l'AOP.
com.google.inject
guice
3.0

  • Slf4j, comme moteur de logging (avec logback en l'occurence comme implémentation).
  org.slf4j
  slf4j-api
  1.6.1
  • AOP, pour l'interface MethodInterceptor
  aopalliance
  aopalliance
  1.0

On commence par créer une annotation : 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}


On créait l'implémentation de l'interception avec la classe LogInterceptor : 

public class LogInterceptor implements MethodInterceptor {
     public Object invoke(MethodInvocation invocation) throws Throwable {

     }
}


Et on fait l'association dans le module Guice :

public class MyModule extends AbstractModule {
    @Override
    protected void configure() {
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(Log.class),
          new LogInterceptor());
    }
}



Une contrainte est que bien évidemment la méthode sur laquelle on rajoute cette annotation doit faire parti d'une classe elle-même injectée par Guice.

Et rajouter l'annotation sur la méthode à tracer : 

public class MyService {
    @Log
    public String getAvailableCode(Code code) {
        switch (code) {
        case FIRST:
            return "my first code";
        case SECOND:
            return "my second code";
        default:
            return "null";
        }
    }
}



Maintenant on passe à l'implémentation de la classe LogInterceptor, on commence par récupérer le logger de la classe de la méthode tracée :


public class LogInterceptor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        Class declaringClass = method.getDeclaringClass();
        Logger logger = LoggerFactory.getLogger(declaringClass);
    }
}



Et avant de tracer quelquechose, on vérifie si le niveau de log est activé, sinon on renvoie directement le résultat : 

if (!logger.isDebugEnabled()) {
    return invocation.proceed();
}


Sinon on trace l'appel est les paramètres passés à la méthode : 

StringBuilder sb = new StringBuilder();
sb.append(method.getName()).append("()").append("[ENTER]");
Object[] arguments = invocation.getArguments();
if (arguments != null && arguments.length != 0) {
    sb.append("[");
    for (int i = 0; i < arguments.length; i++) {
        sb.append("[argument(").append(i).append(")=\"")

          .append(arguments[i]).append("\"]");
    }
    sb.append("]");
}
logger.debug(sb.toString());


Et le résultat de la méthode :

Object proceed = invocation.proceed();
if (profile) {
    stopWatch.stop();
}
sb = new StringBuilder();
sb.append(method.getName()).append("()").append("[EXIT ]");
sb.append("[return=\"").append(proceed).append("\"]");
if (profile) {
    sb.append(" in ").append(stopWatch.getElapsedTime()).append(" ms");
}
logger.debug(sb.toString());
return proceed;


Et dans les logs :

15:48:42.382 [main] DEBUG net.antoine.aopguice.MyService - getAvailableCode()[ENTER][[argument(0)="FIRST"]]
15:48:42.424 [main] DEBUG net.antoine.aopguice.MyService - getAvailableCode()[EXIT ][return="my first code"]

Après le premier problème sur lequel on peut tomber est le cas où la méthode renvoie une exception comme par exemple ici :


@Log
public void throwException() {
    throw new IllegalArgumentException("just for test");
}


Dans ce cas, on catche l'erreur, logue l'exception et on la renvoie :

Exception exception = null;
Object proceed = null;
try {
    proceed = invocation.proceed();
} catch (Exception e) {
    exception = e;
} finally {
    sb = new StringBuilder();
    sb.append(method.getName()).append("()").append("[EXIT ]");
    if (exception == null) {
        sb.append("[return=\"").append(proceed).append("\"]");
    } else {
        sb.append("[throws exception :").append(exception.toString())
          .append("]");
    }
    logger.debug(sb.toString());
    if (exception != null) {
        throw exception;
    }
}
return proceed;


Ce qui donne dans les logs :

16:01:04.594 [main] DEBUG net.antoine.aopguice.MyService - throwException()[ENTER]
16:01:04.603 [main] DEBUG net.antoine.aopguice.MyService - throwException()[EXIT ][throws exception :java.lang.IllegalArgumentException: just for test]

Maintenant un élément intéressant serait de tracer le temps d'éxécution de la méthode, on va utiliser pour celà Perf4j : 
  
  org.perf4j
  perf4j
  0.9.14

Idéalement, on aimerait que ce soit une option désactivé par défaut puisque inutile dans la plupart des cas. On rajoute une attribut à l'annotation : 


public @interface Log {
    /**
    * Active timing execution if set to true, false by default.
    */
    boolean profile() default false;
}


Et on va récupérer ce paramètre dans la méthode d'interception : 

Log annotation = method.getAnnotation(Log.class);
boolean profile = false;
if (annotation != null) {
    profile = annotation.profile();
}
StopWatch stopWatch = null;
if (profile) {
    stopWatch = new LoggingStopWatch(method.getName());
}

if (profile) {
    stopWatch.start();
}
Object proceed = null; 

try { 
    proceed = invocation.proceed(); 
} catch (Exception e) { 
    exception = e; 
} finally { 
    if (profile) { 
        stopWatch.stop(); 
    }
    //etc
    if (profile) {
        sb.append(" in ").append(stopWatch.getElapsedTime())
          .append(" ms");
    }
    //etc
}



Et avec cette méthode :  

@Log(profile = true)
public void traceTime() throws InterruptedException {
    Random random = new Random();
    int nextInt = random.nextInt(100);
    Thread.sleep(50 + nextInt);
}


On obtient ces logs : 

16:22:11.759 [main] DEBUG net.antoine.aopguice.MyService - traceTime()[ENTER]
start[1307283731759] time[117] tag[traceTime]
16:22:11.877 [main] DEBUG net.antoine.aopguice.MyService - traceTime()[EXIT ][return="null"] in 117 ms

Après on pourrait rajouter un attribut dans l'annotation pour pouvoir loguer en info, on rajouter un message à rajouter dans les traces. N'hésitez pas à rajouter un commentaire si vous avez d'autres idées.

mercredi 2 décembre 2009

Cirque Éloize - Rain, cette semaine au Colisée de Roubaix


"Rain", c'est le nom du spectacle en représentation ce soir au Colisée de Roubaix. Ce spectacle, c'est encore son metteur en scène, Daniele Finzi Pasca qui en parlera le mieux :
J'ai grandi dans une monde où le souvenir était fait d'instants, d'impossibles immobilités et d'images volées au temps. Moi, dans mes spectacles, j'ai simplement mis en mouvement ces images.
C'est le genre de spectacle qui ne vous laisse pas le souvenir d'une histoire, mais le souvenir des émotions qu'on y a ressenti : de la poésie, de la beauté. C'est un mélange de la virtuosité du Cirque du soleil, de la narration d'un Ghibli, et de l'imagination d'un film de Jeunet.
A voir donc absolument, le Cirque Éloize est encore en représentation, ce jeudi et vendredi 3,4 décembre. Sachant que le colisée fait des places "Dernière Minute" à 8 euro, et qu'il restait des places ce soir. Ce serait une injustice que les 2 prochaines dates ne soient pas complètes : pitié précipitez-vous !!

Flickr - Mario Groleau - cc