动态Java字节码操作框架比较

Vic*_*usa 29 bytecode-manipulation cglib javassist java-bytecode-asm jvm-bytecode

有一些框架用于动态字节码生成,操作和编织(BCEL,CGLIB,javassist,ASM,MPS).我想了解它们,但由于我没有太多时间知道所有这些细节,我希望看到一种比较图表,说明一种与其他的优缺点,以及对为什么.

在SO中,我发现了许多类似问题的问题,答案通常说"你可以使用cglib或ASM",或者"javassist比cglib更好",或者"BCEL已经老了,正在死"或"ASM是最好的,因为它给出X和Y".这些答案很有用,但并没有完全回答我想要的范围内的问题,更深入地比较它们并给出每个问题的优点和缺点.

小智 20

如果您对字节码生成的兴趣只是使用它,则比较图表变得相当简单:

你需要理解字节码吗?

对于javassist:没有

对于所有其他人:是的

当然,即使使用javassist,您在某些时候也可能遇到字节码概念.同样,一些其他库(例如ASM)具有更高级别的api和/或工具支持,以保护您免受许多字节码细节的影响.

然而,真正区分javassist的是包含基本的java编译器.这使得编写复杂的类转换变得非常容易:您只需要将一个java片段放在一个String中,并使用该库将它插入到程序中的特定点.包含的编译器将构建等效的字节码,然后将其插入到现有的类中.


AMD*_*MDG 12

分析字节码库

正如我从你在这里得到的答案以及你所看到的问题中得到的答案所说的那样,这些答案并没有以你所陈述的明确方式正式解决问题.你要求进行比较,同时这些答案含糊地说明了你的目标是什么(例如你需要知道字节码吗?[y/n]),还是太窄了.

这个答案是对每个字节码框架的简短分析,并在最后提供了快速比较.

了Javassist

  • Tiny(javassist.jar(3.21.0)是~707KB/javassist-rel_3_22_0_cr1.zip是~1.5MB)
  • 高(/低)级
  • 直截了当
  • 功能完善
  • 需要最少到没有类文件格式的知识
  • 需要中等的Java指令集知识
  • 最小的学习努力
  • 在单行/多行编译插入字节码方法中有一些怪癖

我个人更喜欢Javassist,因为你可以多快地使用它并使用它来构建和操作类.本教程简单明了,易于理解.jar文件是一个小的707KB,所以它很好,便携; 使其适用于独立应用程序.


ASM

ObjectWeb的ASM是一个非常全面的库,它与构建,生成和加载类没有任何关系.实际上,它甚至还具有带预定义分析器的类分析工具.它被认为是字节码操作的行业标准.这也是我避开它的原因.

当我看到ASM的例子时,它似乎是一个繁琐的任务,它具有修改或加载类所需的行数.甚至一些方法的一些参数似乎有点神秘而且不适合Java.有了类似的东西ACC_PUBLIC,并且null到处都有大量的方法调用,老实说看起来它更适合像C这样的低级语言.为什么不简单地只传递像"public"或enum这样的字符串文字Modifier.PUBLIC?它更友好,更易于使用.不过,这是我的意见.

供参考,这是一个ASM(4.0)教程:https://www.javacodegeeks.com/2012/02/manipulating-java-class-files-with-asm.html


BCEL

从我所看到的,这个库是你的基本类库,可以让你做你需要的一切 - 如果你可以节省几个月或几年.

这是一个BCEL教程,真正说明了它:http://www.geekyarticles.com/2011/08/manipulating-java-class-files-with-bcel.html? m = 1


CGLIB

  • 很小(cglib-3.2.5.jar是295KB/源代码)
  • 取决于ASM
  • 高水平
  • 功能完整(字节码生成)
  • 很少或不需要Java字节码知识
  • 简单易学
  • 神秘图书馆

尽管您可以从类中读取信息,并且可以转换类,但库似乎是针对代理而定制的.本教程是关于代理的bean的,它甚至提到"数据访问框架用于生成动态代理对象和拦截字段访问".尽管如此,我认为没有理由将它用于字节码操作的更简单目的而不是代理.


