0

I'm trying create a generic Java Agent to instrument any Java application's methods. I've followed this tutorial https://javapapers.com/core-java/java-instrumentation/ and created a java agent. The java agent is supposed to look for a particular class ( I'm restricting it to one class now since it's not working for me) Once the class is found, I'm using JavaAssist API to add a local variable to the beginning of each method and capture the current time. In the end of the method I'd like to simply print the time it took for the method to execute. (Pretty much following all the typical examples about Java agent.

I run my test application ( a web server using Vert.x ) with --javaagent flag pointing to the Java agent jar file I created ( the code is down below).

This works just fine for methods that either don't have return value and no parameters or return/take a primitive type.

However when a method is returning or taking a parameter that is an object from a another class (that has not been loaded yet I think) I get a CannotCompileException exception with the message that that class which is in the parameters list or in the return statement is not found. For example the instrumentation for this method works:

@Override
public void start() throws Exception {
    logger.debug("started thread {}", Thread.currentThread().getName());
    for (int port : ports) {
        HttpServer httpServer = getVertx().createHttpServer(httpServerOptions);
        Router router = setupRoutes();
        httpServer.requestHandler(router::accept);
        logger.info("Listening on port {}", port);
        httpServer.listen(port);
    }
}

However for this method that returns io.vertx.ext.web.Router:

private Router setupRoutes() {
    Router router = Router.router(getVertx());
    router.get(STATUS_PATH).handler(this::statusHandler);
    router.route().handler(BodyHandler.create());
    router.post().handler(this::handleBidRequest);
    router.put().handler(this::handleBidRequest);
    router.get(SLEEP_CONTROLLER_PATH).handler(this::sleepControllerHandler);
    return router;
}

I get an exception and the output of my java agent is :

Instrumenting method rubiconproject.com.WebServerVerticle.setupRoutes()
Could not instrument method setupRoutes error: cannot find io.vertx.ext.web.Router

This the code for my java agent:

import java.lang.instrument.Instrumentation;
import transformers.TimeMeasuringTransformer;

public class TimeCapturerAgent {
public static void premain(String agentArgs, Instrumentation inst) {
    System.out.println(TimeCapturerAgent.class.getCanonicalName() + " is loaded...... ");
    inst.addTransformer(new TimeMeasuringTransformer());
}}

package transformers;

import java.io.ByteArrayInputStream;
import java.io.IOException;
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;

public class TimeMeasuringTransformer implements ClassFileTransformer {

public TimeMeasuringTransformer() {
    System.out.println("TimeMeasuringTransformer added ");
}

@Override
public byte[] transform(ClassLoader loader,
        String className,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) throws IllegalClassFormatException {


    if(className != null && className.contains("WebServerVerticle")) {
        System.out.println("Instrumenting class " + className);
        return modifyClass(classfileBuffer);
    }

    return null;

}

private byte[] modifyClass(byte[] originalClassfileBuffer)  {
    ClassPool classPool = ClassPool.getDefault();
    CtClass compiledClass;
    try {

        compiledClass = classPool.makeClass(new ByteArrayInputStream(originalClassfileBuffer));
        System.out.println("Created new compiled Class " + compiledClass.getName());

    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

    instrumentMethods(compiledClass);

    byte [] newClassByteCode =  createNewClassByteArray(compiledClass);
    compiledClass.detach();
    return newClassByteCode;
}

private byte[] createNewClassByteArray(CtClass compiledClass) {
    byte[] newClassByteArray = null;
    try {
        newClassByteArray = compiledClass.toBytecode();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (CannotCompileException e) {
        e.printStackTrace();
    } finally {
        return newClassByteArray;
    }
}

private void instrumentMethods(CtClass compiledClass) {
    CtMethod[] methods = compiledClass.getDeclaredMethods();
    System.out.println("Class has " + methods.length + " methods");
    for (CtMethod method : methods) {
        try {
            System.out.println("Instrumenting method " + method.getLongName());
            method.addLocalVariable("startTime", CtClass.longType);
            method.insertBefore("startTime = System.nanoTime();");
            method.insertAfter("System.out.println(\"Execution Duration "
                    + "(nano sec): \"+ (System.nanoTime() - startTime) );");
        } catch (CannotCompileException e) {
            System.out.println("Could not instrument method " + method.getName()+" error: " + e.getMessage());
            continue;
        }
    }
}}
Michael P
  • 2,017
  • 3
  • 25
  • 33
  • The idea to do Instrumentation with something that works like a source code compiler is a dead end, despite that Javassist advertises it. A compiler may need to access the classes that are not available due to the fact that you are just instrumenting them. In contrast, inserting these few instructions on the bytecode level is trivial, but of course, requires understanding of the bytecode. But once you have that understanding, you likely realize how insane the Instrumentation with source code approach is. – Holger Jan 22 '18 at 10:02
  • Can you print `e.getCause()` in exception message. It may tell you why the transformation is failing. Also, if you can dump the `newClassByteArray` to a file and use javap to see the bytecodes, it may help in debugging the issue. – Ashutosh Jan 22 '18 at 10:26
  • @Holger, I was surprised that the entire method had to be recompiled, I though the rest of the byte code could stay the way it is, and only the added lines had to be compiled, and those lines don't need any additional classes. If you think the idea is dead, what are you proposing as an alternative? Should I not use Java Assist all together? – Michael P Jan 22 '18 at 21:26
  • @Ashutosh e.getCause() just prints the name of the class that the method is missing. – Michael P Jan 23 '18 at 05:17
  • However I followed your suggestion and I dumped the new class binary into a class file and inspected with javap, I do not see all the methods in the class file! In fact I see only the ones that I could later inspect. I don't understand why. (I see only one method and the 2 constructors) I'm not sure why I do not see them in the new class. – Michael P Jan 23 '18 at 05:18
  • I am wondering if you need to add a classpath to the `ClassPool` using `insertClassPath()` API to be able to locate `io.vertx.ext.web.Router` in case it is not on system classpath. – Ashutosh Jan 23 '18 at 06:10
  • A class file only contains the declared methods, not the inherited methods. Regarding the class path, you should use the `ClassLoader` that has been passed to the `transform` method, but keep in mind that all classes that Javassist tries to load, will go through your class file transformer, potentially creating a circular dependency. Javassist will not recompile the entire method, that’s not even possible without the source code. But the machinery supports a wide variety of code constructs that need the surrounding context, the fact that you’re not using all of them doesn’t matter. – Holger Jan 23 '18 at 07:49
  • @Holger, thanks for the reply. The methods that are missing are declared methods of my class. That's why it's so weird that I don't see them. And I'm talking about those methods missing from the compiledClass = classPool.makeClass(new ByteArrayInputStream(originalClassfileBuffer)); I put the compiledClass content into a class file. And that's missing methods. – Michael P Jan 23 '18 at 19:14
  • It’s hard to analyze without further context. Did you specify the `-p` option to `javap`? – Holger Jan 24 '18 at 10:44

0 Answers0