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

42

有一些针对动态字节码生成、操作和编织的框架,比如BCEL、CGLIB、javassist、ASM和MPS。我想学习它们,但由于时间有限,我希望看到一种比较图表,以便了解它们之间的优缺点,并解释其原因。

在这里(指Stack Overflow),我发现很多类似的问题,答案通常是“你可以使用cglib或ASM”,或者“javassist比cglib更好”,或者“BCEL已经过时并快要停用” 或者“ASM最好,因为它提供了X和Y”。这些答案很有用,但不能完全回答我所想要的问题范围内的问题,深入比较它们并给出每个框架的优缺点。

3个回答

40

字节码库分析

从您在这里得到的答案和您查看的问题中的答案来看,这些答案并没有正式地以您明确陈述的方式回答问题。您要求进行比较,而这些答案模糊地说明了基于您的目标可能需要什么(例如,您是否需要了解字节码?[是/否]),或者过于狭窄。

本答案对每个字节码框架进行了简短的分析,并在结尾提供了快速比较。

Javassist

  • 轻量级(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",或者一个枚举 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字节码知识
  • 易于学习
  • 神秘的库

尽管您可以从类中读取信息,也可以转换类,但该库似乎专门针对代理。 tutorial 关于代理的所有内容都是关于代理的bean,它甚至提到了它被“数据访问框架用于生成动态代理对象和拦截字段访问”。尽管如此,我仍然看不出为什么您不能将其用于更简单的目的,即代替代理进行字节码操作。


ByteBuddy

  • 相比之下,小bin/"巨大的"src(byte-buddy-dep-1.8.12.jar大约为2.72 MB/1.8.12 (zip)大小为124.537 MB(精确值))
  • 依赖于ASM
  • 高级
  • 功能齐全
  • 个人认为,一个服务模式类的名称很奇怪(ByteBuddy.class)
  • 几乎不需要Java字节码知识
  • 易于学习

长话短说,在BCEL缺乏的地方,ByteBuddy丰富多彩。它使用一个名为ByteBuddy的主类,使用服务设计模式。您可以创建一个新的ByteBuddy实例,这代表了您想要修改的类。当您完成修改后,就可以使用make()创建一个DynamicType

在他们的网站上,有一个包含API文档的完整教程。目的似乎是进行相当高级的修改。关于方法,官方教程或任何第三方教程中似乎没有关于从头创建方法的内容,除了委托方法(如果您知道这是在哪里解释的,请编辑此处)。
他们的教程可以在他们的网站上找到。一些示例可以在这里找到。

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

}

(变量初始化器生产规范供您参考。)

从上面的片段可以看出,每个ClassDefinition都是不可变的。这使得jCLA更安全、线程安全、网络安全和易于使用。该系统主要围绕ClassDefinitions作为高级方式查询有关类信息的对象,并且该系统是以这样一种方式构建的,即ClassDefinition被转换为目标类型(如ClassBuilder和ClassFile)。

jCLA使用分层系统来处理类数据。在底部,您有不可变的ClassFile:一个类文件的结构或软件表示。然后你有不可变的ClassDefinition,它们被转换成比较不加密和更易于管理和使用的东西,对于修改或读取类中的数据的程序员来说是可比较的,类似于通过java.lang.Class访问的信息。最后,您有可变的ClassBuilder。ClassBuilder是如何修改或创建类的。它允许您直接从当前状态的构建器创建ClassDefinition。不需要为每个类创建新的构建器,因为reset()方法会清除变量。

(该库的分析将在发布时提供。)

但在此之前,截至今天:

  • 小巧 (源文件: 227.704 KB 精确, 6/2/2018)
  • 自给自足 (除了Java的已提供库,无需其他依赖)
  • 高级别的
  • 不需要java bytecode或class文件知识 (对于一级API如ClassBuilder, ClassDefinition等)
  • 易学习 (如果从ByteBuddy转来则更容易)

我仍然建议学习java bytecode。这将使调试更容易。


比较

考虑所有这些分析(暂时不包括jCLA),最广泛的框架是ASM,最易于使用的是Javassist,最基本的实现是BCEL,而用于字节码生成和代理的最高级别是cglib。

ByteBuddy值得单独解释。它像Javassist一样易于使用,但似乎缺少一些使Javassist变得出色的功能,例如从头开始创建方法,因此显然需要使用ASM。如果您需要对类进行轻量级修改,则应选择ByteBuddy,但是如果需要在保持高度抽象的同时进行更高级别的类修改,则应选择Javassist。

注意:如果我错过了某个库,请编辑此答案或在评论中提到它。


优秀的比较,但是插入个人意见段落会不必要地分散注意力。另外,你指出ByteBuddy有多好,以及它比BCEL更好,但在比较的最后一节中提到的是BCEL。你是后来添加的ByteBuddy,忘记更新最后一节了吗? - Benny Bottema
1
@BennyBottema 是的,我忘记把它添加到比较部分了。谢谢你指出来。我也会删除我对ByteBuddy的个人看法,而是直接将其作为反馈发给开发人员。这确实可以提供一些关于ByteBuddy如何工作以及我认为它可能更好的小见解,但显然不合适。 - AMDG
请注意,由于我正在使用一种非Java语言开发一个更大的项目,因此jCLA目前被搁置。该项目甚至可能取代jCLA,并且最初是用C编写的。目前,它更像是一个概念验证,使用简单的模式匹配算法,类似于通用符号树的更强大形式的正则表达式。有关详细信息,请查看存储库:https://github.com/AnimaPlacidus/Catholicus-Compiler-Generator - AMDG
由于已经过去了一年...我想指出jCLA可能不会被开发。它已经多年没有得到开发,而我已经转向C和汇编语言的领域,即使这也是暂时的,因为我希望为自己和其他程序员提供一个真正完整的语言,以满足我们所有的需求。考虑到大多数语言迄今为止都是面向业务的,包括C++(参见http://yosefk.com/c++fqa/fqa.html),是时候有一些面向程序员的东西加入游戏了,没有像“安全”这样的错误概念(*咳嗽* Rust 咳嗽)。 - AMDG
@Maowcraft TIL。很高兴我不再需要处理OOP或Java了。最好只使用C制作这种东西,并使用JNI进行接口交互。除非你想要像LWJGL这样的东西,否则更快且更容易操作,除非它自从我上次看到它以来已经被模块化,否则它并不像一个简单的libc接口与外部分配的内存一样轻量级。虽然从未像libffi一样为Java编写过任何东西,但如果存在的话,我会去看看。 - AMDG
显示剩余2条评论

22

如果您只是使用字节码生成技术,那么比较表格就变得简单了:

您需要理解字节码吗?

对于javassist:不需要。

对于其他所有库:需要。

当然,即使使用javassist,在某些时候也可能会涉及到字节码的概念。同样地,其他一些库(如ASM)具有更高级的API和/或工具支持,可屏蔽许多字节码细节。

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


7

首先,一切都取决于你的任务。你是想生成新代码还是分析现有的字节码,以及你需要多复杂的分析。此外,你想花多少时间学习Java字节码。你可以将字节码框架分为高级API提供者(例如javaassist和CGLIB),这样可以避免学习低级操作码和JVM内部,以及需要了解JVM或使用一些字节码生成工具时使用的底层框架(ASM和BCEL)。对于分析来说,BCEL在历史上有所发展,但ASM提供了相当不错的功能,并且易于扩展。此外,请注意,ASM可能是唯一一个提供最先进的STACK_MAP信息支持的框架,该信息在Java 7中默认启用了新的字节码验证器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接