JiBX 框架构建在类处理技术之上,用于在 Java 对象和 XML 之间进行快速而灵活的转换。但是生成正确的、经得起检验的字节码并不容易。首席开发人员 Dennis Sosnoski 在开发 1.0 产品发行版的过程中,经历了一些痛苦的类处理体验。在这篇文章中,他分享了自己的感受,讨论了用于代码生成的内部结构以及确保生成的代码符合 JVM 规则所采取的步骤。
我的 JiBX XML 数据绑定框架是在 Java 对象和 XML 文档之间进行转换的迅速而灵活的工具。大多数 XML 数据绑定框架采用的方式是从 XML 方案生成 Java 类,由框架代码实现构建到生成类中的绑定。而 JiBX 采用类处理技术增强了编译后的 Java 类文件,给类文件添加了实现绑定的方法。这种方式让 JiBX 既能处理现有的类又能处理生成的类,还提供了运行时相对较小而且操作非常迅速的好处。
与大多数使用字节码增强的框架相比,JiBX 数据绑定实现了更复杂的代码生成。在开发 JiBX 期间,我不得不应对许多挑战,好让这种复杂的代码生成可以工作。在这篇文章中,我将总结我在把 JiBX 开发到 1.0 生产发行版的过程中遇到的一些挑战和解决方案。首先,我先从介绍 JiBX 的字节码生成架构开始。
我的 JiBX 开发是在几个具体目标指导下进行的。第一个目标是:它应当支持到现有类的灵活绑定,而不一定使用生成的类。第二个目标是:它应当迅速,使用字节码增强直接把绑定代码添加到应用程序类(与侵入性较小但是较慢的反射技术相反)。第三个目标是:它不仅应当支持在单一绑定内部以不同方式使用同一个类,还应当支持到同一个类的多重绑定。随生产发行版的出现,还添加了其他目标,但是这三个初始目标已经证明是框架架构的主要影响因素。
JiBX 由两个主要组件构成:绑定编译器,它负责类实际的字节码增强;运行时,生成的代码用它对文档进行实际的编排(从对象生成 XML)和反编排(从 XML 生成对象)。运行时随着更多选项添加进来而逐渐增长,但是运行时代码的结构基本保持与项目开始的时候一样。另一方面,绑定编译器的尺寸和复杂性都增长了,字节码增强内核已经重新调整了几次结构以添加功能和提高代码质量。在 1.0 发行版中,绑定编译器的尺寸是运行时的 4 倍(228 K 比 54K),复杂性则高出许多倍。因为这个专栏关注的是类处理,所以我只讨论绑定编译器组件。
我先从一个实际的绑定示例开始。图 1 显示了我在上一期中用作 JiBX 示例的一对绑定。这两个绑定为同一 Java 类定义了不同的 XML 格式。在图中,我用颜色突出了两个绑定之间的区别(以及在两个文档中对应的区别) —— 蓝色代表 Name 类引用的处理,绿色代表提供地址信息的 Customer 类的属性,红色代表 phone 属性。
图 1. 示例绑定
图 1 演示了绑定编译器的一些基本的灵活性(虽然表达方式非常有限)。这一对绑定提供了研究绑定编译器工作方式的良好起点。
为了处理编排和反编排,JiBX 向绑定中包含的类添加了新类和新方法。对于所有绑定到 XML 结构(与简单的文本值相对)的类,JiBX 创建了在实际实现与 XML 之间进行转换的方法。对于绑定中的顶级映射类(可以与独立文档相互转换的类),JiBX 也添加编排和反编排接口,以及这些接口定义的方法。最后,对于顶级类和其他映射类,JiBX 都生成独立的支持类,提供一个中间级别,实现了调用对应编排/反编排实现方法的接口。这种方法和类的组合看起来可能有点费解,但是这是为了支持 JiBX 绑定允许的灵活性程度所必需的。
如果把 图 1 的 Java 源代码编译成类文件,然后用 JiBX 绑定编译器编译绑定定义,那么就会得到图 2 所示的一组类和方法。在这个示例中,原始类中没有方法,所以图 2 中显示的所有方法都是 JiBX 绑定编译器添加的。
图 2.绑定之后的类图
对于图 2 中的信息不需要在这里深入介绍,但是我要给出一个快速概述。在逻辑的最高级别(但是在图中是在中间),添加的类 simple.JiBX_binding1Factory 和 simple.JiBX_binding2Factory 提供了到编译绑定的运行时访问,主要是通过 createMarshallingContext() 和 createUnmarshallingContext() 方法。JiBX 运行时为用户提供了访问特定绑定的工厂类的途径(使用 org.jibx.runtime.BindingDirectory 类),而且一旦发现绑定工厂,就可以用这些方法创建编排和反编排上下文,控制 Java 对象和 XML 文档之间的转换。
第二对添加的类 simple.JiBX_binding1Customer_access 和 simple.JiBX_binding2Customer_access,位于图的底部,是我在这一节的第一段中提到的中间支持类。这些类充当运行时的粘合剂,把绑定与实现编排和反编排操作的映射类(在这个示例中,是 simple.Customer)的具体方法关联起来 。对于每个类,每个绑定工厂类都用这个绑定中的 定义去引用支持类。
详细的编排和反编排实现代码被直接添加到绑定类中。在 simple.Customer 类中,这些方法包括一个 JiBX_binding1_newinstance_1_0() 方法(用来创建类的新实例),JiBX_bindingX_marshal_1_0() 和 JiBX_bindingX_unmarshal_1_0() 方法(用来编排和反编排与类对应的元素的内容),以及 JiBX_binding2_marshalAttr_1_0() 和 JiBX_binding2_unmarshalAttr_1_0() 方法(用来编排和反编排与类对应的元素的属性)。Customer 类也有几个方法,由添加到顶级映射对象的接口使用。在 simple.Name 中,只有三个添加的方法:JiBX_binding1_newinstance_1_0()、JiBX_binding1_marshal_1_0() 和 JiBX_binding1_unmarshal_1_0()。
添加到每个类的具体方法,表现了绑定编译器的一项很出色的特性:绑定编译器并不盲目地把每个绑定的通用方法添加到绑定中使用的每个类,而是只创建实际需要的方法。如果一个方法已经添加到符合绑定需要的类中,那么会重用这个方法,而不会添加新方法。这就是为什么 图 2 中的每个类只有一个“newinstance”方法,也是为什么 Name 类只有一个编排方法和一个反编排方法 —— 两个绑定中对于来自同一类的数据的处理是相同的。在这篇文章后面,当我讨论字节码生成的细节时,我还会回到这一问题上。
在继续讨论之前,我要指出:某些类无法用直接的方法插入处理。例如,系统类是不可修改的,而用户定义接口类虽然可以修改,但是不能包含实际的实现代码。如果绑定要处理接口或不可修改的类,JiBX 转而把必要的编排/反编排代码以 static 方法的形式,添加到特殊的助手类(叫作“munge”类)。这种方式没有提供在可以直接修改类时可能有的完整的灵活性(例如,不能在绑定中使用不可修改类的私有字段,而在使用可修改类时可以),但是,这种方式在 Java 语言和 JVM 许可的限制内,提供了可以使用的支持级别。
为了控制上一节中描述的所有方法和类的添加过程,JiBX 绑定编译器首先用代码生成树结构的形式,创建了每个绑定的内部表示形式。这个树结构反映了编排和反编排所需要的操作嵌套。这个树的每个组件都实现了一个代码生成接口,接口定义了不同类型的代码生成使用的方法(例如 getAttributeMarshal()、genContentMarshal()、genContentPresentTest() 等等)。每个方法调用把当前构造的方法的信息作为自己的参数。被调用的组件在从调用返回之前,把适当的字节码指令添加到正在构造的方法之后,从而支持一种模块化的代码生成方式:每种类型的组件负责某个具体功能,但是可以把组件以不同的方式组合,从而满足每个绑定的需求。
图 3 和图 4 显示了根据 图 1 中的绑定构建的代码生成树结构。两个树的上层结构是相同的,而底层结构的组织很不同,这反映了两个绑定定义的不同。
图 3. 绑定 1 的代码生成模型
图 4. 绑定 2 的代码生成模型
组件对代码生成方法调用的通用处理方式包括三个步骤:生成需要的设置代码、调用子组件添加子组件的代码生成、生成需要的包装代码。例如,在使用 元素包装器 组件时,genContentMarshal() 调用生成的代码首先写入元素的开始标记,然后调用子组件处理元素内容的编排,最后生成代码写入元素的结束标记。如果子组件包含一个或多个属性,那么这个过程会复杂一些,因为要用一个单独的步骤向元素的开始标记中写入属性。
对象绑定 组件是向绑定中包含的类实际添加新方法的组件。例如,在执行 genContentMarshal() 方法时,对象绑定组件首先检查编排方法在绑定组件的类中是否已经生成。如果没有,对象绑定组件就创建新的编排方法,将它添加到绑定组件中。作为代码生成的一部分,它还调用子组件,生成新方法的代码。如果发现或生成了绑定组件的编排方法,对象绑定组件添加的代码只是让绑定组件的方法去调用传递给它的原始方法。
绑定编译器的早期版本不经历构造代码生成模型这一步,而是直接从绑定生成代码。添加代码生成模型是为了让字节码生成更加模块化、更容易维护。虽然这种方式有些问题(我会在下一节讨论),模型方式对这些目标来说工作得很好。
|
JiBX 1.0 使用 BCEL 框架对字节码进行操纵。虽然 BCEL 可以用于实现这个目的,但是,它有时使用起来要比我开始这个项目时期望的更困难。我遇到的一些问题是 BCEL 特有的,但是其他问题在其他字节码框架中也有。在这一节中,我将介绍这两类问题以及我在开发 JiBX 1.0 期间对它们的处理。
在 BCEL 框架中,我发现了几个让它使用起来很笨拙的问题。第一个是类的反射访问(在 org.apache.bcel.classfile 包中)和操纵、构建类的 API(在 org.apache.bcel.generic 包中)两者的分离。这些相似的 API 造成了在许多情况下需要维护两种类信息。
BCEL 实现的字节码处理看起来也过于复杂,虽然有很强的灵活性,但是要操纵的对象数量也极为巨大。这些对象包括:实际的 org.apache.bcel.generic.ClassGen 对象,表示要构建的类;与类关联的 org.apache.bcel.generic.ConstantPoolGen 对象;org.apache.bcel.generic.InstructionFactory 对象,用来创建具体类和常数池的指令;org.apache.bcel.generic.InstructionList 对象,表示生成的字节码指令序列。
对于 JiBX 绑定编译器,我发现最容易的方式就是把所有 BCEL 的细节隐藏在几个包装器类中。例如,用来构建新方法的类(org.jibx.binding.classes.MethodBuilder)处理所有 BCEL 方法构建的细节,同时提供相关调用,向当前列表添加必需的指令类型。虽然用这种方式会损失 BCEL InstructionList(它允许向列表插入指令和从列表删除指令,而不仅仅是添加指令)的一些灵活性,但是对于绑定编译器使用的顺序字节码生成来说,工作得很好。类似的类为现有类和方法包装了 BCEL 信息。
把 BCEL 细节隐藏在包装器中,也提供了方便地添加更多功能的位置,就像在类和方法比较代码中一样。这个比较代码定义了 hashCode() 和 equals() 方法,用来检测类或方法什么时候可以相互替代。利用这些比较,绑定编译器可以确定在类中发现的已经存在的方法是否仍然需要,并避免创建重复的类或方法。
在字节码生成的一般性问题方面,我列出了一个大问题:需要确保正在生成的代码符合 JVM 规范。如果生成的字节码指令序列的任何细节有误,那么把包含这个字节码的类装入 JVM 时,检验就会失败。这个失败会导致痛苦而艰难的任务:从检验失败的类返回,寻找生成错误指令的代码。
我在 JiBX 开发上花的时间,有很大一部分花在跟踪非法字节码生成问题上。正因为如此,我对代码做了多项修改,以便更容易发现这些问题。第一个修改添加了一个选项,使用包含在 org.apache.bcel.verifier 包中的 Justice 检验器,针对生成的类运行 BCEL 自己的检验。使用 Justice 检验的优势是:提供的错误信息要比 JVM 提供的信息更详细(包括堆栈状态和部分问题代码)。但是,我发现对于有些类,Justice 发现的问题是 JVM 类装入器忽略的。为了避免把时间花在这类问题上,我添加了另外一个选项,尝试直接在绑定编译器内装入修改后的类。第二个选项为修改后的类提供了一种烟雾测试 —— 如果它们能够由绑定编译器成功装载,那么用户就可以确信他们的应用程序在运行时需要的时候也能成功地装入相同的类。
我还添加了一个选项,可以打印出绑定的代码生成模型树。Justice 输出给出问题的快照。在反汇编了非法类修改后的字节代码(使用 BCEL 的 org.apache.bcel.util.Class2HTML 工具)并把它与 Justice 的问题代码快照进行比较之后,可以看出在较大的流程中错误的位置。一旦理解了有问题的代码想做什么,就可以把错误与代码生成树联系起来,并分离出生成出错的模型组件。即使这样,我仍然不得不经常调试组件的代码才能真正发现问题所在。这类问题变得如此可怕,以致于我开始避免对代码生成做任何变化,因为害怕无意中破坏了什么(而且这种恐惧并不随着项目构建中执行的测试绑定日益增加而减轻)。
正如本.富兰克林所说,“一盎司预防顶得上一磅治疗”。当我试图从生成的代码回头去寻找问题时,我就处在疲于进行治疗的状态。我需要提前预防问题。为了做到这一点,我在方法构建器类中添加了代码,跟踪代码生成中每一点的堆栈状态,这样如果要添加的指令不符合堆栈状态,就会抛出运行时异常。从本质上说,我是在实现自己的字节码即时检验器。我发现产生的结果完全值得我付出那么多努力。
清单 1 显示的是这个即时检验产生的错误输出的示例。在这个示例中,我在处理集合的代码生成中引入了一个错误,在项目反编排循环的循环体中,向堆栈添加了一个额外项目。这个额外的项目导致堆栈状态在循环末端与开始时不匹配,所以当代码生成试图添加一个回到循环顶部的分支时,就会出现错误。错误报告包括:生成分支指令的组件(“generated by ...”这一行)和两个堆栈状态(代表 “to” 和 “from” 这两个分支位置),还有错误发生的那一点上的堆栈跟踪。
清单 1. 字节码生成的错误报告
java.lang.IllegalStateException: Stack size mismatch on branch in method simple.JiBX_MungeAdapter.JiBX_mybinding7a_unmarshal generated by org.jibx.binding.def.NestedCollection@1ebde03 from stack: 0: java.lang.Object[] 1: java.lang.Object[] to stack: 0: java.lang.Object[] at org.jibx.binding.classes.BranchWrapper.setTarget (BranchWrapper.java:184) at org.jibx.binding.classes.BranchWrapper.setTarget (BranchWrapper.java:201) at org.jibx.binding.def.NestedCollection.genContentUnmarshal (NestedCollection.java:172) at org.jibx.binding.def.ObjectBinding.genUnmarshalContentCall (ObjectBinding.java:750) ... |
在添加了堆栈状态跟踪之后,字节码问题的跟踪可以精确到代码的具体行。这种可确定性并不总能让修补容易一些,但是由于可以发现问题的原因,所以可以避免所有早期的问题。因为在调试方面轻松了许多,我就可以在 1.0 版发行之前向字节码生成添加一些更重要的新特性了。
|
对于任何想研究复杂的字节码转换实现的人来说,JiBX 1.0 字节码生成架构形成了一个有趣的案例研究。我在开发期间学到的许多教训适用于不同的字节码框架,而且预先知道缺陷可以帮您避免重蹈我在 JiBX 1.0 生产发行版的开发过程中犯的错误。
下个月,我将把 JiBX 1.0 放在一边,转移到目前针对 2.0 版 正在进行的修改。这些变化包括:对编译器代码生成进行完全重写,添加用源代码增强代替字节码增强的选项。在与字节码生成相同的框架内处理源代码生成,肯定会产生一些有趣的新问题。下个月请回来,看看我用了什么手段让这种不太可能的技术组合为下一代 JiBX 框架工作。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130201/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/374079/viewspace-130201/