有一些框架用于动态字节码生成,操作和编织(BCEL,CGLIB,javassist,ASM,MPS)。我想了解它们,但由于我没有太多时间知道所有这些细节,我希望看到一种比较图表,说明一个与其他的优缺点,并解释为什么。
在SO中,我发现了许多类似问题的问题,答案通常说“你可以使用cglib或ASM”,或者“javassist比cglib更好”,或者“BCEL已经老了,正在死”或“ASM是最好的,因为它给出X和Y“。这些答案很有用,但并没有完全回答我想要的范围内的问题,更深入地比较它们并给出每个问题的优点和缺点。
如果您对字节码生成的兴趣只是使用它,则比较图表变得相当简单:
你需要理解字节码吗?
对于javassist:没有
对于所有其他人:是的
当然,即使使用javassist,您在某些时候也可能遇到字节码概念。同样,一些其他库(例如ASM)具有更高级别的api和/或工具支持,以保护您免受许多字节码细节的影响。
然而,真正区分javassist的是包含基本的java编译器。这使得编写复杂的类转换变得非常容易:您只需将一个java片段放在一个String中,然后使用该库将它插入到程序中的特定点。包含的编译器将构建等效的字节码,然后将其插入到现有的类中。
正如我从你在这里得到的答案以及你所看到的问题中得到的答案所说的那样,这些答案并没有以你所陈述的明确方式正式解决问题。您要求进行比较,同时这些答案模糊地说明了您的目标是什么(例如,您需要知道字节码吗?[y / n]),还是太窄。
这个答案是对每个字节码框架的简短分析,并在最后提供了快速比较。
我个人更喜欢Javassist,因为你可以多快地使用它并使用它来构建和操作类。 tutorial简单明了,易于理解。 jar文件是一个小的707KB,所以它很好,便携;使其适用于独立应用程序。
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教程,真正说明了:http://www.geekyarticles.com/2011/08/manipulating-java-class-files-with-bcel.html?m=1
尽管您可以从类中读取信息,并且可以转换类,但库似乎是针对代理而定制的。 tutorial是关于代理的bean,它甚至提到它被“数据访问框架用于生成动态代理对象和拦截字段访问”。尽管如此,我认为没有理由不将它用于字节码操作的更简单目的而不是代理。
长话短说,BCEL缺乏,ByteBuddy很丰富。它使用服务设计模式使用名为ByteBuddy的主类。您创建了ByteBuddy的新实例,这表示您要修改的类。当你完成修改后,你可以用DynamicType
制作一个make()
。
在他们的网站上是一个包含API文档的完整教程。目的似乎是进行相当高级别的修改。在方法方面,官方教程或任何第三方教程中似乎没有任何关于从头开始创建方法的内容,除了委托方法(EDITME,如果你知道在哪里解释)。
他们的教程可以找到here on their website。一些例子可以找到here。
我有自己的字节码库,我正在构建,它将被称为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();
}
}
}
(Qazxswpoi)
从上面的片段中可以暗示,每个VariableInitializer production specification for your convenience.都是不可变的。这使jCLA更安全,线程安全,网络安全且易于使用。系统主要围绕ClassDefinitions作为以高级方式查询有关类的信息的选择对象,并且系统的构建方式使得ClassDefinition与目标类型(如ClassBuilder和ClassFile)之间进行转换。
jCLA对类数据使用分层系统。在底部,您有不可变的ClassDefinition
:类文件的结构或软件表示。然后你就有了不可变的ClassFile
s,它们从ClassFiles转换成一些不那么神秘,更易于管理和有用的程序员正在修改或读取类中的数据,并且与通过ClassDefinition
访问的信息相当。最后,你有可变的java.lang.Class
s。 ClassBuilder是修改或创建类的方法。它允许您可以从构建器当前状态直接创建ClassBuilder
。不需要为每个类创建新的构建器,因为ClassDefinition
方法将清除变量。
(一旦准备好发布,就可以对该库进行分析。)
但直到那时,截至今天:
我仍然建议学习java字节码。它将使调试更容易。
考虑到所有这些分析(目前不包括jCLA),最广泛的框架是ASM,最容易使用的是Javassist,最基本的实现是BCEL,而字节码生成和代理的最高级别是cglib。
ByteBuddy值得自己解释。它很容易像Javassist一样使用,但似乎缺少一些使Javassist很棒的功能,比如从头开始创建方法,因此你需要使用ASM。如果你需要对类进行一些轻量级修改,ByteBuddy是可行的方法,但是为了在保持高级抽象的同时对类进行更高级的修改,Javassist是一个更好的选择。
注意:如果我错过了图书馆,请编辑此答案或在评论中提及。
首先,这一切都取决于你的任务。您想生成新代码还是分析现有字节码以及您可能需要的复杂分析。您还需要投入多少时间来学习Java字节码。您可以将字节码框架分解为提供高级API的框架,允许您在需要了解JVM或使用某些字节码时远离学习低级操作码和JVM内部(例如,javaassist和CGLIB)和低级框架生成工具(ASM和BCEL)。对于analyzis BCEL历史上进化了一些,但ASM提供了一个容易扩展的体面功能。另请注意,ASM可能是唯一一个为Java 7中默认启用的新字节码验证程序所需的STACK_MAP信息提供最高级支持的框架。