bcel_BCEL的字节码工程

在本系列的最后三篇文章中,我向您展示了如何使用Javassist框架进行类工作。 这次,我将使用Apache字节码工程库(BCEL)来介绍一种非常不同的字节码操作方法。 BCEL在实际的JVM指令级别上运行,这与Javassist支持的源代码接口不同。 当您确实想控制程序执行的每个步骤时,低级方法使BCEL非常有用,但是对于两种情况都可以使用的情况,使用BCEL的工作也比使用Javassist复杂得多。

我将首先介绍基本的BCEL体系结构,然后将本文的大部分时间用于用BCEL重建我的第一个Javassist类工作示例。 我将快速浏览BCEL软件包中包含的一些工具以及开发人员在BCEL之上构建的一些应用程序。

BCEL班级访问

BCEL为您提供与Javassist相同的基本功能,以检查,编辑和创建Java二进制类。 与BCEL的明显区别是,所有内容都旨在在JVM汇编程序语言级别上工作,而不是Javassist提供的源代码接口。 幕后有一些更深层次的差异,包括在BCEL中使用两个单独的组件层次结构-一个用于检查现有代码,另一个用于创建新代码。 我将假设您熟悉本系列以前的文章中的Javassist(请参阅侧栏不要错过本系列的其余部分 )。 因此,在您开始使用BCEL时,我将集中讨论可能会使您感到困惑的差异。

与Javassist一样,BCEL的类检查方面基本上可以复制通过Reflection API直接在Java平台中提供的功能。 在类工作工具包中,此重复是必要的,因为通常在修改完类之后 ,才希望加载正在使用的类。

BCEL在org.apache.bcel包中提供了一些基本的常量定义,但是除了这些定义之外,所有与检查相关的代码都在org.apache.bcel.classfile包中。 该包中的起点是JavaClass类。 在使用常规Java反射时,此类在使用BCEL访问类信息中的作用与java.lang.Class相同。 JavaClass定义了获取类的字段和方法信息的方法,以及有关超类和接口的结构信息。 与java.lang.Class不同, JavaClass还提供对类的内部信息的访问,包括常量池和属性,以及完整的二进制类表示形式(字节流)。

JavaClass实例通常是通过解析实际的二进制类来创建的。 BCEL提供了org.apache.bcel.Repository类来为您处理解析。 默认情况下,BCEL解析并缓存在JVM类路径中找到的类的表示,从org.apache.bcel.util.Repository实例获取实际的二进制类表示(请注意程序包名称中的差异)。 org.apache.bcel.util.Repository实际上是二进制类表示源的接口。 您可以替换其他路径来查找类文件,或使用其他访问类信息的方式来代替使用类路径的默认源。

换班

除了对类组件的反射式访问之外, org.apache.bcel.classfile.JavaClass还提供了用于更改类的方法。 您可以使用这些方法将任何类组件设置为新值。 但是,它们通常没有很多直接用途,因为包中的其他类不支持以任何合理的方式构造组件的新版本。 而是在org.apache.bcel.generic包中提供了一整套单独的类,它们提供了由org.apache.bcel.classfile类表示的相同组件的可编辑版本。

就像org.apache.bcel.classfile.JavaClass是使用BCEL检查现有类的起点一样, org.apache.bcel.generic.ClassGen是创建新类的起点。 它还可以用于修改现有的类-为了处理这种情况,有一个构造函数采用JavaClass实例,并使用它来初始化ClassGen类信息。 完成类修改后,您可以通过调用返回JavaClass的方法来从ClassGen实例中获得可用的类表示形式,该方法又可以转换为二进制类表示形式。

听起来令人困惑? 我觉得是这样的。 实际上,在这两个程序包之间来回切换是使用BCEL时最尴尬的方面之一。 重复的类结构往往会给您带来麻烦,因此,如果您对BCEL进行了大量工作,则可能值得编写可隐藏其中一些差异的包装器类。 在本文中,我将主要使用org.apache.bcel.generic包类,并避免使用包装器,但这是您需要自己做的事情。

除了ClassGen之外, org.apache.bcel.generic包还定义了一些类来管理各种类组件的构造。 这些结构类包括ConstantPoolGen处理常量池, FieldGenMethodGen的领域和方法, InstructionList用的JVM指令序列工作。 最后, org.apache.bcel.generic包还定义了表示每种JVM指令类型的类。 您可以直接创建这些类的实例,或者在某些情况下,使用org.apache.bcel.generic.InstructionFactory帮助器类。 使用InstructionFactory的优点是,它可以为您处理许多指令构建的簿记细节(包括根据指令需要将项目添加到常量池中)。 在下一节中,您将看到如何使所有这些类一起玩。

