注意:本文章主要依据BCEL官方手册进行阐述,大部分内容都是从该手册直接翻译过来的,并做了一定的简化,同时还参考了《深入理解Java虚拟机》(周志明著)。笔者在下面阐述的时候也会给出相应的章节,如果读者有不清楚的地方请参考BCEL官方手册或是BCEL API,以及《深入理解Java虚拟机》。
BCEL是什么?
相信搜索到这篇文章的读者应该知道BCEL是啥,不过还是简要提一下吧:BCEL(Byte Code Engineering Library)原本是Apache Jakarta的一个子项目,目前已成为Apache Commons的一个子项目,主要用于分析、创建、操纵Java class文件。
为了说明如何使用BCEL,需要简要介绍一下JVM结构和class文件的结构。
JVM结构
JVM是Java提供平台无关性的基础,它是一台抽象的机器,主要任务是装载class文件并且执行其中的字节码。Java虚拟机的体系结构如上图所示。这里仅给出JVM中比较重要的部分的说明,详细介绍请参考《深入理解Java虚拟机》(周志明著)和《Java Virtual Machiine Specification》(参见Oracle网站)。
- 类装载子系统:每个Java虚拟机都有一个类装载子系统,它根据给定的全限定名来装入类或接口。
- 运行时数据区:用于组织需要内存来存储的东西,如,字节码,程序创建的对象,传递给方法的参数,返回值,局部变量,运算的中间结果,……
- Java堆:存放运行时创建的对象实例,以及对象间的引用关系
- Java栈:其实更准确的称呼应该是“Java Virtual Machine Stack”,每当一个方法被执行的时候,都会创建一个Java栈帧(stack frame),栈帧中保存着该方法的操作数栈、局部变量表(local variable table)、动态链接、方法出口等信息,栈帧创建后就会放到Java栈上,执行完毕后抛弃该栈帧。我们平时所说的方法内的局部变量保存在“栈”上其实就是指Java栈。
- 方法区:保存被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。注意方法区中包含有一个运行区常量池,class文件中的常量池(将在后面讲解)中的内容将在类加载后存放到这个运行时常量池中。
- 程序计数器(PC计数器):PC寄存器的值总是指示下一条将被执行的指令
- 本地方法栈:用于本地方法的执行,本地方法参考《Core Java》中有关JNI的阐述。
Java class文件格式
注:要完整的说明class文件的结构是个非常费劲的事情,JVM指令系统更是如此。限于篇幅这里不再详细阐述,仅给出简要的说明,详细说明请参考《深入理解Java虚拟机》和《Java Virtual Machiine Specification》
class文件的格式大体如下图所示
class文件的header部分包含有一个“魔数”(magic number)0xCAFEBABE(其实是”cafe baby”,此乃当年Java开发团队的幽默),然后是版本号,再之后是常量池,访问权限标志位,当前类(注意,这里的类是指类和接口,是比较泛泛的概念,下文同)所实现的接口列表,当前类所包含的域、方法,最后是当前类的属性(可以包含用户自定义的数据)。
由于需要在运行时动态解析的类、域、方法的符号引用实际都是以字符串常量的形式保存在常量池中,所以常量池实际上占了class文件的很大比重,一般是60%,而byte code一般只占12%。
常量池中一般包含有如下类型的常量:References to methods, fields and classes, strings, integers, floats, longs, and doubles.(以上参考 2.1 java class file format)
注意,class文件中,非抽象方法包含有一个属性“Code”,包含有栈帧所需的最大深度、局部变量的个数、byte code指令的数组。当在方法执行过程中发生异常时,JVM会查找异常表(table of exception handlers),该表标记了handler,也就是哪些异常处理代码负责处理具体哪个部分代码所抛出的异常,如果没有合适的handler,异常将会传递给该方法(callee)的调用者(caller)。handler的信息保存在“Code”属性中。(2.3 Method Code)
实例分析
(这一部分参考2.6 Code Example)
首先结合上述的内容,分析以下Java代码编译后所得到的class文件。(将就着看吧,wordpress里一行行调代码缩进实在太麻烦,就没怎么调整~~)
import java.io.*; public class Factorial { private static BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); public static final int fac(int n) { return (n == 0)? 1 : n * fac(n - 1); } public static final int readInt() { int n = 4711; try { System.out.print("Please enter a number> "); n = Integer.parseInt(in.readLine()); } catch(IOException e1) { System.err.println(e1); } catch(NumberFormatException e2) { System.err.println(e2); } return n; } public static void main(String[] argv) { int n = readInt(); System.out.println("Factorial of " + n + " is " + fac(n)); } }
fac()方法对应的字节码如下:
0: iload_0 1: ifne #8 4: iconst_1 5: goto #16 8: iload_0 9: iload_0 10: iconst_1 11: isub 12: invokestatic Factorial.fac (I)I (12) 15: imul 16: ireturn LocalVariable(start_pc = 0, length = 16, index = 0:int n)
每条指令的功能这里就不细说,需要注意的是,JVM是一种基于栈的虚拟机,任何操作都需要先将操作数放入栈帧中的操作数栈,然后调用相关方法,操作的结果也会自动的压入操作数栈。在本例中,fac()方法采用了递归的方式计算n!,第12行体现了这一点,在JVM执行到底12行时,会创建一个新的栈帧,以执行新的fac()方法调用。此外,ireturn会将结果返回给当前方法(callee)的调用者(caller),压入到caller栈帧中操作数栈的栈顶。
readInt()方法的字节码如下:
0: sipush 4711 3: istore_0 4: getstatic java.lang.System.out Ljava/io/PrintStream; 7: ldc "Please enter a number> " 9: invokevirtual java.io.PrintStream.print (Ljava/lang/String;)V 12: getstatic Factorial.in Ljava/io/BufferedReader; 15: invokevirtual java.io.BufferedReader.readLine ()Ljava/lang/String; 18: invokestatic java.lang.Integer.parseInt (Ljava/lang/String;)I 21: istore_0 22: goto #44 25: astore_1 26: getstatic java.lang.System.err Ljava/io/PrintStream; 29: aload_1 30: invokevirtual java.io.PrintStream.println (Ljava/lang/Object;)V 33: goto #44 36: astore_1 37: getstatic java.lang.System.err Ljava/io/PrintStream; 40: aload_1 41: invokevirtual java.io.PrintStream.println (Ljava/lang/Object;)V 44: iload_0 45: ireturn Exception handler(s) = From To Handler Type 4 22 25 java.io.IOException(6) 4 22 36 NumberFormatException(10)
这个例子需要说明的是,平时我们调用System.out.println()打印输出时,虽然只是传入要打印的内容,但在字节码层次上,首先需要将System类的静态变量out压入到栈顶,然后将需要打印的内容(这里是字符串“”Please enter a number>”)压入栈,接着调用println()方法。之所以需要将out压入栈,是因为实例方法调用的时候,都会将该实力对象的引用隐式地作为第一个参数(Instance methods always implicitly take an instance reference as their first argument)。
另外,Java代码中的异常处理代码,try语句不会产生任何实际代码,只是规定了exception handler在哪些代码发生异常时会被调用。
BCEL API
BCEL API主要分为以下3个部分(3. The BCEL API):
- bcel.classfile.*:主要用于查看class文件的结构(尤其是在没有源代码的情况下),一般不用作byte code的修改。
- bcel.generic.*:动态产生或是修改class文件,可以插入代码、从class文件中剔除无用代码、实现一个Java编译器的代码生成器后端。
- 其他:代码示例,使用工具
bcel.classfile.*的类设计如下图:
通过下面的代码,即可访问一个class文件的JavaClass对象,然后通过这个对象的get/set方法,即可访问这个class文件的各个部分:
JavaClass clazz = Repository.lookupClass("java.lang.String");
比如说,下面的printCode()就能输出String类的所有方法:
System.out.println(clazz); printCode(clazz.getMethods()); ... public static void printCode(Method[] methods) { for(int i=0; i < methods.length; i++) { System.out.println(methods[i]); Code code = methods[i].getCode(); if(code != null) // Non-abstract method System.out.println(code); } }
3.3 ClassGen
bcel.generic.*的类设计如下图:
要创建一个类,需要使用ClassGen,而要给这个类添加其他部分,则需要使用上图中给出的其他类。在给出具体实例之前,还需要先确定如何表示类型——域需要指明其类型,方法则需要给出参数及其返回值的类型。
3.3.1 Types
使用BCEL的Type类用法如下:
Type return_type = Type.VOID; Type[] arg_types = new Type[] { new ArrayType(Type.STRING, 1) }
3.3.2 Generic fields and methods
域需要使用FieldGen来创建,并且需要指定其访问权限。方法则需要添加可能需要的异常、local variables、exception handler。由于包含有byte code的地址的引用,这两者被称之为instruction targeter,这些targeter包含有updateTarget()方法,用于更新目标地址,这个地方是采用Observer模式实现的。一般方法(不是抽象方法)会指向一个instruction list(包含instruction对象),对byte code地址的引用由instruction对象处理,当instruction list更新时,instruction targeter也会被更新。
每个方法的maximum stack size和maximum number of local variables可以手动或是采用setMaxStack()和setMaxLocals()方法自动设置。
3.3.3 Instructions
instruction包含有 opcode(有时叫做tag),字节长度,在byte code中的偏移量。有些指令是不可变的(比如operators),InstructionConstants用于提供预定义的常量供用户使用(flyweight模式)。
指令分类:按照type hierarchy of instruction classes(附录有)。也可以按照所实现的接口分类。
重要的指令:branch instructions,比如goto,使得这些指令也可以看做instruction targeter。
所有指令都可以通过accept(Visitor v)方法来访问(visitor模式)。(3.3.3 Instructions)
3.3.4 Instruction lists
instruction list:对instructions的引用不是由直接指向instruction的指针,而是指向instruction handle的指针。这使得添加、插入、删除byte code很方便,同时允许重用不可变的指令对象(flyweight object)。由于使用符号引用,具体的bytecode偏移量的计算直到finalization才计算即可,即用户停止操作bytecode的时候。下文将instruction handle和instruction视为同一概念。instruction handle通过addAttribute()方法可以包含有用户自定义数据。
append操作:
instructionlist的append方法会返回一个instruction handle,用作branch instruction的目标地址。
InstructionList il = new InstructionList(); ... GOTO g = new GOTO(null); il.append(g); ... // Use immutable fly-weight object InstructionHandle ih = il.append(InstructionConstants.ACONST_NULL); g.setTarget(ih);
insert操作:
指令可以插入到现有list的任意位置,需要插入到给定instruction handle的前边,insert方法会返回一个instruction handle用于可能的异常处理的目标地址。
InstructionHandle start = il.insert(insertion_point, InstructionConstants.NOP); ... mg.addExceptionHandler(start, end, handler, "java.io.IOException");
delete操作:
需要给出指定范围,该范围内的指令将被删除并释放资源(dispose)。但instruction targeter仍引用着将要被删除的指令时,delete方法可能抛出TargetLostException。用户需要自己处理这些异常。参见附录的窥孔优化的例子。
try { il.delete(first, last); } catch(TargetLostException e) { InstructionHandle[] targets = e.getTargets(); for(int i=0; i < targets.length; i++) { InstructionTargeter[] targeters = targets[i].getTargeters(); for(int j=0; j < targeters.length; j++) targeters[j].updateTarget(targets[i], new_target); } };
finalize操作:
当instruction list已经操作完毕、打算生成纯粹的字节码时,所有符号引用被映射为真实的byte code offset,这一操作由getByteCode()完成(默认由Method.getMethod())。生成字节码后用户应该调用dispose()一遍使得这些instrution handle能别重用。
InstructionList il = new InstructionList(); ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object", "<generated>", ACC_PUBLIC | ACC_SUPER, null); MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC, Type.VOID, new Type[] { new ArrayType(Type.STRING, 1) }, new String[] { "argv" }, "main", "HelloWorld", il, cp); ... cg.addMethod(mg.getMethod()); il.dispose(); // Reuse instruction handles of list
3.3.6 Instruction Factories
为了简化某些指令的创建,用户可以使用InstructionFactory类创建指令(提供了很多有用的方法用于创建指令),也可以使用compound instruction:当产生byte code,某些“模式”出现的比较频繁,比如算术或是比较运算,可以用一个compound instruction(一个只有单一的一个getInstructionList()方法的接口),这可以用于任何位置,尤其是添加操作。
例子:将操作数压入栈,用PUSH可以自动产生合适的指令
InstructionFactory f = new InstructionFactory(class_gen); InstructionList il = new InstructionList(); ... il.append(new PUSH(cp, "Hello, world")); il.append(new PUSH(cp, 4711)); ... il.append(f.createPrintln("Hello World")); ... il.append(f.createReturn(type));
regular expressions
可以用正则表达式来搜索特定模式的代码。——org.apache.bcel.util.InstructionFinder的search()方法,找到后会返回一个迭代器,其他的约束条件可以通过code constraint对象来表达。
CodeConstraint constraint = new CodeConstraint() { public boolean checkCode(InstructionHandle[] match) { IfInstruction if1 = (IfInstruction)match[0].getInstruction(); GOTO g = (GOTO)match[2].getInstruction(); return (if1.getTarget() == match[3]) && (g.getTarget() == match[4]); } }; InstructionFinder f = new InstructionFinder(il); String pat = "IfInstruction ICONST_0 GOTO ICONST_1 NOP(IFEQ|IFNE)"; for(Iterator e = f.search(pat, constraint); e.hasNext(); ) { InstructionHandle[] match = (InstructionHandle[])e.next();; ... match[0].setTarget(match[5].getTarget()); // Update target ... try { il.delete(match[1], match[5]); } catch(TargetLostException ex) { ... } }
例子:优化boolean表达式
用BCEL生成HelloWorld
最后,举个简单的例子,说明具体如何使用BCEL直接生成class文件。比如说,我们现在要使用BCEL生成一个HelloWorld.class文件,实现如下代码的功能:
try { // BCEL Appendix A example // ClassGen(String class_name, String super_class_name, String // file_name, int access_flags, String[] interfaces) ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object", "<generated>", ACC_PUBLIC | ACC_SUPER, null); ConstantPoolGen cp = cg.getConstantPool(); InstructionList il = new InstructionList(); // create main method // MethodGen(int access_flags, Type return_type, Type[] arg_types, // String[] arg_names, String method_name, String class_name, // InstructionList il, ConstantPoolGen cp) MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC, Type.VOID, new Type[] { new ArrayType(Type.STRING, 1) }, new String[] { "argv" }, "main", "HelloWorld", il, cp); InstructionFactory factory = new InstructionFactory(cg); // define some often used types ObjectType i_stream = new ObjectType("java.io.InputStream"); ObjectType p_stream = new ObjectType("java.io.PrintStream"); //%% //BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); //%% // create variables in and name il.append(factory.createNew("java.io.BufferedReader")); il.append(InstructionConstants.DUP); il.append(factory.createNew("java.io.InputStreamReader")); il.append(InstructionConstants.DUP); //init input stream il.append(factory.createFieldAccess("java.lang.System", "in", i_stream, Constants.GETSTATIC)); il.append(factory.createInvoke("java.io.InputStreamReader", "<init>", Type.VOID, new Type[] { i_stream }, Constants.INVOKESPECIAL)); il.append(factory.createInvoke("java.io.BufferedReader", "<init>", Type.VOID, new Type[] { new ObjectType("java.io.Reader") }, Constants.INVOKESPECIAL)); //add in into the local variable pool and get the index automatically LocalVariableGen lg = mg.addLocalVariable("in", new ObjectType( "java.io.BufferedReader"), null, null); int in = lg.getIndex();//index of "in" var lg.setStart(il.append(new ASTORE(in)));//store the reference into local variable //首先创建对象,并初始化,操作结果在JVM的“堆”里,还需要在本地变量表中创建引用,因此在本地变量表中添加一个“in”比那辆, //然后根据索引值调用“astore”指令,即可将对象引用赋值给本地变量 /* 0: new #8; //class java/io/BufferedReader 3: dup 4: new #10; //class java/io/InputStreamReader 7: dup 8: getstatic #16; //Field java/lang/System.in:Ljava/io/InputStream; 11: invokespecial #20; //Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V 14: invokespecial #23; //Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V 17: astore_1 * */ //%% //String name = null; //%% // create local variable name and init it to null lg = mg.addLocalVariable("name", Type.STRING, null, null); int name = lg.getIndex(); il.append(InstructionConstants.ACONST_NULL);//add "null" to the stack top lg.setStart(il.append(new ASTORE(name)));//"store" the value of "null" into "name" var //%% //System.out.print("Please enter your name> ") //%% // create try_catch block InstructionHandle try_start = il.append(factory.createFieldAccess( "java.lang.System", "out", p_stream, Constants.GETSTATIC)); //从常量池中取出“please .....”,压入栈顶:这里感觉有问题,这个字符串常量应该先压入常量池才可以(最好是在这之前加一句, //加一句添加常量池操作其实并不影响实际运行的效率) il.append(new PUSH(cp, "Please enter your name> ")); il.append(factory.createInvoke("java.io.PrintStream", "print", Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL)); //%% //name = in.readLine(); //%% //将本地变量“in”推送至栈顶 il.append(new ALOAD(in)); il.append(factory.createInvoke("java.io.BufferedReader", "readLine", Type.STRING, Type.NO_ARGS, Constants.INVOKEVIRTUAL));//调用readLine()方法 il.append(new ASTORE(name));//接收的结果在栈顶,需要保存,因此加上保存到“name”slot的指令 //%% // } catch(IOException e) { return; } //%% GOTO g = new GOTO(null); InstructionHandle try_end = il.append(g); //add return:如果出异常,才会走到这条“return”指令,并返回到caller中 InstructionHandle handler = il.append(InstructionConstants.RETURN); // add exception handler which returns from the method mg.addExceptionHandler(try_start, try_end, handler, null); //%% //没有异常,继续执行:System.out.println("Hello, " + name); //%% // "normal" code continues, set the branch target of the GOTO InstructionHandle ih = il.append(factory.createFieldAccess( "java.lang.System", "out", p_stream, Constants.GETSTATIC)); g.setTarget(ih); // print "Hello":创建一个StringBuffer对象,通过调用StringBuffer的append操作,实现 //string1 + string2的操作,并且操作结果调用toString方法 il.append(factory.createNew(Type.STRINGBUFFER)); il.append(InstructionConstants.DUP); il.append(new PUSH(cp, "Hello, ")); il.append(factory.createInvoke("java.lang.StringBuffer", "<init>", Type.VOID, new Type[] { Type.STRING }, Constants.INVOKESPECIAL)); il.append(new ALOAD(name)); il.append(factory.createInvoke("java.lang.StringBuffer", "append", Type.STRINGBUFFER, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL)); // il.append(factory.createInvoke("java.lang.StringBuffer", "toString", Type.STRING, Type.NO_ARGS, Constants.INVOKEVIRTUAL)); il.append(factory.createInvoke("java.io.PrintStream", "println", Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL)); il.append(InstructionConstants.RETURN); // finalization mg.setMaxStack(); cg.addMethod(mg.getMethod()); il.dispose(); cg.addEmptyConstructor(ACC_PUBLIC); // dump the class cg.getJavaClass().dump("HelloWorld.class"); System.out.println("dump successly"); } catch (java.io.IOException e) { System.err.println(e); } catch (Exception e1) { e1.printStackTrace(); }