- Blog Xebia France - http://blog.xebia.fr -
Legacy code – gestion des exceptions avec Java Instrumentation
Posted By Nabil Gasri On Vendredi 23 décembre 2011 @ 13:30 In Java / JEE | No Comments
Dans un récent billet, je vous ai présenté JPDA afin de résoudre le problème d’envoi de mail à l’interception des exceptions levées dans une application legacy. Dans cette deuxième partie de la série, je vous propose de résoudre le même problème avec l’API Java Instrumentation.
L’API instrumentation a vu le jour avec Java 5. Elle décrit le mécanisme de manipulation du byte-code utilisé par les outils d’analyse, afin de collecter des données relatives à l’exécution des programmes. Avant la version 5 de Java, les programmes utilisaient d’autres techniques pour analyser les programmes pendant leur exécution, notamment JPDA (présentée dans le précédent article). L’instrumentation est réalisée avec les « agents » Java. Un agent est un programme Java qui se déploie sous forme d’un jar. Il est lancé au démarrage de la JVM en ajoutant l’option « -javaagent:cheminAgent=options » à la ligne de commande (on remarquera au passage la similarité avec le lancement de JPDA). « cheminAgent » est le chemin pointant vers le jar de l’agent et « options » la liste des arguments sous forme d’une seule chaine de caractères.
Pour qu’il soit valide, l’agent doit contenir:
public static void premain(String agentArgs, Instrumentation inst);
ou
public static void premain(String agentArgs);
Cette méthode ressemble à la méthode main d’un programme Java. Elle sera appelée par la JVM au lancement de l’agent en lui fournissant une chaîne de caractères, à analyser par le programme, représentant les options de l’agent et un objet instrumentation. Typiquement, la méthode premain décompose les options et ajoute un ou plusieurs ClassFileTransformer à l’objet instrumentation.
Pour résoudre le problème des exceptions Java levées par l’application, nous allons créer notre propre agent ExceptionHandlerAgent. Celui-ci modifiera le byte-code des classes afin de rajouter le traitement des exceptions. Il implémente donc l’interface ClassFileTransformer. La manipulation du byte-code Java n’est pas chose facile. Elle nécessite une connaissance approfondie du format du byte-code. Heureusement, il existe beaucoup de frameworks (ASM, BCEL, Javassist…) qui rendent cette tâche plus facile. Dans notre exemple, nous allons utiliser la librairie Javassist. Elle nous offre un ensemble de méthodes haut-niveau pour la manipulation du byte-code ainsi qu’un micro-compilateur intégré pour la compilation des petits bouts de code Java. Nous présenterons cette librairie en détail dans un prochain article.
La méthode premain se contente d’ajouter une nouvelle instance d’ExceptionHandlerAgent
public class ExceptionHandlerAgent implements ClassFileTransformer {
public static void premain(String agentArgument, Instrumentation instrumentation) {
instrumentation.addTransformer(new ExceptionHandlerAgent());
}
...
}
La manipulation du byte-code est effectuée dans la méthode transform(), qui est appelée à chaque chargement de classe. Instrumenter les classes de la JVM ne nous intéresse pas et peut même être dangereux. Il en est de même pour les classes des librairies utilisées par l’application. Nous nous intéressons donc uniquement au classes du package fr.xebia.
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.startsWith("fr/xebia")) {
ClassPool pool = ClassPool.getDefault();
CtClass cl = null;
try {
cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
if (!cl.isInterface()) {
CtBehavior[] methods = cl.getDeclaredBehaviors();
for (int i = 0; i < methods.length; i++) {
if (!methods[i].isEmpty()) {
enrichMethod(cl, methods[i]);
}
}
classfileBuffer = cl.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cl != null) {
cl.detach();
}
}
}
return classfileBuffer;
}
Notre but est de traiter les exceptions « catchées » et non « catchées ». Pour les exceptions non catchées, nous pouvons utiliser Thread.setDefaultUncaughtExceptionHandler(eh), qu’il suffit d’insérer au début de la méthode main. Javaassist ne permet pas de compiler les classes internes et anonymes, nous allons faire en sorte que la classe contenant la méthode main implémente l’interface UncaughtExceptionHandler. Il suffit alors de passer une instance de cette classe à la méthode statique setDefaultUncaughtExceptionHandler(). Pour les autres exceptions, nous pouvons modifier les clauses catch afin d’insérer le traitement d’exception au début de la clause. C’est ce qui est fait par la méthode enrichMethod.
private void enrichMethod(CtClass cl, CtBehavior ctBehavior) throws CannotCompileException, NotFoundException {
if (ctBehavior.getName().equals("main")) {
CtClass eh = ClassPool.getDefault().makeClass("java.lang.Thread$UncaughtExceptionHandler");
cl.addInterface(eh);
CtMethod m = CtNewMethod.make("public void uncaughtException(Thread t, Throwable e){System.out.println(\"An Uncaughed exception is thrown\");e.printStackTrace();}",cl);
cl.addMethod(m);
ctBehavior.insertBefore("Thread.setDefaultUncaughtExceptionHandler( new " + cl.getName() + "());");
}
ctBehavior.instrument(new ExprEditor() {
@Override
public void edit(Handler h) throws CannotCompileException {
h.insertBefore("System.out.println(\"Caugh an Exception\");");
}
});
}
Contrairement à la solution avec JPDA, cette solution n’ajoute pas d’overhead à l’exécution de l’application. Cependant, le pré-requis de bien comprendre un minimum le format byte-code avant de pouvoir en profiter reste un handicap pour la démocratisation de cette API, qui reste réservée aux développeurs de frameworks. La simplicité de l’API et la complexité des librairies de manipulation de code font de cette solution une arme à double tranchant.
Article printed from Blog Xebia France: http://blog.xebia.fr
URL to article: http://blog.xebia.fr/2011/12/23/legacy-code-gestion-des-exceptions-avec-java-instrumentation/
Click here to print.