BCEL的课堂工作

对于应用BCEL的示例,我将使用与第4部分中的Javassist示例相同的任务-测量执行方法所花费的时间。 我什至将使用与Javassist相同的方法:我将创建原始方法的副本,以使用修改后的名称进行计时,然后将原始方法的主体替换为将计时计算包裹在调用周围的代码。重命名的方法。

选择豚鼠

清单1给出了一个示例方法,我将用于演示: StringBuilder类的buildString方法。 正如我在说4部分 ,这种方法构造了一个String它一再追加一个字符的字符串的结尾创建一个更长的字符串-正好做任何Java性能专家会告诉你不要做任何要求的长度。 因为字符串是不可变的,所以这种方法意味着每次通过循环都会构造一个新的字符串,并从旧字符串中复制数据,并在最后添加一个字符。 最终结果是,此方法用于创建更长的字符串时,将产生越来越多的开销。

清单1.要计时的方法
public class StringBuilder
{
    private String buildString(int length) {
        String result = "";
        for (int i = 0; i < length; i++) {
            result += (char)(i%26 + 'a');
        }
        return result;
    }
    
    public static void main(String[] argv) {
        StringBuilder inst = new StringBuilder();
        for (int i = 0; i < argv.length; i++) {
            String result = inst.buildString(Integer.parseInt(argv[i]));
            System.out.println("Constructed string of length " +
                result.length());
        }
    }
}

清单2显示了与我将使用BCEL进行的类工作更改等效的源代码。 在这里,包装方法只保存当前时间,然后调用重命名的原始方法并打印时间报告,然后再将调用结果返回到原始方法。

清单2.添加到原始方法的时间
public class StringBuilder
{
    private String buildString$impl(int length) {
        String result = "";
        for (int i = 0; i < length; i++) {
            result += (char)(i%26 + 'a');
        }
        return result;
    }
    
    private String buildString(int length) {
        long start = System.currentTimeMillis();
        String result = buildString$impl(length);
        System.out.println("Call to buildString$impl took " +
            (System.currentTimeMillis()-start) + " ms.");
        return result;
    }
    
    public static void main(String[] argv) {
        StringBuilder inst = new StringBuilder();
        for (int i = 0; i < argv.length; i++) {
            String result = inst.buildString(Integer.parseInt(argv[i]));
            System.out.println("Constructed string of length " +
                result.length());
        }
    }
}

编码转换

实现代码以添加方法计时使用了BCEL类访问部分中概述的BCEL API。 在JVM指令级别上工作使代码比在第4部分中的Javassist示例更长,因此在这里,我将一次逐步介绍它,然后再提供完整的实现。 在最后的代码中,所有这些部分将组成一个方法,该方法带有一对参数: cgen ,是org.apache.bcel.generic.ClassGen类的实例,该类已使用要修改的类的现有信息进行了初始化; 和method ,这是我要计时的方法的org.apache.bcel.classfile.Method实例。

清单3包含了transform方法的第一段代码。 从注释中可以看到,第一部分只是初始化我将要使用的基本BCEL组件,其中包括使用要计时的方法的信息初始化新的org.apache.bcel.generic.MethodGen实例。 我为此MethodGen设置了一个空指令列表,稍后将使用实际的计时代码进行填写。 在第二部分中,我从原始方法创建了第二个org.apache.bcel.generic.MethodGen实例,然后从类中删除了原始方法。 在第二个MethodGen实例上,我只是将名称更改为使用“ $ impl”后缀,然后调用getMethod()将可修改的方法信息转换为org.apache.bcel.classfile.Method实例的固定形式。 然后,我使用addMethod()调用将重命名的方法添加到该类中。

清单3.添加拦截方法
// set up the construction tools
InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method, cname, pgen);
wrapgen.setInstructionList(ilist);
    
// rename a copy of the original method
MethodGen methgen = new MethodGen(method, cname, pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());

