如何在运行时发出和执行 Java 字节码?

Posted

技术标签:

【中文标题】如何在运行时发出和执行 Java 字节码?【英文标题】:How to emit and execute Java bytecode at runtime? 【发布时间】:2011-04-30 07:12:43 【问题描述】:

我正在用 Java 为具有一些脚本功能的特定领域语言编写解释器。我已经实现了一个解析器,现在需要做一个后端。为此,我正在考虑编写自己的解释器(使用抽象语法树或使用一些自定义字节码)或目标 JVM(在运行时发出并执行 Java 字节码)。

在这方面有更多经验的人能否说一下以 JVM 为目标的方法的可行性以及您推荐使用哪些库来发出 Java 字节码?

【问题讨论】:

是否已经设计/指定了 DSL?还是您正在与解析器和解释器一起开发 DSL? @Stobor: 语言已经指定并且解析器已经写好了。 【参考方案1】:

这是一个使用ObjectWeb ASM(我推荐的库)制作的“hello world”:

package hello;

import java.lang.reflect.Method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class HelloWorldASM implements Opcodes 
    public static byte[] compile(String name) 
        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "hello/HelloWorld", null,
                "java/lang/Object", null);

        cw.visitSource("HelloWorld.java", null);

        
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(4, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                    "()V");
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lhello/HelloWorld;", null, l0, l1,
                    0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        
        
            mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main",
                    "([Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(7, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            mv.visitLdcInsn(String.format("Hello, %s!", name));
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
                    "(Ljava/lang/String;)V");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(8, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l2,
                    0);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        
        cw.visitEnd();

        return cw.toByteArray();
    

    public static class DynamicClassLoader extends ClassLoader 
        public Class<?> define(String className, byte[] bytecode) 
            return super.defineClass(className, bytecode, 0, bytecode.length);
        
    ;

    public static void main(String[] args) throws Exception 
        DynamicClassLoader loader = new DynamicClassLoader();
        Class<?> helloWorldClass = loader.define("hello.HelloWorld",
                compile("Test"));
        Method method = helloWorldClass.getMethod("main", String[].class);
        method.invoke(null, (Object) new String[] );
    

要生成代码,我发现Bytecode Outline for Eclipse 插件非常有用。虽然您可以像这样使用 ASMifier(包含在 ASM 中):

ClassReader cr = new ClassReader(new FileInputStream("HelloWorld.class"));
cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0);

在运行时,如果您需要为创建的类获取Class 对象,您可以通过扩展类加载器并发布(例如通过另一种方法)defineClass 方法并提供类来加载您的类作为一个字节数组,如示例中所列。

您还可以使用接口处理创建的类,如下例所示:

package hello;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class HelloWorldPlugin implements Opcodes 
    public static interface Plugin 
        void sayHello(String name);
    

    public static byte[] compile() 

        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "hello/MyClass", null,
                "java/lang/Object",
                new String[]  "hello/HelloWorldPlugin$Plugin" );

        cw.visitInnerClass("hello/HelloWorldPlugin$Plugin",
                "hello/HelloWorldPlugin", "Plugin", ACC_PUBLIC + ACC_STATIC
                        + ACC_ABSTRACT + ACC_INTERFACE);

        
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(5, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                    "()V");
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lhello/MyClass;", null, l0, l1, 0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        
        
            mv = cw.visitMethod(ACC_PUBLIC, "sayHello",
                    "(Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(9, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitLdcInsn("Hello, ");
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder",
                    "<init>", "(Ljava/lang/String;)V");
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
                    "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
                    "toString", "()Ljava/lang/String;");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
                    "(Ljava/lang/String;)V");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(10, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("this", "Lhello/MyClass;", null, l0, l2, 0);
            mv.visitLocalVariable("name", "Ljava/lang/String;", null, l0, l2,
                    1);
            mv.visitMaxs(4, 2);
            mv.visitEnd();
        
        cw.visitEnd();

        return cw.toByteArray();
    

    public static class DynamicClassLoader extends ClassLoader 
        public DynamicClassLoader(ClassLoader parent) 
            super(parent);
        

        public Class<?> define(String className, byte[] bytecode) 
            return super.defineClass(className, bytecode, 0, bytecode.length);
        
    ;

    public static void main(String[] args) throws Exception 
        DynamicClassLoader loader = new DynamicClassLoader(Thread
                .currentThread().getContextClassLoader());
        Class<?> helloWorldClass = loader.define("hello.MyClass", compile());
        Plugin plugin = (Plugin) helloWorldClass.newInstance();
        plugin.sayHello("Test");
    

玩得开心。

PS:如果不够清楚,我可以在代码中添加 cmets。我没有,因为答案已经太长了。不过,我给你的建议是尝试调试它。

【讨论】:

是的,我知道。不客气。但我不会把功劳归于原作者安德烈·洛斯库托夫。【参考方案2】:

我可以建议你看看这些库:

CGLIB BCEL asm Javassist

【讨论】:

感谢您的链接。你用过这些吗?你会推荐哪一个? 我使用过 Javassist 和 CGLIB,但用于比您需要的更简单的任务。需要注意的一点是,BCEL 使用的是 CGLIB,因此它可能具有更大的功能。【参考方案3】:

查看Jetbrains MPS。由为我们带来 IDEA 的人打造。

【讨论】:

【参考方案4】:

从另一个角度,我问你是否考虑使用XText。这旨在使您能够创建 DSL、具有代码完成功能的代码编辑器、编译器、代码生成器等。我认为这真的很酷,并且有一个很好的documentation。值得一看。您可以基于它为您的 DSL 轻松创建编译器。

【讨论】:

感谢您的链接。看来使用 XText 我将不得不重新实现我想避免的前端。 是的,我明白了。这取决于您的团队规模和公司规模。我认为有时有必要完全放弃专有的东西来使用别人也使用的东西。

以上是关于如何在运行时发出和执行 Java 字节码?的主要内容,如果未能解决你的问题,请参考以下文章

JVM原理探索字节码指令集调用执行流程分析(语法分析篇)

虚拟机字节码操作引擎-----基于栈的字节码解释引擎

[jvm解析系列][十一]字节码执行之栈帧,你的字节码是如何运行的?

深入理解Java虚拟机06--虚拟机字节码执行引擎

Java虚拟机--虚拟机字节码执行引擎

深入JAVA虚拟机之字节码执行引擎