ByteBuddy

  • 小bin/"巨大"src(通过比较)(byte-buddy-dep-1.8.12.jar是~2.72 MB/1.8.12(zip)是124.537 MB(精确))
  • 取决于ASM
  • 高水平
  • 功能完善
  • 就个人而言,Service Pattern类的一个特殊名称(ByteBuddy.class)
  • 很少或不需要Java字节码知识
  • 简单易学

长话短说,BCEL缺乏,ByteBuddy很丰富.它使用服务设计模式使用名为ByteBuddy的主类.您创建了ByteBuddy的新实例,这表示您要修改的类.当你与你的修改完成后,你可以再作DynamicTypemake().

在他们的网站上是一个包含API文档的完整教程.目的似乎是进行相当高级别的修改.在方法方面,官方教程或任何第三方教程中似乎没有任何关于从头开始创建方法的内容,除了委托方法(EDITME,如果你知道在哪里解释).

他们的教程可以在他们的网站上找到.这里可以找到一些例子.

警告:未来的意见

这是一个很棒的图书馆.我唯一的挑剔是创建一个名为ByteBuddy的类的实例来修改或创建类.就个人而言,我会重新设计这有一个叫做ByteBuddy作为可以被实例化一个静态实用工具类类,并有一个名为ClassData或EmptyClass类,你给一个ByteBuddy实例进行修改,这样就可以从单独的ByteBuddy的功能它修改或创建的数据.我这样做是使用ClassBuilder作为修改除了ClassBuilder ClassDefinitions(不可变的纯数据类)的手段JCLA 副本都从一个类定义的数据进行修改,并构建()方法创建,当你完成了一个新的,不可改变的ClassDefinition通过修改和更新旧类,必须显式调用ClassDefinitionString是完全限定的类名(my.package.MyClass).

因此,而不是:

import jcla.ClassPool;
import jcla.ClassBuilder;
import jcla.ClassDefinition;
import jcla.MethodBuilder;
import jcla.FieldBuilder;

import jcla.jar.JavaArchive;

import jcla.classfile.ClassFile;

import jcla.io.ClassFileOutputStream;

public class JCLADemo {

    public static void main(String... args) {
        // get the class pool for this JVM instance
        ClassPool classes = ClassPool.getLocal();
        // get a class that is loaded in the JVM
        ClassDefinition classDefinition = classes.get("my.package.MyNumberPrinter");
        // create a class builder to modify the class
        ClassBuilder clMyNumberPrinter= new ClassBuilder(classDefinition);

        // create a new method with name printNumber
        MethodBuilder printNumber = new MethodBuilder("printNumber");
        // add access modifiers (use modifiers() for convenience)
        printNumber.modifier(Modifier.PUBLIC);
        // set return type (void)
        printNumber.returns("void");
        // add a parameter (use parameters() for convenience)
        printNumber.parameter("int", "number");
        // set the body of the method (compiled to bytecode)
        // use body(byte[]) or insert(byte[]) for bytecode
        // insert(String) also compiles to bytecode
        printNumber.body("System.out.println(\"the number is: \" + number\");");
        // add the method to the class
        // you can use method(MethodDefinition) or method(MethodBuilder)
        clMyNumberPrinter.method(printNumber.build());

        // add a field to the class
        FieldBuilder HELLO = new FieldBuilder("HELLO");
        // set the modifiers for hello; convenience method example
        HELLO.modifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL);
        // set the type of this field
        HELLO.type("java.lang.String");
        // set the actual value of this field
        // this overloaded method expects a VariableInitializer production
        HELLO.value("\"Hello from \" + getClass().getSimpleName() + \"!\"");

        // add the field to the class (same overloads as clMyNumberPrinter.method())
        clMyNumberPrinter.field(HELLO.build());

        // redefine
        classDefinition = clMyNumberPrinter.build();
        // update the class definition in the JVM's ClassPool
        // (this updates the actual JVM's loaded class)
        classes.update(classDefinition);

        // write to disk
        JavaArchive archive = new JavaArchive("myjar.jar");
        ClassFile classFile = new ClassFile(classDefinition);
        ClassFileOutputStream stream = new ClassFileOutputStream(archive);