清单4给出了transform方法的下一段代码。 这里的第一部分计算堆栈上方法调用参数所占用的空间。 这部分是必需的,因为在调用包装方法之前将开始时间存储在堆栈帧中,我需要知道什么偏移量可以用于局部变量(请注意,我可以使用BCEL的局部变量处理来获得相同的效果,但是对于本文我更喜欢一种显式方法)。 该代码的第二部分生成对java.lang.System.currentTimeMillis()的调用,以获取开始时间,并将其保存到堆栈帧中计算出的局部变量offset中。

您可能想知道为什么我在参数大小计算开始时检查该方法是否是静态的,然后将堆栈帧插槽初始化为零(如果不是,则初始化为零)。 此方法与Java语言如何处理方法调用有关。 对于非静态方法,每个调用中的第一个(隐藏)参数都是目标对象的this引用,在计算堆栈帧上完整的参数集大小时需要考虑这一点。

清单4.设置打包的调用
// compute the size of the calling parameters
Type[] types = methgen.getArgumentTypes();
int slot = methgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
    slot += types[i].getSize();
}
    
// save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System",
    "currentTimeMillis", Type.LONG, Type.NO_ARGS, 
    Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createStore(Type.LONG, slot));

清单5显示了生成对包装方法的调用并保存结果(如果有)的代码。 本文的第一部分再次检查该方法是否是静态的。 如果该方法不是静态的,则生成代码以this对象引用加载到堆栈,并将方法调用类型设置为virtual (而不是static )。 然后, for循环生成将所有调用参数值复制到堆栈的代码, createInvoke()方法生成对包装方法的实际调用,最后的if语句将结果值保存到堆栈帧中的另一个局部变量位置(if结果类型不是void )。

清单5.调用包装的方法
// call the wrapped method
int offset = 0;
short invoke = Constants.INVOKESTATIC;
if (!methgen.isStatic()) {
    ilist.append(InstructionFactory.createLoad(Type.OBJECT, 0));
    offset = 1;
    invoke = Constants.INVOKEVIRTUAL;
}
for (int i = 0; i < types.length; i++) {
    Type type = types[i];
    ilist.append(InstructionFactory.createLoad(type, offset));
    offset += type.getSize();
}
Type result = methgen.getReturnType();
ilist.append(ifact.createInvoke(cname, 
    iname, result, types, invoke));
    
// store result for return later
if (result != Type.VOID) {
   ilist.append(InstructionFactory.createStore(result, slot+2));
}

现在进入总结。 清单6生成了用于实际计算自开始时间起经过的毫秒数的代码,并将其打印为格式正确的消息。 这部分看起来很复杂,但是大多数操作实际上只是编写输出消息的各个部分。 它确实说明了我在较早的代码中没有使用的几种操作类型,包括字段访问(对java.lang.System.out访问)和一些不同的指令类型。 如果您将JVM视为基于堆栈的处理器,那么其中的大多数内容应该很容易理解,因此在此不再赘述。

清单6.使用的计算和打印时间
// print time required for method call
ilist.append(ifact.createFieldAccess("java.lang.System", "out", 
    new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
String text = "Call to method " + methgen.getName() + " took ";
ilist.append(new PUSH(pgen, text));
ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
    Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));
