Apache BCEL.docx
《Apache BCEL.docx》由会员分享,可在线阅读,更多相关《Apache BCEL.docx(18页珍藏版)》请在冰豆网上搜索。
ApacheBCEL
ApacheBCEL让您深入JVM汇编语言进行类操作的细节
DennisM.Sosnoski(dms@),总裁,SosnoskiSoftwareSolutions,Inc.
DennisSosnoski(dms@)是西雅图地区的Java咨询公司SosnoskiSoftwareSolutions,Inc.的创始人和首席顾问。
他有30多年的专业软件开发经验,最近几年致力于服务器端的Java技术,包括servlet、EnterpriseJavaBeans和XML。
他经常在全国性的会议上就Java中的XML和J2EE技术发表言论。
简介:
ApacheByteCodeEngineeringLibrary(BCEL)可以深入Java类的字节码。
可以用它转换现有的类表示或者构建新的类,因为BCEL在单独的JVM指令级别上进行操作,所以可以让您对代码有最强大的控制。
不过,这种能力的代价是复杂性。
在本文中,Java顾问DennisSosnoski介绍了BCEL的基本内容,并引导读者完成一个示例BCEL应用程序,这样您就可以自己决定是否值得以这种复杂性来换取这种能力。
标记本文!
发布日期:
2004年4月01日
级别:
初级
访问情况:
4408次浏览
评论:
0 (查看 | 添加评论-登录)
平均分(8个评分)
为本文评分
在本系列的最后三篇文章中,我展示了如何用Javassist框架操作类。
这次我将用一种很不同的方法操纵字节码——使用ApacheByteCodeEngineeringLibrary(BCEL)。
与Javassist所支持的源代码接口不同,BCEL在实际的JVM指令层次上进行操作。
在希望对程序执行的每一步进行控制时,底层方法使BCEL很有用,但是当两者都可以胜任时,它也使BCEL的使用比Javassist要复杂得多。
我将首先讨论BCEL基本体系结构,然后本文的大部分内容将讨论用BCEL重新构建我的第一个Javassist类操作的例子。
最后简要介绍BCEL包中提供的一些工具和开发人员用BCEL构建的一些应用程序。
BCEL类访问
BCEL使您能够同样具备Javassist提供的分析、编辑和创建Java二进制类的所有基本能力。
BCEL的一个明显区别是每项内容都设计为在JVM汇编语言的级别、而不是Javassist所提供的源代码接口上工作。
除了表面上的差别,还有一些更深层的区别,包括在BCEL中组件的两个不同层次结构的使用——一个用于检查现有的代码,另一个用于创建新代码。
我假定读者已经通过本系列前面的文章熟悉了Javassist(请参阅侧栏不要错过本系列的其余部分)。
因此我将主要介绍在开始使用BCEL时,可能会让您感到迷惑的那些不同之处。
与Javassist一样,BCEL在类分析方面的功能基本上与Java平台通过RelfectionAPI直接提供的功能是重复的。
这种重复对于类操作工具箱来说是必要的,因为一般不希望在所要操作的类被修改之前就装载它们。
不要错过本系列的其余部分
第1部分:
“类和类装入”(2003年4月)
第2部分,“引入反射”(2003年6月)
第3部分,“应用反射”(2003年7月)
第4部分,“用Javassist进行类转换”(2003年9月)
第5部分,“动态转换类”(2004年2月)
第6部分,“用Javassist进行面向方面的更改”(2004年3月)
BCEL在org.apache.bcel包中提供了一些基本常量定义,但是除了这些定义,所有分析相关的代码都在org.apache.bcel.classfile包中。
这个包中的起点是JavaClass类。
这个类在用BCEL访问类信息时起的作用与使用常规Java反射时,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、用于字段和方法的FieldGen和MethodGen和处理一系列JVM指令的InstructionList。
最后,org.apache.bcel.generic包还定义了表示每一种类型的JVM指令的类。
可以直接创建这些类的实例,或者在某些情况下使用org.apache.bcel.generic.InstructionFactoryhelper类。
使用InstructionFactory的好处是它处理了许多指令构建的簿记细节(包括根据指令的需要在常量池中添加项)。
在下面一节您将会看到如何使所有这些类协同工作。
回页首
用BCEL进行类操作
作为使用BCEl的一个例子,我将使用第4部分中的一个Javassist例子——测量执行一个方法的时间。
我甚至采用了与使用Javassist时的相同方式:
用一个改过的名字创建要计时的原方法的一个副本,然后,通过调用改名后的方法,利用包装了时间计算的代码来替换原方法的主体。
选择一个试验品
清单1给出了一个用于展示目的示例方法:
StringBuilder类的buildString方法。
正如我在第4部分所说的,这个方法采用了所有Java性能专家告诫您不要使用的方式来构建一个String——它重复地在字符串的未尾附加单个字符以创建更长的字符串。
因为字符串是不可变的,所以这种方式意味着每次循环时会构建一个新的字符串,从老的字符串拷贝数据并在最后增加一个字符。
总的效果就是用这个方法创建更长的字符串时,它会产生越来越大的开销。
清单1.要计时的方法
publicclassStringBuilder
{
privateStringbuildString(intlength){
Stringresult="";
for(inti=0;iresult+=(char)(i%26+'a');
}
returnresult;
}
publicstaticvoidmain(String[]argv){
StringBuilderinst=newStringBuilder();
for(inti=0;iStringresult=inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructedstringoflength"+
result.length());
}
}
}
清单2显示了等同于用BCEL进行类操作改变的源代码。
这里包装器方法只是保存当前时间,然后调用改名后的原方法,并在返回调用原方法的结果之前打印时间报告。
清单2.在原方法中加入计时
publicclassStringBuilder
{
privateStringbuildString$impl(intlength){
Stringresult="";
for(inti=0;iresult+=(char)(i%26+'a');
}
returnresult;
}
privateStringbuildString(intlength){
longstart=System.currentTimeMillis();
Stringresult=buildString$impl(length);
System.out.println("CalltobuildString$impltook"+
(System.currentTimeMillis()-start)+"ms.");
returnresult;
}
publicstaticvoidmain(String[]argv){
StringBuilderinst=newStringBuilder();
for(inti=0;iStringresult=inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructedstringoflength"+
result.length());
}
}
}
编写转换代码
用我在BCEL类访问一节中描述的BCELAPI实现添加方法计时的代码。
在JVM指令级别上的操作使代码比第4部分中Javassist的例子要长得多,所以这里我准备在提供完整的实现之前,一段一段地介绍。
在最后的代码中,所有片段构成一个方法,它有两个参数:
cgen——它是org.apache.bcel.generic.ClassGen类的一个实例,用被修改的类的现有信息初始化,和方法——要计时方法的org.apache.bcel.classfile.Method实例。
清单3是转换方法的第一段代码。
可以从注释中看到,第一部分只是初始化要使用的基本BCEL组件,它包括用要计时方法的信息初始化一个新的org.apache.bcel.generic.MethodGen实例。
我为这个MethodGen设置一个空的指令清单,在后面我将用实际的计时代码填充它。
在第2部分,我用原来的方法创建第二个org.apache.bcel.generic.MethodGen实例,然后从类中删除原来的方法。
在第二个MethodGen实例中,我只是让名字加上“$impl”后缀,然后调用getMethod()以将可修改的方法信息转换为固定形式的org.apache.bcel.classfile.Method实例。
然后调用addMethod()以便在类中添加改名后的方法。
清单3.添加拦截方法
//setuptheconstructiontools
InstructionFactoryifact=newInstructionFactory(cgen);
InstructionListilist=newInstructionList();
ConstantPoolGenpgen=cgen.getConstantPool();
Stringcname=cgen.getClassName();
MethodGenwrapgen=newMethodGen(method,cname,pgen);
wrapgen.setInstructionList(ilist);
//renameacopyoftheoriginalmethod
MethodGenmethgen=newMethodGen(method,cname,pgen);
cgen.removeMethod(method);
Stringiname=methgen.getName()+"$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());
清单4给出了转换方法的下一段代码。
这里的第一部分计算方法调用参数在堆栈上占用的空间。
之所以需要这段代码,是因为为了在调用包装方法之前在堆栈帧上存储开始时间,我需要知道局部变量可以使用什么偏移值(注意,我可以用BCEL的局部变量处理得到同样的效果,但是在本文中我选择使用显式的方式)。
这段代码的第二部分生成对java.lang.System.currentTimeMillis()的调用,以得到开始时间,并将它保存到堆栈帧中计算出的局部变量偏移处。
您可能会奇怪为什么在开始参数大小计算时要检查方法是否是静态的,如果是静态的,将堆栈帧槽初始化为零(不是静态正好相反)。
这种方式与Java如何处理方法调用有关。
对于非静态的方法,每次调用的第一个(隐藏的)参数是目标对象的this引用,在计算堆栈帧中完整参数集大小时我要考虑到这点。
清单4.设置包装的调用
//computethesizeofthecallingparameters
Type[]types=methgen.getArgumentTypes();
intslot=methgen.isStatic()?
0:
1;
for(inti=0;islot+=types[i].getSize();
}
//savetimepriortoinvocation
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语句将结果值保存到位于堆栈帧中的另一个局部变量中(如果结果类型不是void)。
清单5.调用包装的方法
//callthewrappedmethod
intoffset=0;
shortinvoke=Constants.INVOKESTATIC;
if(!
methgen.isStatic()){
ilist.append(InstructionFactory.createLoad(Type.OBJECT,0));
offset=1;
invoke=Constants.INVOKEVIRTUAL;
}
for(inti=0;iTypetype=types[i];
ilist.append(InstructionFactory.createLoad(type,offset));
offset+=type.getSize();
}
Typeresult=methgen.getReturnType();
ilist.append(ifact.createInvoke(cname,
iname,result,types,invoke));
//storeresultforreturnlater
if(result!
=Type.VOID){
ilist.append(InstructionFactory.createStore(result,slot+2));
}
现在开始包装。
清单6生成实际计算开始时间后经过的毫秒数,并作为编排好格式的消息打印出来的代码。
这一部分看上去很复杂,但是大多数操作实际上只是写出输出消息的各个部分。
它确实展示了几种我在前面的代码中没有使用的操作类型,包括字段访问(到java.lang.System.out)和几种不同的指令类型。
如果将JVM想象为基于堆栈的处理器,则其中大多数是容易理解的,因此我在这里就不再详细说明了。
清单6.计算并打印所使用的时间
//printtimerequiredformethodcall
ilist.append(ifact.createFieldAccess("java.lang.System","out",
newObjectType("java.io.PrintStream"),Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
Stringtext="Calltomethod"+methgen.getName()+"took";
ilist.append(newPUSH(pgen,text));
ilist.append(ifact.createInvoke("java.io.PrintStream","print",
Type.VOID,newType[]{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,newType[]{Type.LONG},Constants.INVOKEVIRTUAL));
ilist.append(newPUSH(pgen,"ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream","println",
Type.VOID,newType[]{Type.STRING},Constants.INVOKEVIRTUAL));
生成了计时消息代码后,留给清单7的就是保存包装的方法的调用结果值(如果有的话),然后结束构建的包装器方法。
最后这部分涉及几个步骤。
调用stripAttributes(true)只是告诉BCEL不对构建的方法生成调试信息,而setMaxStack()和setMaxLocals()调用计算并设置方法的堆栈使用信息。
完成了这一步后,就可以实际生成方法的最终版本,并将它加入到类中。
清单7.完成包装器
//returnresultfromwrappedmethodcall
if(result!
=Type.VOID){
ilist.append(InstructionFactory.createLoad(result,slot+2));
}
ilist.append(InstructionFactory.createReturn(result));
//finalizetheconstructedmethod
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();
完整的代码
清单8显示了完整的代码(稍微改变了一下格式以适合显示宽度),包括以类文件的名字为参数的main()方法和要转换的方法:
清单8.完整的转换代码
publicclassBCELTiming
{
privatestaticvoidaddWrapper(ClassGencgen,Methodmethod){
//setuptheconstructiontools
InstructionFactoryifact=newInstructionFactory(cgen);
InstructionListilis