【问题标题】:Javassist: How to add agent to classpath?Javassist:如何将代理添加到类路径?
【发布时间】:2020-11-14 22:20:43
【问题描述】:

我有一个代理,我将其动态加载到正在运行的 Java 应用程序中,该应用程序在连接时会打开一个简单的 Swing JFrame。它还允许将新行追加到该 JFrame 内的 TextArea 中。

我的目标是改变一些方法在代理加载到的应用程序中的工作方式。

public class MyAgent {
    public static void agentmain(String args, Instrumentation instrumentation) {
        UI.openWindow();
        UI.addMessage("Agent loaded: %s", args);
        instrumentation.addTransformer(new MyTransformer());
        instrumentation.redefineClasses(new ClassDefinition(Class.forName("app.TargetClass"), ...));
    }
}

UI 窗口在代理可访问的另一个类中进行管理。加载代理时,它会成功打开一个窗口并附加一条文本消息。

public class UI {
    private static SwingWindow swingWindow;
    
    public static void addMessage(String format, Object... args) {
        System.out.println("UI: " + String.format(format, args));
        swingWindow.appendToTextArea(format, args);
    }
    
    public static void openWindow() {
        try {
            SwingUtilities.invokeAndWait(() -> swingWindow = new SwingWindow());
        }
        catch (Exception e) {}
    }
}

我正在使用 Javassist 在我的转换器中生成字节码。

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, ..., byte[] classBuffer) {
        if (className.equals("app/TargetClass")) {
            UI.addMessage("Now transforming the class that I need!");

            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass targetClass = classPool.get("app.TargetClass");
                CtMethod targetMethod = targetClass.getDeclaredMethod("importantMethod");
                targetMethod.insertBefore("me.domain.agent.ui.UI.addMessage(\"Hello from Javassist!\")");
                byte[] byteCode = targetClass.toBytecode();
                targetClass.detach();
                return byteCode;
            }
            catch (Exception e) {
                UI.addMessage("Couldn't transform the class I needed.");
            }
        }

        return classBuffer;
    }
}

找到了目标类,但字节码没有编译:

UI:转换类 app.TargetClass 失败:[源错误] 没有这样的类:me$domain.agent.ui.UI

但是,UI 类在代理内部:

agent.jar
├── META-INF
└── me.domain.agent
    ├── ui
    │   └── UI.class
    ├── MyTransformer.class
    └── Agent.class

我已经尝试将代理的 ClassLoader 添加到 Javassist 的 ClassPool 中:

classPool.insertClassPath(new LoaderClassPath(Agent.class.getClassLoader()));

但它不起作用。如何将对代理 UI 的调用添加到字节码中?

【问题讨论】:

    标签: java instrumentation javassist javaagents


    【解决方案1】:

    我决定使用 ASM 从字节码调用我的代理 UI。找到课程没有问题。

    这是基于 ASM 的类转换器的外观:

    public class MyTransformer implements ClassFileTransformer {
        public void transformClass(ClassNode classNode) {
            MethodNode methodNode = findMethodNodeOfClass(classNode, "importantMethod", "()V");
            if (methodNode == null) {
                throw new TransformerException("app.TargetClass#importantMethod not found");
            }
    
            AbstractInsnNode firstInsn = findFirstInstruction(methodNode);
            if (firstInsn == null) {
                throw new TransformerException("No instructions in app.TargetClass#importantMethod");
            }
    
            InsnList insnList = new InsnList();
            insnList.add(new LdcInsnNode("Hello from ASM!"));
            insnList.add(new MethodInsnNode(INVOKESTATIC, Type.getInternalName(UI.class), "addMessage", "(Ljava/lang/String;)V"));
            methodNode.instructions.insertBefore(firstInsn, insnList);
        }
    
        @Override
        public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] byteCode) {
            if (className.equals("app/TargetClass")) {
                try {
                    ClassNode classNode = new ClassNode();
                    ClassReader classReader = new ClassReader(byteCode);
                    classReader.accept(classNode, 0);
                    this.transformClass(classNode);
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
                    classNode.accept(classWriter);
                    return classWriter.toByteArray();
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            return byteCode;
        }
    }
    

    这将在app.TargetClass#importantMethod 的第一个非标签指令之前插入静态调用UI.addMessage("Hello from ASM!")

    【讨论】:

    • 只在方法的开头插入一条日志语句就不需要重新计算堆栈图帧。此外,由于您保留原始代码,您可以pass the ClassReader to the ClassWriter’s constructor,以使转换更加高效。
    • @Holger 谢谢!当我在另一个方法中插入(标签、调用静态、返回)指令列表时,我得到了由 VerifyError 引起的 InvokationTargetException,但我无法捕捉到它并看到了 VerifyError 的描述(启用了 COMPUTE_FRAMES)。如何捕捉这些错误以及如何避免 VerifyError?
    • 当您插入invokestatic; return; 时,意味着后续(原始)代码变得无法访问。然后,你需要一个新的框架后返回。当你使用COMPUTE_FRAMES 时,ASM 会发现它无法计算不可达代码的起始帧,只需将整个不可达代码替换为nop。换句话说,当您要有效地替换代码时(即通过插入无条件的return),您实际上也可以替换代码。
    • @Holger 抱歉,我忘了说我还在第一条指令之前插入了ifne label;,在最后一条指令之后插入了label; invokestatic; ldc; areturn;,所以如果条件匹配,则原始代码为避免,而是返回我的值。触发了 VerifyError,因为我使用的是 return; 而不是 areturn;。在areturn;之后应该插入什么样的框架?
    • 由于return 之后的状态应该与方法的初始状态相同(并且中间没有其他帧),您可以简单地在之后插入一个F_SAME 类型的帧return。然后忽略其他参数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多