ilist.append(ifact.createInvoke("java.lang.System", 
    "currentTimeMillis", Type.LONG, Type.NO_ARGS, 
    Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createLoad(Type.LONG, slot));
ilist.append(InstructionConstants.LSUB);
ilist.append(ifact.createInvoke("java.io.PrintStream", "print", 
    Type.VOID, new Type[] { Type.LONG }, Constants.INVOKEVIRTUAL));
ilist.append(new PUSH(pgen, " ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream", "println", 
    Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));

在生成了时序消息代码之后,清单7剩下的就是包装方法代码的完成,并从包装方法调用中返回保存的结果值(如果有的话),然后完成构造的包装方法的确定。 最后一部分涉及几个步骤。 调用stripAttributes(true)只是告诉BCEL不要为构造的方法生成调试信息,而setMaxStack()setMaxLocals()调用将为该方法计算并设置堆栈使用情况信息。 完成之后,我实际上可以生成该方法的最终版本并将其添加到类中。

清单7.完成包装器
// return result from wrapped method call
if (result != Type.VOID) {
    ilist.append(InstructionFactory.createLoad(result, slot+2));
}
ilist.append(InstructionFactory.createReturn(result));
    
// finalize the constructed method
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();

完整代码

清单8显示了完整的代码(略微重新格式化以适合宽度),包括一个main()方法,该方法采用类文件的名称以及要转换的方法:

清单8.完整的转换代码
public class BCELTiming
{
    private static void addWrapper(ClassGen cgen, Method method) {
        
        // set up the construction tools
        InstructionFactory ifact = new InstructionFactory(cgen);
        InstructionList ilist = new InstructionList();
        ConstantPoolGen pgen = cgen.getConstantPool();
        String cname = cgen.getClassName();
        MethodGen wrapgen = new MethodGen(method, cname, pgen);
        wrapgen.setInstructionList(ilist);
        
        // rename a copy of the original method
        MethodGen methgen = new MethodGen(method, cname, pgen);
        cgen.removeMethod(method);
        String iname = methgen.getName() + "$impl";
        methgen.setName(iname);
        cgen.addMethod(methgen.getMethod());
        Type result = methgen.getReturnType();
        
        // compute the size of the calling parameters
        Type[] types = methgen.getArgumentTypes();
        int slot = methgen.isStatic() ? 0 : 1;
        for (int i = 0; i < types.length; i++) {
            slot += types[i].getSize();
        }
        
        // save time prior to invocation
        ilist.append(ifact.createInvoke("java.lang.System",
            "currentTimeMillis", Type.LONG, Type.NO_ARGS, 
            Constants.INVOKESTATIC));
        ilist.append(InstructionFactory.
            createStore(Type.LONG, slot));
        
        // call the wrapped method
        int offset = 0;
        short invoke = Constants.INVOKESTATIC;
        if (!methgen.isStatic()) {
            ilist.append(InstructionFactory.
                createLoad(Type.OBJECT, 0));
            offset = 1;
            invoke = Constants.INVOKEVIRTUAL;
        }
        for (int i = 0; i < types.length; i++) {
            Type type = types[i];
            ilist.append(InstructionFactory.
                createLoad(type, offset));
            offset += type.getSize();
        }
        ilist.append(ifact.createInvoke(cname, 
            iname, result, types, invoke));
        
        // store result for return later
        if (result != Type.VOID) {
            ilist.append(InstructionFactory.
                createStore(result, slot+2));
        }
        
        // print time required for method call
        ilist.append(ifact.createFieldAccess("java.lang.System",
            "out",  new ObjectType("java.io.PrintStream"),
            Constants.GETSTATIC));
        ilist.append(InstructionConstants.DUP);
        ilist.append(InstructionConstants.DUP);
        String text = "Call to method " + methgen.getName() +
            " took ";
        ilist.append(new PUSH(pgen, text));
        ilist.append(ifact.createInvoke("java.io.PrintStream",
            "print", Type.VOID, new Type[] { Type.STRING },
            Constants.INVOKEVIRTUAL));
        ilist.append(ifact.createInvoke("java.lang.System", 
            "currentTimeMillis", Type.LONG, Type.NO_ARGS, 
            Constants.INVOKESTATIC));
        ilist.append(InstructionFactory.
            createLoad(Type.LONG, slot));
        ilist.append(InstructionConstants.LSUB);
        ilist.append(ifact.createInvoke("java.io.PrintStream",
            "print", Type.VOID, new Type[] { Type.LONG },
            Constants.INVOKEVIRTUAL));
        ilist.append(new PUSH(pgen, " ms."));
        ilist.append(ifact.createInvoke("java.io.PrintStream",
            "println", Type.VOID, new Type[] { Type.STRING },
            Constants.INVOKEVIRTUAL));
            
        // return result from wrapped method call
        if (result != Type.VOID) {
            ilist.append(InstructionFactory.
                createLoad(result, slot+2));
        }
        ilist.append(InstructionFactory.createReturn(result));
        
        // finalize the constructed method
        wrapgen.stripAttributes(true);
        wrapgen.setMaxStack();
        wrapgen.setMaxLocals();
        cgen.addMethod(wrapgen.getMethod());
        ilist.dispose();
    }
    
    public static void main(String[] argv) {
        if (argv.length == 2 && argv[0].endsWith(".class")) {
            try {
            
                JavaClass jclas = new ClassParser(argv[0]).parse();
                ClassGen cgen = new ClassGen(jclas);
                Method[] methods = jclas.getMethods();
                int index;
                for (index = 0; index < methods.length; index++) {
                    if (methods[index].getName().equals(argv[1])) {
                        break;
                    }
                }
                if (index < methods.length) {
                    addWrapper(cgen, methods[index]);
                    FileOutputStream fos =
                        new FileOutputStream(argv[0]);
                    cgen.getJavaClass().dump(fos);
                    fos.close();
                } else {
                    System.err.println("Method " + argv[1] + 
                        " not found in " + argv[0]);
                }
            } catch (IOException ex) {
                ex.printStackTrace(System.err);
            }
            
        } else {
            System.out.println
                ("Usage: BCELTiming class-file method-name");
        }
    }
}

旋转一下

清单9所显示的第一运行结果StringBuilder在未修改形式的程序,然后运行该BCELTiming程序以添加定时信息,最后运行StringBuilder程序它被修改后。 您可以看到StringBuilder在被修改后如何开始报告执行时间,以及由于效率低下的字符串构造代码,其时间如何比构造的字符串的长度快得多。

清单9.运行程序
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000
Constructed string of length 2000
Constructed string of length 4000
Constructed string of length 8000
Constructed string of length 16000

[dennis]$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildString

[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
Call to method buildString$impl took 79 ms.
Constructed string of length 2000
Call to method buildString$impl took 250 ms.
Constructed string of length 4000
Call to method buildString$impl took 879 ms.
Constructed string of length 8000
Call to method buildString$impl took 3875 ms.
Constructed string of length 16000

总结BCEL

BCEL不仅仅是我在本文中显示的基本的课堂工作支持,还有更多。 它还包括一个完整的验证程序实现,以确保根据JVM规范,二进制类是有效的(请参阅org.apache.bcel.verifier.VerifierFactory ),这是一个反汇编程序,可生成框架清晰并链接在一起的JVM级别的二进制视图类,甚至是BCEL程序生成器,它都会输出BCEL程序的源代码以构建您提供的类。 (Javadocs中未包含org.apache.bcel.util.BCELifier类,因此请查看源代码以供使用。此功能很吸引人,但输出可能太神秘了,无法供大多数开发人员使用)。

在我自己使用BCEL的过程中,我发现HTML反汇编程序特别有用。 要进行尝试,只需执行BCEL JAR中的org.apache.bcel.util.Class2HTML类,并以您要反汇编的类文件的路径作为命令行参数即可。 它将在当前目录中生成HTML文件。 例如,在这里,我将分解用于时序示例的StringBuilder类:

[dennis]$ java -cp bcel.jar org.apache.bcel.util.Class2HTML StringBuilder.class
Processing StringBuilder.class...Done.

图1是反汇编程序生成的帧输出的屏幕截图。 在此快照中,右上方的大框显示了添加到StringBuilder类中的定时包装器方法的反汇编。 完整HTML输出包含在下载文件中-如果您想实时观看,只需在浏览器窗口中打开StringBuilder.html文件即可。”

图1.拆卸StringBuilder
拆卸StringBuilder

当前,BCEL可能是Java类工作中使用最广泛的框架。 它列出了网站上使用BCEL的许多其他项目,包括Xalan XSLT编译器,Java编程语言的AspectJ扩展以及几种JDO实现。 许多其他未列出的项目也正在使用BCEL,包括我自己的JiBX XML数据绑定项目。 但是,此后BCEL列出的几个项目已切换到其他库,因此不要将列表的长度作为BCEL受欢迎程度的绝对指南。

BCEL的最大优势在于其对商业友好的Apache许可以及广泛的JVM指令级支持。 这些功能加上其稳定性和使用寿命,使其成为班级应用程序的非常受欢迎的选择。 但是,BCEL似乎并没有为速度或易用性而精心设计。 Javassist为大多数目的提供了一个友好得多的API,至少在我的简单测试中,它具有同等(甚至更好)的速度。 如果您的项目可以使用Mozilla公共许可证(MPL)或GNU较小通用公共许可证(LGPL)来使用软件,则Javassist可能是一个更好的选择(可以使用以下两种许可证之一)。

下一个

既然我已经向您介绍了Javassist和BCEL,那么本系列的下一篇文章将深入探讨比到目前为止所见更为有用的类工作应用程序。 回到第2部分 ,我演示了对方法的反射调用比直接调用要慢得多。 在第8部分中,我将展示如何在运行时使用Javassist和BCEL来用动态生成的代码替换反射调用,从而显着提高性能。 下个月再检查一下Java编程动态,以了解详细信息。


翻译自: https://www.ibm.com/developerworks/java/library/j-dyn0414/index.html

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值