JiBX 1.0 采用类处理技术对类编译后生成的字节码进行了增强并且支持直接生成新类。字节码生成比工作在源代码级具有一些显著的优势,然而,有时它却在生成和调试应用程序时造成一些麻烦。即使不考虑方便的问题,一些开发者也是除了“源代码”之外什么也不信任。JiBx 2.0 的首席开发人员 Dennis Sosnoski 要使 JiBX 2.0 同时支持字节码生成技术和源代码生成技术。在这篇文章中,他讨论了源代码生成技术和字节码生成技术的不同之处并且对于如何协调二者给出了自己的看法。
类处理技术允许程序直接处理由 Java™ 源程序编译后生成的二进制类表示。我的 JiBX XML 数据绑定框架就是这样的一个例子,它采用类处理技术增强了 Java 类文件,给类文件添加了实现与 XML 文件相互转换的方法。直接处理二进制类具有许多优势,包括当源文件不可用时仍然可以对类进行修改。在大多数情况下,这种二进制方法都是非常有效的。
但是有时缺少源代码也有不利之处。例如:在调试过程中要使用源文件。Java 调试器是被设计为工作在源代码级,如果没有与字节码指令相匹配的 Java 源代码,调试器实际上是无用的。如果使用基于类处理技术的框架产生错误,在追踪这些错误时,缺少源文件就成问题了。而 JiBX 并不从类文件中删除调试信息 —— 这样您仍然可以照常调试原始代码 —— 但是不能通过添加的与 XML 相互转换的方法进行调试。除了调试的实用性问题外,很多开发者并不愿意信任一个框架在字节码级上处理他们的程序,并且他们自己不能方便地检查处理结果也让他们不舒服。
我为 JiBX 2.0 开发定的目标之一就是增加用源代码增强代替字节码增强的选项。在这篇文章里,我将对照地介绍处理这两种类型代码的难点和我实现时所采用的技术。同时,我也会讨论字节码操作的一些细节,这是我以前的文章中没有涉及到的,特别是在调用方法和控制流领域。
通常,编译器把 Java 源代码翻译成字节码指令序列。因此,大多数类处理库完全忽视了源代码并且只工作在字节码级上。惟一的例外情况是 Javassist 库,它允许使用 Java 源代码的一种形式将字节码插入方法中或者构造新的方法(请参阅 参考资料,找到我早期关于 Javassist 的 Java programming dynamics 文章)。
JiBX 2.0 对于源代码/字节码提供双重支持的可能性,有可能建立在 Javaassist 的源代码处理方法上:不断地产生源代码,然后,当直接增强类文件时,Javaassist 将源代码转换成字节码。但是 Javassist 对于源代码的支持是有限的,并且包含一些与标准 Java 源代码不同的特性(包括方法变量的引用方式)。其次,Javassist 比一些其他的字节码库(特别是 ASM 库,参看在文章 “ Classworking toolkit: ASM Classworking” 中的讨论)速度慢。我认为字节码增强仍然是 JiBX 2.0 的主要目标,在某些情况下(如使用 JiBX 联合一种 IDE 自动汇编),可能需要重复进行字节码增强,所以速度也是至关重要的。最后,javassist CPL 证书与 JiBX 的 BSD 证书并不兼容。鉴于上述原因,我决定采用另外一种方法。
我计划使用一个策略原型代替代码生成的实现,这时,根据使用源代码策略或者字节码策略的不同,同类型的操作将做不同的翻译。字节码生成与 JiBX 1.X 的实现基本上是相同的(通过使用 ASM 库而不是 BCEL 库)。源代码生成是新功能,并且它的结构形式必须考虑到在操作层面上与字节码生成的兼容性。
|
Java 源代码通常被编译成字节码,并且一些工具甚至可以将字节码(至少是由通常的编译器产生的文件形式)反编译成源代码。这两种代码形式之间的相互转换表明二者之间具有很高的兼容性。即使如此,使用源代码的编程技术与字节码的编程技术之间仍然存在实质的不同。在这一节,我将举例说明一些不同之处。
Java 源代码通常把方法的参数当成一个特殊形式的本地变量,参数声明直接包含在函数声明中。这个原则有一个例外,虚方法使用一个特殊的第一参数,这个参数不显示在方法的参数列表中。这个隐藏参数是指向调用这个方法的类实例的 this 指针。
字节码对于方法参数的处理与本地变量相似。对于字节码,在方法执行的时候,每个参数占用堆栈结构的一个或者两个字。与源代码不同,字节码中的每个参数都是明确的 —— 虚方法的 this 指针通常在堆栈结构中的 0 位置,接着是方法声明中明确定义的参数。不同的参数占用不同数量的堆栈结构位置,这取决于参数值类型的大小与标准字大小的比较。
源代码中一般的本地变量都定义在一个块中,这个块可能是一个完整的方法体或者是一个嵌套块。在字节码中遵循同样的原则。尽管不是明确的块,字节码中定义本地变量时,也要定义一个指令范围,在这个范围内,变量是有效的。同方法变量一样,本地变量占用堆栈结构中的字。在字节码中,为了使方法所需要的堆栈空间最小化,不同位置的不同本地变量可能会使用堆栈结构中的同一个字,只要变量的有效范围不重叠。
图 1 给出了一个简单方法的堆栈分配情况,包括本地变量。 long 型的值每个占用堆栈中的两个字,而 int 型和指针类型的值每个占用一个字。
图 1. 堆栈的使用
在 Java 源代码中,方法调用看起来非常简单:只要在方法名称后紧接着在括号里填入用逗号隔开的实参列表。实参是有位置的并且它必须与方法声明中相应的参数位置相同。如果方法返回一个结果值,则可以直接把这个值赋给一个本地变量、直接使用这个值或者不对它做任何处理。
而在相应的字节码中,情况就复杂多了。方法调用之前,必须根据参数声明,按照从左到右的顺序把对应的实参压入堆栈。虚方法调用(与静态调用相对)时,调用对象实例的指针必须在其他自变量之前被压入堆栈。当所有的自变量都在堆栈中时,方法可以被调用,并且调用结束后,在堆栈中的整个自变量列表将被方法的返回值代替。为了使堆栈状态有效,字节码必须考虑虚方法与静态方法调用的不同和返回值的问题。
我将举例说明堆栈的使用。清单 1 在一个类中定义了一个方法,这个方法与 图 1 表示的相似。清单 2 给出了一个字节码的注释版本,在 main() 方法中调用清单 1 中类的 power()。清单 2 的粗体部分表示实际的 power() 方法调用的建立和返回处理。
清单 1. 例子的源代码
public class PowerTest { private long power(long value, int power) { long result; if (power < 5) { // just compute value inline for low loop count result = 1; for (int i = 0; i < power; i++) { result *= value; } } else { // split the computation using recursion for speed result = power(value, power/2); result = result*result; if ((power % 2) == 1) { result *= value; } } return result; } public static void main(String[] args) { PowerTest inst = new PowerTest(); long value = Long.parseLong(args[0]); int power = Integer.parseInt(args[1]); System.out.println(value + " to the power " + power + " is " + inst.power(value, power)); } } |
// create and initialize class instance (using default constructor) new PowerTest dup invokespecial PowerTest. // store reference (duplicated before initializer call) to "inst" astore_1 // load first command line argument value string from array aload_0 iconst_0 aaload // convert and store value to "value" invokestatic Long.parseLong lstore_2 // load second command line argument value string from array aload_0 iconst_1 aaload // convert and store value to "power" invokestatic Integer.parseInt istore %4 // call power() and save result value to "result" aload_1 lload_2 iload %4 invokespecial PowerTest.power lstore %5 ... |
虽然字节码的堆栈操作增加了复杂度,但是它也具有一些源代码所不具备的灵活性。例如:字节码可以处理那些不止一次被用到的值,采用在堆栈中复制它们的方式。在源代码中要获得同样的效果,则需要定义一个本地变量来保存这个值。可以构造许多操作类型以利用字节码所提供的使用堆栈的灵活性,而且 JiBX 1.X 代码产生时很大程度上就是利用这种灵活性。
在字节码中控制程序执行的正常流程也比在源代码中要复杂一些。Java 平台提供条件执行(使用 if),三种风格的循环(for、do 和 while),一种开关结构(switch)。在字节码级上,只有两种不同的基本结构,一种对应于 switch 语句,另外一种是分支。然而,分支语句具有许多变化形式,这些变化形式足以弥补基本结构的稀少。
为了演示基本分支操作,清单 3 显示了清单 1 中的 power() 方法。这个例子包含几个分支,三个分支语句的字节码显示为粗体。第一个分支是一个 if_icmpge 条件转移语句。这个分支使用堆栈顶端的两个字,从第二个字中减去第一个字并且如果减的结果为非负值则转到此分支。第二个分支是无条件转移 goto。它对堆栈没有影响,在任何时候都转向目标分支。第三个分支是一个 if_icmpne 条件转移语句。这个分支使用堆栈顶端的两个字,从第二个字从减去第一个字并且如果减的结果为非零值则转到此分支。
清单 3. 添加了注释的具有分支的字节码
// check if "power" less than 5 iload_3 iconst_5 if_icmpge 29 // initialize "result" to 1 and "i" to 0 lconst_1 lstore %4 iconst_0 istore %6 // jump to end if "i" greater than or equal to "power" 11: iload %6 iload_3 if_icmpge 59 // multiply "result" value by "value" lload %4 lload_1 lmul lstore %4 // increment "i" value and loop back to test iinc %6 1 goto 11 // make recursive call for half the "power" 29: aload_0 lload_1 iload_3 iconst_2 idiv invokespecial PowerTest.power // square the returned "result" value lstore %4 lload %4 lload %4 lmul lstore %4 // check for odd "power" value iload_3 iconst_2 irem iconst_1 if_icmpne 59 // odd "power", multiple "result" again for final value lload %4 lload_1 lmul lstore %4 // return "result" value 59: lload %4 lreturn |
清单 3 演示了 Java 条件执行(if 语句)和一种形式的循环(for 语句)是如何被翻译成字节码的。 Java 源代码中的另外一种循环结构的处理方式与 for 相似。 switch 语句就复杂多了,在字节码中,有时采用正常的条件分支,有时采用两种基于表的条件分支之一。
|
仅仅通过提供两个完全独立的代码生成实现来支持源代码和字节码生成的合并是一种自然的方法。这种方法的确有用,但是显而易见,这将涉及很多重复的工作(在开始的时候和维护的过程中都有)。
我想要避免这种重复的工作。与其重复所有的生成代码,我宁愿实现一种策略类型方式,这种方式可以同时处理这两种类型的代码。这种方式使用公用代码控制生成过程,调用策略实现方法为一种特定的操作生成合适的代码。我将用一个简单的例子阐明这种技术
在 上个月的专栏 中,我描述了 JiBX 1.X 绑定编译器如何基于代码生成树结构产生字节码,这种树结构是根据绑定的定义构建的。采用基于树的方法处理代码生成具有如下优点:所有的代码按顺序生成 —— 不需要回退到前面生成的代码序列插入新生成的字节码。这就使实际代码生成变得相对简单。
虽然 JiBX 2.0 比 JiBX 1.X 更直接地依赖绑定定义,但是它依然沿用了 JiBX 1.X 使用树表示的工作原理。为了同时支持源代码和字节码生成,JiBX 2.0 添加了一个策略层,这个层加在由树生成的抽象操作序列和实际生成的代码之间。为了演示这个策略层是如何工作的,我将仔细讲解图 2(上个月文章中的一个例子)中的 JiBX 1.X 模型的代码生成部分,揭示字节码和源代码如何使用相同的抽象操作序列。
图2. 代码生成模型的例子
清单 4 为图 2 的左下部分给出了实现从 XML 到 Java 对象的解编排所需的抽象操作列表。清单的顶端部分完成的是一个 Customer 实例的解编排,底部完成的是一个 Name 实例的解编排。 JiBX 通常将解编排的代码变成虚方法添加到类中,添加的虚方法的名称以“JiBX”开头。在清单 4 中,我列出了 Customer 解编排方法的一部分抽象操作序列和 Name 解编排方法的完整抽象操作序列。
清单 4. 名称域解编排逻辑操作
load or create object from "name" field, save to local call Name unmarshalling method store reference from local variable to "name" field unmarshal "street1" field value from "street" element unmarshal "city" field value from "city" element ... // Name unmarshalling method call unmarshalling context method to push instance on stack unmarshal "firstName" field value from "first-name" element unmarshal "lastName" field value from "last-name" element call unmarshalling context method to pop instance from stack |
清单 5 显示了清单 4 中的抽象操作列表产生的字节码。字节码假定添加到类的解编排方法都是只有一个参数的虚方法,JiBX 解编排上下文被用来解释 XML 输入文档。因为它们是虚方法,所以堆栈的第一个值(也就是偏移量为 0 的位置)填入的是对象实例的引用;堆栈的第二个值是解编排上下文的引用,后面紧接着是代码中的本地变量。
清单 5. 名称域解编排字节码
// load or create object from "name" field, save to local aload_0 getfield name dup astore_3 ifnonnull 20 aload_1 invokestatic Name.JiBX_binding1_newinstance_1_0 astore_3 // call Name unmarshalling method 20: aload_3 aload_1 invokevirtual Name.JiBX_binding1_unmarshal_1_0 // store reference from local variable to "name" field aload_0 aload_3 putfield name // unmarshal "street1" field value from "street" element aload_0 aload_1 aconst_null ldc "street" invokevirtual org.jibx...UnmarshallingContext.parseElementText putfield street1 // unmarshal "city" field value from "city" element aload_0 aload_1 aconst_null ldc "city" invokevirtual org.jibx...UnmarshallingContext.parseElementText putfield city ... // Name.JiBX_binding1_unmarshal_1_0 method // call unmarshalling context method to push instance on stack aload_1 aload_0 invokevirtual org.jibx...UnmarshallingContext.pushObject // unmarshal "firstName" field value from "first-name" element aload_0 aload_1 aconst_null ldc "first-name" invokevirtual org.jibx...UnmarshallingContext.parseElementText putfield firstName // unmarshal "lastName" field value from "last-name" element aload_0 aload_1 aconst_null ldc "last-name" invokevirtual org.jibx...UnmarshallingContext.parseElementText putfield lastName // call unmarshalling context method to pop instance from stack aload_1 invokevirtual org.jibx...UnmarshallingContext.popObject return |
清单 6 显示了抽象操作列表生成的 Java 源代码,这组抽象操作与生成清单 5 中字节码的抽象操作相同。源代码中变量名“ctx”指的是每种方法的解编排上下文参数。
清单 6. 名称域解编排源代码
// load or create object from "name" field, save to local Name local1 = name; if (local1 == null) { local1 = Name.JiBX_binding1_newinstance_1_0(); } // call Name unmarshalling method local1.JiBX_binding1_unmarshal_1_0(ctx); // store reference from local variable to "name" field name = local1; // unmarshal "street1" field value from "street" element street1 = ctx.parseElementText(null, "street"); // unmarshal "city" field value from "city" element city = ctx.parseElementText(null, "city"); ... // Name.JiBX_binding1_unmarshal_1_0 method // call unmarshalling context method to push instance on stack ctx.pushObject(this); // unmarshal "firstName" field value from "first-name" element firstName = ctx.parseElementText(null, "first-name"); // unmarshal "lastName" field value from "last-name" element lastName = ctx.parseElementText(null, "last-name"); // call unmarshalling context method to pop instance from stack ctx.popObject(); |
从这个简单的例子可以很容易地看出,如何从抽象操作同时实现字节码和源代码的生成。虽然支持完全灵活的 JiBX 数据绑定涉及到更复杂的东西,但是原理是相同的。
|
JiBX 1.X 绑定编译器检查每个绑定类,检查类中名称与实现绑定方法的模式相匹配的方法。如果绑定编译器发现名称符合的方法,它就认为在绑定编译器以前运行的时候已经添加了这些方法。当构造一个新方法时,在真正把这个新方法添加到类之前,绑定编译器首先检查是否与一个已经存在的绑定方法(在绑定编译器执行前就已经存在于类中,或者是在绑定编译器先前的执行过程中添加进来的)相匹配。如果找到相匹配的方法,则用已经存在的方法代替新构造的方法。如果类中原来存在的一个绑定方法没有被绑定编译器使用,则从类中把它删除。这种方法匹配方式不仅使 JiBX 添加的代码总量最小化,而且在绑定重编译的时候避免了不必要的对类文件的改变。
在 JiBX 1.X 实现时,这种方法匹配方式具有一些局限性。特别是,它不能匹配相互递归调用的方法,也就是每个方法都调用其他的方法。对于 JiBX 2.0,我希望能够克服这种局限性。然而,在这篇文章中我只简单谈谈下面两个问题:第一,字节码方法比较通常是如何工作的;第二,我计划如何实现相应的源代码方法比较。这不是指源代码与字节码的比较 —— 对于这个问题涉及到的更多,但是幸运的是 JiBX 2.0 的使用者并不需要关心这个问题,使用者只需要指定某个特定的类是使用字节码还是源代码修改。
JiBX 1.X 的字节码方法匹配基于对方法签名和方法的二进制字节码指令序列的比较。因为绑定编译器经常为一个绑定组件生成相同的字节码指令序列,这种匹配过程像预期的那样有效,甚至对于那些调用处于同一个类或者其他类中的其他方法的方法也同样有效,只要在调用之前检查了被调用方法的重复性。JiBX 代码生成所使用的基于树的方法总能按照深度优先顺序构造方法;如果在方法调用图中没有循环(方法的相互调用),重复检查是有效的。
源代码方法匹配需要比字节码方法匹配多做一些工作。其中一种方式是匹配一个方法的 Java 语言标志(包括源代码)序列,这是与字节码比较方法签名和指令序列的方法最相近的方式。与字节码生成相同,绑定编译器总是对一个绑定组件生成相同的源代码标志序列。如果一个新构造的方法的标志序列与一个现存方法的标志序列相匹配,则这两个方法是相同的,就用已经存在的方法代替新构造的方法。
在源代码中添加和删除方法也比在字节码中复杂。在字节码中,一个类描述中的方法排列顺序没有任何特别的意义。在字节码中,可以增加和删除方法而丝毫也不影响用户代码;并且生成的方法与从用户代码编译生成的代码本质上是相同的。另一方面,在源代码中,用户代码可能会含有注释和格式,这些注释和格式虽然不影响代码的含义但是对于用户来说,它们仍然非常重要。任何对原始形式的用户源代码的曲解都将造成麻烦。
框架所使用的在用户代码中添加生成的源代码的标准技术是:定义一个定界符将生成的代码从原始代码中分离。JiBX 2.0 采用一种更灵活的方式:开始时,在 Java 源文件中已经存在的用户方法后添加新生成的方法,但是如果生成的方法被后来的绑定编译修改了,则直接替换掉原来的代码。这种方法使用户能够对生成的代码进行移动和对格式进行重新调整,而不会影响到方法匹配过程。
当代码需要扫描大量的源代码文件时,效率也是一个潜在的重要因素。JiBX 能够避免这个问题,因为它要求绑定类编译后的版本必须存在,即使是在生成代码增强的时候。绑定编译器通常能够轻易地检查到 JiBX 生成的源代码方法名,不需要处理每个未处理过的源代码文件。只有那些添加了 JiBX 方法的源代码文件才需要扫描,这样,在比较时就能够记录和使用每个方法标志序列。
|
这个月,我着重讲述了允许用户在绑定代码时选择源代码增强和字节码增强所涉及到的问题,绑定的代码是由我的 JiBX XML 数据绑定框架生成的。到目前为止,我得出这样的结论:使用正确的架构,这种选择是能够实现的,并且这毫无疑问会给用户带来很多的好处。我希望明年年初将会开发出支持这项特性的 JiBX 2.0 bata 版。
下个月,我将着重讲述 JiBX 2.0 中类处理技术的另一个方面的改变。Java 5 增加了对语言泛型的支持,使开发者既可以在编译时检查集合类型安全性,又可以隐藏使用集合所涉及的运行时重塑(尽管以一个输入有时会非常杂乱的语法为代价)。我完全不是因为泛型有助于书写清晰的代码而感到欣喜,而是我对于使用附加类型信息非常感兴趣,这种附加类型信息会被加入编译的类文件中。在下一期中,我将深入讲述泛型方面的内容,并且向您阐明如何在运行时使用反射访问泛型信息。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130202/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/374079/viewspace-130202/