У меня есть образец банки, которую я загружаю в пул классов с диска. Оттуда я могу легко получить доступ к методам внутри этого класса и использовать их, как вы можете видеть, с помощью метода JsEval.

Однако внутри класса примеров Helloworld я хотел бы иметь возможность инструментировать вызовы других библиотечных функций. В этом примере я пытаюсь инструментировать функцию eval из скриптового движка nashorn. Однако это не работает. Я могу нормально получить доступ к классу (pool.get) и исправить методы eval. Но когда я запускаю SampleClass из cl.run (), методы выполняются, как будто код не был вставлен. Я подозреваю, что это как-то связано с загрузчиком классов, который я использую для выполнения Sampleclass, но я застрял. Есть идеи о том, что я здесь делаю неправильно?

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);
    }
  }
}
0
ril3y 5 Ноя 2019 в 22:11

1 ответ

Лучший ответ

Я знаю, что вопрос старый, но до сих пор без ответа, и мне было любопытно.

Причина, по которой это не работает, заключается в том, что getDeclaredMethods("eval") не выполняет поиск методов в суперклассах, как описано в документации Javadoc. Метод, который вы вызываете, то есть метод, принимающий единственный параметр String, определен в родительском классе AbstractScriptEngine, но не в NashornScriptEngine. Таким образом, вы должны либо изменить целевой класс на класс, в котором метод действительно определен, либо искать методы через getMethod(..) или getMethods(), оба из которых также возвращают унаследованные методы. Поскольку getMethods() не может принимать параметр имени метода, но возвращает все методы, и вам придется снова фильтровать по имени в вашем цикле инструментовки, я предлагаю вам выделить один метод, который вы действительно хотите инструментировать, указав его точную сигнатуру:

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

Обновление: К сожалению, я пропустил тот факт, что вы пытаетесь изменить класс начальной загрузки или, в более общем смысле, класс, который уже был загружен. В этом случае преобразование не имеет никакого эффекта, если вы не используете API инструментария Java, т. Е. Используете 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;
    }
  }

}

Возможно, вы заметили, что я переименовал несколько имен ваших классов и методов, чтобы они соответствовали стандартам Java.

Теперь журнал консоли:

Warning: Nashorn engine is planned to be removed from a future JDK release
Scripting engine eval(String) called
hello world
1
kriegaex 11 Июл 2020 в 13:21
Спасибо за помощь, это похоже на то, как я в итоге заставил его работать. Приносим извинения за поздний ответ и еще раз благодарим за помощь!
 – 
ril3y
1 Дек 2020 в 18:24