ril3y
ril3y

Reputation: 952

How do I instrument methods that are loaded from a specific jar with javassist?

I have a sample jar that I am loading into the the class pool from disk. From there I can easily get access to the methods inside of this class and instrument them as you can see I did with the JsEval method.

However, inside of the Helloworld sampleclass I would like to be able to instrument other library function calls. In this example I am attempting to instrument the eval function from the nashorn scripting engine. However this does not work. I am able to get access to the class (pool.get) just fine and am able to patch the methods for eval. But when I run the SampleClass from cl.run() the methods execute like no code was inserted. I suspect this is something to do with the class loader I am using to execute the Sampleclass but I am stuck. Any ideas on what I am doing wrong here?

public class maventest {

  public static void main(String[] args)
    throws NotFoundException, CannotCompileException, Throwable
  {
    ClassPool pool = ClassPool.getDefault();
    Loader cl = new Loader(pool);

    //pool.importPackage(Test.class.getPackage().getName());
    //Get the Jar from disk. This works and the method is instrumented.
    pool.insertClassPath(
      "Z:\\HelloWorld\\target\\HelloWorld-1.0-SNAPSHOT-jar-with-dependencies.jar"
    );  
    pool.importPackage("com.mycompany.helloworld");
    //pool.appendClassPath();

    CtClass helloworld = pool.get("com.mycompany.helloworld.SampleClass");
    helloworld
      .getDeclaredMethod("JsEval")
      .insertBefore(
        "System.out.println(\"Calling JsEval from within helloworld\\n\");"
      );

    //This does not work.
    //Attempt to instrument the eval function that is called from inside of HelloWorld
    String classToLoad = "jdk.nashorn.api.scripting.NashornScriptEngine";
    String constuctor_name = "eval";
    CtClass nash = pool.get(classToLoad);
    //Multiple eval calls.. Just instrument them all.
    CtMethod[] meths = nash.getDeclaredMethods("eval");
    for (CtMethod m : meths) {
      m.insertBefore(
        "System.out.println(\"Nashorn Scripting Engined eval called.\");"
      );
    }

    //Execute the hello world class with null args
    cl.run("com.mycompany.helloworld.SampleClass", null);
  }

}

Here is the sample code that calls the lib functions I wish to instrument.

public class SampleClass {
  public static void main(String[] args) throws IOException, NotFoundException {
    JsEval("var greeting='hello world'; print(greeting) + greeting");
  }

  private static void JsEval(String js) {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
    try {
      Object result = engine.eval(js);
    }
    catch (ScriptException ex) {
      Logger.getLogger(SampleClass.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

Upvotes: 0

Views: 345

Answers (1)

kriegaex
kriegaex

Reputation: 67297

I know the question is kind of old, but it is still unanswered and I was curious.

The reason this is not working is that getDeclaredMethods("eval") does not search for methods in superclasses, as is documented in the Javadoc. The method you are calling, i.e. the one taking the single String parameter is defined in the parent class AbstractScriptEngine, though, not in NashornScriptEngine. So either you have to change the target class to the class where the method is really defined or you search for methods via getMethod(..) or getMethods(), both of which also return inherited methods. Because getMethods() cannot take a method name parameter but returns all methods and you would have to filter by name again in your instrumentation loop, I suggest you single out the one method you really want to instrument by specifying its exact signature:

String classToLoad = "jdk.nashorn.api.scripting.NashornScriptEngine";
CtClass nash = pool.get(classToLoad);
CtMethod m = nash.getMethod(
  "eval",
  Descriptor.ofMethod(
    pool.get("java.lang.Object"),
    new CtClass[] { pool.get("java.lang.String") }
  )
);
m.insertBefore("System.out.println(\"Nashorn Scripting Engined eval called.\");");

Or if Descriptor.ofMethod(..) is too long-winded for you and you feel comfortable with the descriptor syntax:

String classToLoad = "jdk.nashorn.api.scripting.NashornScriptEngine";
CtClass nash = pool.get(classToLoad);
CtMethod m = nash.getMethod("eval", "(Ljava/lang/String;)Ljava/lang/Object;");
m.insertBefore("System.out.println(\"Nashorn Scripting Engined eval called.\");");

Now your console log output is as expected:

Calling JsEval from within helloworld

Warning: Nashorn engine is planned to be removed from a future JDK release
hello world

Update: Oops, I missed the fact that you are trying to modify a bootstrap class or more generally a class which was already loaded. In that case the transformation has no effect, unless you use Java instrumentation API, i.e. use a ClassFileTransformer which you either integrate into a Java agent (use your favourite web search engine if you don't know what a Java agent is and how to build one) or attach dynamically. I use the tiny byte-buddy-agent library in order to hot-attach it to the running JVM here in this example, just so I can show you the effect.

A super-simplistic version which is not very generic but engineered to only look for the eval(String) method, looks like this:

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import jdk.nashorn.api.scripting.NashornScriptEngine;
import net.bytebuddy.agent.ByteBuddyAgent;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.logging.Level;
import java.util.logging.Logger;

public class MavenTest {

  public static void main(String[] args) throws UnmodifiableClassException {
    Instrumentation instrumentation = ByteBuddyAgent.install();
    instrumentation.addTransformer(new ScriptEngineTransformer());

    Class<?> targetClass = NashornScriptEngine.class;
    // Go up the super class hierarchy, pretending we don't know the exact
    // super class class in which the target method is defined
    while (!targetClass.equals(Object.class)) {
      instrumentation.retransformClasses(targetClass);
      targetClass = targetClass.getSuperclass();
    }

    jsEval("var greeting='hello world'; print(greeting)");
  }

  private static void jsEval(String js) {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
    try {
      engine.eval(js);
    }
    catch (ScriptException ex) {
      Logger.getLogger(MavenTest.class.getName()).log(Level.SEVERE, null, ex);
    }
  }

  static class ScriptEngineTransformer implements ClassFileTransformer {
    private static final ClassPool CLASS_POOL = ClassPool.getDefault();

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
      CtClass targetClass;
      try {
        // Caveat: Do not just use 'classPool.get(className)' because we would miss previous transformations.
        // It is necessary to really parse 'classfileBuffer'.
        targetClass = CLASS_POOL.makeClass(new ByteArrayInputStream(classfileBuffer));
        CtMethod evalMethod = targetClass.getDeclaredMethod("eval", new CtClass[] { CLASS_POOL.get("java.lang.String") });
        targetClass.defrost();
        evalMethod.insertBefore("System.out.println(\"Scripting engine eval(String) called\");");
      }
      catch (Exception e) {
        return null;
      }

      byte[] transformedBytecode;
      try {
        transformedBytecode = targetClass.toBytecode();
      }
      catch (Exception e) {
        e.printStackTrace();
        return null;
      }

      return transformedBytecode;
    }
  }

}

You may have noticed that I renamed a few of your class and method names in order to make them comply to Java standards.

Now the console log is:

Warning: Nashorn engine is planned to be removed from a future JDK release
Scripting engine eval(String) called
hello world

Upvotes: 1

Related Questions