如何使用 javassist 检测从特定 jar 加载的方法?

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

我有一个示例 jar,我正在从磁盘加载到 class 池中。从那里我可以很容易地访问这个 class 中的方法并像你看到的那样对它们进行检测,就像我用 JsEval 方法所做的那样。

但是,在 Helloworld 示例中class 我希望能够检测其他库函数调用。在这个例子中,我试图检测来自 nashorn 脚本引擎的 eval 函数。但是,这不起作用。我能够很好地访问 class (pool.get),并且能够修补 eval 的方法。但是当我 运行 来自 cl.run() 的 SampleClass 时,这些方法的执行就像没有插入代码一样。我怀疑这与我用来执行 Sampleclass 的 class 加载程序有关,但我被卡住了。关于我在这里做错了什么有什么想法吗?

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);
  }

}

这是调用我希望检测的库函数的示例代码。

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);
    }
  }
}

我知道这个问题有点老了,但仍然没有答案,我很好奇。

这不起作用的原因是 getDeclaredMethods("eval") 不在 superclasses 中搜索方法,如 Javadoc 中所述。您正在调用的方法,即采用单个 String 参数的方法在父 class AbstractScriptEngine 中定义,但不在 NashornScriptEngine 中。因此,要么您必须将目标 class 更改为真正定义方法的 class,要么您通过 getMethod(..)getMethods() 搜索方法,这两者也是 return继承的方法。因为 getMethods() 不能采用方法名称参数,但 return 是所有方法,并且您必须在检测循环中再次按名称进行过滤,所以我建议您通过指定来挑出您真正想要检测的方法它的确切签名:

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.\");");

或者,如果 Descriptor.ofMethod(..) 对您来说太啰嗦,而您对描述符语法感到满意:

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.\");");

现在您的控制台日志输出符合预期:

Calling JsEval from within helloworld

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

更新: 哎呀,我错过了你试图修改 bootstrap class 或更普遍的 class 的事实已经加载。在那种情况下,转换没有效果,除非您使用 Java 检测 API,即使用 ClassFileTransformer,您可以将其集成到 Java 代理中(使用您最喜欢的网络搜索如果您不知道 Java 代理是什么以及如何构建代理)或动态附加。在此示例中,我使用微型 byte-buddy-agent 库将其热附加到 运行 JVM,这样我就可以向您展示效果。

一个超级简单的版本,它不是很通用,但被设计成只寻找 eval(String) 方法,看起来像这样:

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;
    }
  }

}

您可能已经注意到我重命名了您的一些 class 和方法名称,以使其符合 Java 标准。

现在控制台日志是:

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