[转载]Classworking 工具箱: 源代码生成与字节码生成的结合

Classworking 工具箱: 源代码生成与字节码生成的结合
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 库)。源代码生成是新功能,并且它的结构形式必须考虑到在操作层面上与字节码生成的兼容性。


blue_rule.gif
c.gif
c.gif
u_bold.gif回页首


代码形式比较

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));
    }
}


清单 2. 添加了注释的方法调用字节码

				
// 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),三种风格的循环(fordowhile),一种开关结构(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 语句就复杂多了,在字节码中,有时采用正常的条件分支,有时采用两种基于表的条件分支之一。


blue_rule.gif
c.gif
c.gif
u_bold.gif回页首


生成机制

仅仅通过提供两个完全独立的代码生成实现来支持源代码和字节码生成的合并是一种自然的方法。这种方法的确有用,但是显而易见,这将涉及很多重复的工作(在开始的时候和维护的过程中都有)。

我想要避免这种重复的工作。与其重复所有的生成代码,我宁愿实现一种策略类型方式,这种方式可以同时处理这两种类型的代码。这种方式使用公用代码控制生成过程,调用策略实现方法为一种特定的操作生成合适的代码。我将用一个简单的例子阐明这种技术

利用树生成代码

上个月的专栏 中,我描述了 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 数据绑定涉及到更复杂的东西,但是原理是相同的。


blue_rule.gif
c.gif
c.gif
u_bold.gif回页首


检查改变

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 方法的源代码文件才需要扫描,这样,在比较时就能够记录和使用每个方法标志序列。


blue_rule.gif
c.gif
c.gif
u_bold.gif回页首


下期讲述泛型

这个月,我着重讲述了允许用户在绑定代码时选择源代码增强和字节码增强所涉及到的问题,绑定的代码是由我的 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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值