        try {
            stream.write(classFile);
        } catch(IOException e) {
            // print to System.out
        } finally {
            stream.close();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

你将会拥有:

import jcla.ClassPool;
import jcla.ClassBuilder;
import jcla.ClassDefinition;
import jcla.MethodBuilder;
import jcla.FieldBuilder;

import jcla.jar.JavaArchive;

import jcla.classfile.ClassFile;

import jcla.io.ClassFileOutputStream;

public class JCLADemo {

    public static void main(String... args) {
        // get the class pool for this JVM instance
        ClassPool classes = ClassPool.getLocal();
        // get a class that is loaded in the JVM
        ClassDefinition classDefinition = classes.get("my.package.MyNumberPrinter");
        // create a class builder to modify the class
        ClassBuilder clMyNumberPrinter= new ClassBuilder(classDefinition);

        // create a new method with name printNumber
        MethodBuilder printNumber = new MethodBuilder("printNumber");
        // add access modifiers (use modifiers() for convenience)
        printNumber.modifier(Modifier.PUBLIC);
        // set return type (void)
        printNumber.returns("void");
        // add a parameter (use parameters() for convenience)
        printNumber.parameter("int", "number");
        // set the body of the method (compiled to bytecode)
        // use body(byte[]) or insert(byte[]) for bytecode
        // insert(String) also compiles to bytecode
        printNumber.body("System.out.println(\"the number is: \" + number\");");
        // add the method to the class
        // you can use method(MethodDefinition) or method(MethodBuilder)
        clMyNumberPrinter.method(printNumber.build());

        // add a field to the class
        FieldBuilder HELLO = new FieldBuilder("HELLO");
        // set the modifiers for hello; convenience method example
        HELLO.modifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL);
        // set the type of this field
        HELLO.type("java.lang.String");
        // set the actual value of this field
        // this overloaded method expects a VariableInitializer production
        HELLO.value("\"Hello from \" + getClass().getSimpleName() + \"!\"");

        // add the field to the class (same overloads as clMyNumberPrinter.method())
        clMyNumberPrinter.field(HELLO.build());

        // redefine
        classDefinition = clMyNumberPrinter.build();
        // update the class definition in the JVM's ClassPool
        // (this updates the actual JVM's loaded class)
        classes.update(classDefinition);

        // write to disk
        JavaArchive archive = new JavaArchive("myjar.jar");
        ClassFile classFile = new ClassFile(classDefinition);
        ClassFileOutputStream stream = new ClassFileOutputStream(archive);

        try {
            stream.write(classFile);
        } catch(IOException e) {
            // print to System.out
        } finally {
            stream.close();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

但那只是我.


Java类助手(jCLA)

我有自己的字节码库,我正在构建,它将被称为Java类助手,或简称jCLA,因为我正在研究的另一个项目,因为Javassist所说的怪癖,但我不会将它发布到GitHub,直到它已经完成,但该项目目前可用于浏览GitHub并提供反馈,因为它目前处于alpha状态,但仍然可以成为一个基本的类库(目前正在编写程序;如果可以,请帮助我!它会很快就会被释放!).

能够从JAR文件读取和写入类文件,以及能够在源代码和类文件之间编译和反编译字节码,这将是非常简单的.

整体使用模式使得使用jCLA变得相当容易,虽然它可能需要一些时间来习惯并且显然与ByteBuddy的方法风格和类修改的方法参数非常相似:

import jcla.ClassPool;
import jcla.ClassBuilder;
import jcla.ClassDefinition;
import jcla.MethodBuilder;
import jcla.FieldBuilder;

import jcla.jar.JavaArchive;

import jcla.classfile.ClassFile;

import jcla.io.ClassFileOutputStream;

public class JCLADemo {

    public static void main(String... args) {
        // get the class pool for this JVM instance
        ClassPool classes = ClassPool.getLocal();
        // get a class that is loaded in the JVM
        ClassDefinition classDefinition = classes.get("my.package.MyNumberPrinter");
        // create a class builder to modify the class
        ClassBuilder clMyNumberPrinter= new ClassBuilder(classDefinition);

        // create a new method with name printNumber
        MethodBuilder printNumber = new MethodBuilder("printNumber");
        // add access modifiers (use modifiers() for convenience)
        printNumber.modifier(Modifier.PUBLIC);
        // set return type (void)
        printNumber.returns("void");
        // add a parameter (use parameters() for convenience)
        printNumber.parameter("int", "number");
        // set the body of the method (compiled to bytecode)
        // use body(byte[]) or insert(byte[]) for bytecode
        // insert(String) also compiles to bytecode
        printNumber.body("System.out.println(\"the number is: \" + number\");");
        // add the method to the class
        // you can use method(MethodDefinition) or method(MethodBuilder)
        clMyNumberPrinter.method(printNumber.build());

        // add a field to the class
        FieldBuilder HELLO = new FieldBuilder("HELLO");
        // set the modifiers for hello; convenience method example
        HELLO.modifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL);
        // set the type of this field
        HELLO.type("java.lang.String");
        // set the actual value of this field
        // this overloaded method expects a VariableInitializer production
        HELLO.value("\"Hello from \" + getClass().getSimpleName() + \"!\"");

        // add the field to the class (same overloads as clMyNumberPrinter.method())
        clMyNumberPrinter.field(HELLO.build());

        // redefine
        classDefinition = clMyNumberPrinter.build();
        // update the class definition in the JVM's ClassPool
        // (this updates the actual JVM's loaded class)
        classes.update(classDefinition);

        // write to disk
        JavaArchive archive = new JavaArchive("myjar.jar");
        ClassFile classFile = new ClassFile(classDefinition);
        ClassFileOutputStream stream = new ClassFileOutputStream(archive);

        try {
            stream.write(classFile);
        } catch(IOException e) {
            // print to System.out
        } finally {
            stream.close();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

(VariableInitializer生产规范为您提供方便.)

从上面的代码片段可以暗示,每个ClassFile都是不可变的.这使jCLA更安全,线程安全,网络安全且易于使用.该系统围绕主要围绕ClassDefinitions作为选择的对象用于查询在一个高层次的方式的一类信息,并且该系统是建立在的ClassDefinition被转换为和从目标类型,如ClassBuilder和ClassFile的这样的方式.

jCLA对类数据使用分层系统.在底部,您有不可变的ClassDefinition:类文件的结构或软件表示.然后你就有了不可变的java.lang.Classs,它们从ClassFiles转换成了一个不那么神秘,更易于管理的东西,对于正在修改或从类中读取数据的程序员来说更有用,并且与通过它访问的信息相当ClassBuilder.最后,你有可变的ClassDefinitions.ClassBuilder是修改或创建类的方法.它允许您可以reset()从构建器的当前状态直接创建.不需要为每个类创建新构建器,因为该ACC_PUBLIC方法将清除变量.

(一旦准备好发布,就可以对该库进行分析.)

但直到那时,截至今天:

  • 小(src:227.704 KB确切,6/2/201)
  • 自给自足(除了Java发布的库之外没有依赖关系)
  • 高水平
  • 没有java字节码或类文件的必需知识(对于第1层API,例如ClassBuilder,ClassDefinition等)
  • 易于学习(如果来自ByteBuddy则更容易)

我仍然建议学习java字节码.它将使调试更容易.


对照

考虑到所有这些分析(不包括JCLA现在)的,最广泛的框架是ASM,最容易使用的是Javassist进行,最简单的实现是BCEL,而最高层的字节码生成和代理是CGLIB.

注意:如果我错过了图书馆,请编辑此答案或在评论中提及.


Eug*_*hov 8

首先,这一切都取决于你的任务.您想生成新代码还是分析现有字节码以及您可能需要的复杂分析.您还需要投入多少时间来学习Java字节码.您可以将字节码框架分解为提供高级API的框架,允许您在需要了解JVM或使用某些字节码时远离学习低级操作码和JVM内部(例如,javaassist和CGLIB)和低级框架生成工具(ASM和BCEL).对于analyzis BCEL历史上进化了一些,但ASM提供了一个容易扩展的体面功能.另请注意,ASM可能是唯一一个为Java 7中默认启用的新字节码验证程序所需的STACK_MAP信息提供最高级支持的框架.