Exemple/Tutoriel: Utiliser un agent java …
Même s’il n’existe pas beaucoup de documentation sur cette possibilité offerte dans Java depuis le JDK 1.5, il existe de bon articles comme cette introduction de « Soft qui peut » ainsi que celle de Xebia.
En condensé, un JavaAgent est déployé sous forme de jar (contenant un Manifest spécifique: Premain-Class: my.package.MyAgent) et utilisé via une option donnée à la JVM (-javaagent:path/mayagent.jar).Voir la documentation Sun pour plus de détails sur les options.
Un Agent Java est un composant qui s’interconnecte entre la machine virtuelle Java et le logiciel. Il est appelé à chaque chargement d’une classe. Il peut donc écouter tous les appels. Son utilisation la plus simple et la plus courante est le profiling, logging …
Il peut également être utilisé pour faire de l’AOP (Programmation orientée Aspect).
Ici je vous propose une implémentation d’un agent permettant de mettre en œuvre un petit outil de monitoring en utilisant l’AOP. L’objectif étant de pouvoir monitorer le temps passé sur n’importe quelle partie de votre code.
Nous lancerons l’agent via la commande:
java -cp classes:javassist.jar -javaagent:agent.jar=debug=false|class.filters=.*TestAgent|method.filters=.* net.dromard.instrumentation.TestAgent
La classe TestAgent
package net.dromard.instrumentation; public class TestAgent extends Thread { public static void main(final String[] args) { new TestAgent().start(); } @Override public void run() { try { System.out.println("Sleeping 3s"); Thread.sleep(3000); System.out.println("Test Agent DONE"); } catch (InterruptedException e) { e.printStackTrace(); } } }
La classe Agent
package net.dromard.instrumentation; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.instrument.Instrumentation; /** * Simple Time based */ public class Agent { private static final String METHOD_FILTERS = "method.filters"; private static final String CLASS_FILTERS = "class.filters"; private static final String DEBUG = "debug"; private static final String LOG_FILE = "log.file"; /** The agent parameter: debug. */ private static boolean debug = false; /** The agent parameter: classFilters. */ private static String classFilters = null; /** The agent parameter: methodFilters. */ private static String methodFilters = null; /** The agent parameter: agentLogFile. */ private static String agentLogFile = null; /** * Called when starting a JVM with option -javaagent:[jarpath/jarname.jar] {@link java.lang.instrument.Instrumentation}. * WARNING: to work the jar must contains a MANIFEST with the option: Premain-Class. * @param agentArgument The arguments given to agent * @param instrumentation The instrumentation */ public static void premain(final String agentArgument, final Instrumentation instrumentation) { String[] args = agentArgument.split("\\|"); for (String arg : args) { if (arg.startsWith(CLASS_FILTERS)) { classFilters = arg.substring(Agent.CLASS_FILTERS.length() + 1); } else if (arg.startsWith(METHOD_FILTERS)) { methodFilters = arg.substring(Agent.METHOD_FILTERS.length() + 1); } else if (arg.startsWith(DEBUG)) { debug = ("true".equals(arg.substring(DEBUG.length() + 1).toLowerCase())); } else if (arg.startsWith(LOG_FILE)) { agentLogFile = arg.substring(LOG_FILE.length() + 1); // Delete old log file if (agentLogFile != null) { File f = new File(agentLogFile); if (f.exists()) { f.delete(); } } } } if (classFilters != null && methodFilters != null) { if (Agent.debug) { Agent.log("[DEBUG] Starting agent with class filter: " + classFilters + " and methodFilter: " + methodFilters); } // Register Instrumentation agent instrumentation.addTransformer(new TimeElapsedTransformer(classFilters, methodFilters, debug)); } else { System.out.println("[WARNING] filters are empty, use options:"); System.out.println("\t" + CLASS_FILTERS + "=.*MyClassToInstrument"); System.out.println("\t" + METHOD_FILTERS + "=MyMethodToInstrument"); System.out.println("\t" + DEBUG + "=true (OPTIONAL)"); } } /** * Used if the agent is added at runtime {@link java.lang.instrument.Instrumentation}. * WARNING: to work the jar must contains a MANIFEST with the option: Agent-Class. * @param agentArgument The arguments given to agent * @param instrumentation The instrumentation */ public static void agentmain(final String agentArgument, final Instrumentation instrumentation) { premain(agentArgument, instrumentation); } /** * A simple logger method. * @param message The message to be log. */ public static void log(final String message) { if (agentLogFile != null) { try { FileWriter fw = new FileWriter(agentLogFile, true); fw.write(message + "\n"); fw.close(); } catch (IOException e) { e.printStackTrace(); System.out.println(message); } } else { System.out.println(message); } } }
La classe TimeElapsedTransformer
package net.dromard.instrumentation; import javassist.CannotCompileException; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; /** * Simple Time based */ public class TimeElapsedTransformer extends AOPTransformer { /** * Constructor. */ public TimeElapsedTransformer(final String classFilters, final String methodFilters, final boolean debug) { super(classFilters, methodFilters, debug); } /** * @param clazz The JavAssist class * @param method The JavAssist method * @throws NotFoundException If method return type is not found !! * @throws CannotCompileException If instrumentation is not valid !! */ @Override protected void instrumentMethod(final CtClass clazz, final CtMethod method) throws NotFoundException, CannotCompileException { // get the method information (throws exception if method with // given name is not declared directly by this class, returns // arbitrary choice if more than one with the given name) String mname = method.getName(); String longName = method.getLongName(); // rename old method to synthetic name, then duplicate the // method with original name for use as interceptor String nname = mname + "$impl"; method.setName(nname); CtMethod mnew = CtNewMethod.copy(method, mname, clazz, null); // start the body text generation by saving the start time // to a local variable, then call the timed method; the // actual code generated needs to depend on whether the // timed method returns a value String type = method.getReturnType().getName(); StringBuffer body = new StringBuffer(); body.append("{\n"); body.append(" long startTime = System.nanoTime();\n"); body.append("try {\n"); if (!"void".equals(type)) { body.append(type + " result = "); } body.append(nname + "($$);\n"); if (!"void".equals(type)) { body.append("return result;\n"); } body.append("} finally {"); // finish body text generation with call to print the timing // information, and return saved value (if not void) body.append("long endTime = System.nanoTime();\n"); body.append("long delta = endTime - startTime;\n"); body.append("net.dromard.instrumentation.Agent.log(\"[Agent] Method:" + longName + " completed in \" + delta + \" nano secs\");\n"); body.append(" }\n"); body.append("}"); // Replace the body of the intercepter method with generated code block and add it to class mnew.setBody(body.toString()); clazz.addMethod(mnew); } }
La classe AOPTransformer
package net.dromard.instrumentation; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; /** * Abstract class that filter class and methods to be instrumented then call implementation. */ public abstract class AOPTransformer implements ClassFileTransformer { /** The agent parameter: debug. */ private final boolean debug; /** The agent parameter: classFilters. */ private final String classFilters; /** The agent parameter: methodFilters. */ private final String methodFilters; /** * Constructor. */ public AOPTransformer(final String classFilters, final String methodFilters, final boolean debug) { this.classFilters = classFilters; this.methodFilters = methodFilters; this.debug = debug; } /* (non-Javadoc) * @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[]) */ public final byte[] transform(final ClassLoader loader, final String className, final Class classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException { return filterClass(className, classBeingRedefined, classfileBuffer); } /** * Filtered class, than filter methods, than instrument them. * @param className The class name to be transformed * @param classBeingRedefined The class to be transformed * @param classfileBuffer The binary class to be transformed * @return The newly instrumented classfileBuffer. * @return */ private final byte[] filterClass(final String className, final Class classBeingRedefined, final byte[] classfileBuffer) { if (classFilters != null) { for (String filter : classFilters.split(",")) { if (className.matches(filter + ".*")) { try { return filterMethod(className, classBeingRedefined, classfileBuffer); } catch (NotFoundException e) { e.printStackTrace(); } } else if (debug) { Agent.log("[SKIPPED] " + className); } } } return null; } /** * Filtered methods, than instrument them. * @param className The class name to be transformed * @param classBeingRedefined The class to be transformed * @param classfileBuffer The binary class to be transformed * @return The newly instrumented classfileBuffer. * @throws NotFoundException If not found. */ private final byte[] filterMethod(final String className, final Class classBeingRedefined, final byte[] classfileBuffer) throws NotFoundException { if (debug) { Agent.log("[DEBUG] Adding profiling info ... for " + className); } ClassPool pool = ClassPool.getDefault(); CtClass cl = null; byte[] returnBytes = null; try { cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer)); if (cl.isInterface() == false) { CtMethod[] methods = cl.getDeclaredMethods(); for (CtMethod method : methods) { if (!method.isEmpty()) { if (methodFilters != null) { for (String filter : methodFilters.split(",")) { if (method.getName().matches(filter)) { instrumentMethod(cl, method); } } } else { instrumentMethod(cl, method); } } } returnBytes = cl.toBytecode(); } } catch (Exception e) { e.printStackTrace(); System.err.println("Could not instrument " + className + ", exception : " + e.getMessage()); } finally { if (cl != null) { cl.detach(); } } return returnBytes; } /** * Instrument given method of given class. * @param clazz The JavAssist class * @param method The JavAssist method * @throws NotFoundException If method return type is not found !! * @throws CannotCompileException If instrumentation is not valid !! */ protected abstract void instrumentMethod(final CtClass clazz, final CtMethod method) throws NotFoundException, CannotCompileException; }
Le manifest:
Manifest-Version: 1.0 Ant-Version: Apache Ant 1.7.0 Created-By: 1.5.0_12 (Sun Microsystems Inc.) Premain-Class: net.dromard.instrumentation.Agent Agent-Class: net.dromard.instrumentation.Agent ProjectName: Dromard Instrumentation Agent ProjectVersion: 0.0.1 Class-Path: javassist.jar
Conculsion
Nous avons vue ici comment maitre en œuvre l’instrumentation de code en utilisant la librairie Javassist. Cet exemple est relativement simple dans ses capacités, mais laisse entrevoir une panoplie intéressante de possibilités.
Et vous, avez vous déjà utilisé l’instrumentation de code ? Pour quel besoin ?