了解并试用新的开放源码工具 — jpa2web — 这种工具可以根据带 JPA 注解的 bean 生成基于 Ajax 的 J2EE Web 应用程序。通过使用 ZK 框架,这个工具生成的应用程序提供一个友好的基于 Ajax 的 Web 用户界面,允许用户添加、删除、搜索、修改和连接与数据库同步的对象实例。
Hibernate(见 参考资料)等工具大大简化了 Java 对象与其数据库存储之间的映射;尤其是,很容易给 Java 类加上注解,从而指定对象持久化的方式。开发人员不再需要编写大量数据库集成代码。Hibernate 解决了持久化问题;但是,仍然需要创建 Web 页面来处理这些元素。对于中等规模的 Web 应用程序,典型的开发过程可能是这样的:开发人员首先编写表示某个领域模型的 Plain Old Java Object(POJO),然后创建不同的事务和 Web 用户界面。一部分模型元素常常涉及非事务性数据。客户、国家、地区、职员和公司是业务模型的典型元素,它们由操作员维护。
为什么不生成一个 Web 表示层,让它根据带注解的 bean 创建、添加、列出、删除和搜索这些元素呢?为什么不让这个表示层产生友好的 Ajax 用户体验呢?这就是 jpa2web 工具的主要目标,它采用以下处理流程:
- 输入:带注解的 POJO bean(和可选的模板)。
- 输出:一个 Ajax Web 应用程序,它可以处理模型元素并进行持久化。
- 使用的技术:FreeMarker + ZK + Hibernate(关于这些技术的更多信息,参见 参考资料 中的链接)。
![]() |
|
这个工具主要使用注解驱动的编程方式指定 ORM 映射。可以重用其中的许多注解来定义 Web 界面或创建可修改的原型。
下面几节解释如何使用 jpa2web 把不同复杂程度的 bean 映射到 Ajax Web 用户界面。接下来,说明 jpa2web 算法的工作原理和一些基本指令。最后,描述 jpa2web 的适用范围和以后的改进。
本文使用一个领域模型示例,一些读者可能很熟悉这个模型。它借鉴了 Bill Burke 和 Richard Monson-Haefel 所写的 Enterprise JavaBeans, 3.0 中的领域模型示例(参见 参考资料 中的链接)。这个领域模型包含一个船只管理类 Ship.java
(见 清单 1),这个类是最简单的 POJO:其中只有基本数据类型的成员。
清单 1.
Ship.java
package com.titan.domain; import javax.persistence.*; @Entity public class Ship implements java.io.Serializable { private int id; private String name; private double tonnage; @Id @GeneratedValue public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getTonnage() { return tonnage; } public void setTonnage(double tonnage) { this.tonnage = tonnage; } public String toString() { return name; } } |
如果希望让 Ship.java
的实例与一个数据库保持同步,那么基本上需要两个用户界面:一个用来添加(或编辑)船只信息,另一个用来列出船只。第一个界面是一个用来输入船只信息(名称和吨位)的表单。第二个界面列出现有的船只及其字段,供用户选择和编辑(见 图 1):
图 1. 船只表单

图 1 显示 jpa2web 生成的表单。注意,有三个图标:一个用来创建新的船只,一个列出所有船只(见 图 2),另一个删除已经持久化的船只。单击 OK 按钮,就会把船只信息持久化到数据库中。注意,因为 ID 字段是一个 GeneratedValue
,所以它看起来被禁用了。
图 2 显示生成的第二个 Web 页面,这个页面列出现有的船只。用户可以单击一个船只,这时会显示前一个表单,让用户编辑船只的详细信息。
图 2. 船只列表

如果对象引用其他类的对象,那么情况会复杂一些。Cabin.java
就属于这种情况,这个类与 Ship.java
之间存在多对一关系,它也有简单的属性(见 清单 2):
清单 2.
Cabin.java
package com.titan.domain; import javax.persistence.*; @Entity public class Cabin implements java.io.Serializable { private int id; private String name; private int bedCount; private int deckLevel; private Ship ship; @Id @GeneratedValue public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getBedCount() { return bedCount; } public void setBedCount(int count) { this.bedCount = count; } public int getDeckLevel() { return deckLevel; } public void setDeckLevel(int level) { this.deckLevel = level; } @ManyToOne public Ship getShip() { return ship; } public void setShip(Ship ship) { this.ship = ship; } public String toString() { return name; } } |
jpa2web 生成一个与 Ship.java
的表单相似的表单,但是提供了将给定的船舱与某一船只联系起来的方法。这需要一个按钮,这个按钮会打开一个模态对话框,让用户选择船只。图 3 显示生成的表单,图 4 显示用来选择船只的模态对话框。ZK 可以很轻松地处理模态对话框,所以这种技术非常方便。
图 3. 船舱表单

图 4. 选择船只

现在看一个更复杂的示例。如果类与其他类形成不同的关系,那么怎么办?例如 Customer.java
类(见 清单 3)。这个类与其他类(分别是 CreditCard、Phone
和 Reservation
)同时形成一对一、多对多和一对多关系。
清单 3.
Customer.java
package com.titan.domain; import javax.persistence.*; @Entity public class Customer implements java.io.Serializable { private int id; private String firstName; private String lastName; private boolean hasGoodCredit; private Address address; private Collection<Phone> phoneNumbers = new ArrayList<Phone>(); private CreditCard creditCard; private Collection<Reservation> reservations = new ArrayList<Reservation>(); @Id @GeneratedValue public int getId() { return id; } public void setId(int id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public boolean getHasGoodCredit() { return hasGoodCredit; } public void setHasGoodCredit(boolean flag) { hasGoodCredit = flag; } @OneToOne(cascade={CascadeType.ALL}) public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } @OneToOne(cascade={CascadeType.ALL}) @IndexColumn(name="INDEX_COL_CC") public CreditCard getCreditCard() { return creditCard; } public void setCreditCard(CreditCard card) { creditCard = card; } @OneToMany(cascade={CascadeType.ALL},fetch=FetchType.EAGER) @IndexColumn(name="INDEX_COL_PHON") public Collection<Phone>getPhoneNumbers() { return phoneNumbers; } public void setPhoneNumbers(Collection<Phone> phones) { this.phoneNumbers = phones; } @ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER) @IndexColumn(name="INDEX_COL_CUS") public Collection<Reservation> getReservations() { return reservations; } public void setReservations(Collection<Reservation> reservations) { this.reservations = reservations; } public String toString() { return getLastName()+","+getFirstName(); } } |
图 5 说明如何显示清单 3 中的类。这个页面包含多个区域,我们将逐一讨论这些区域。
图 5. 客户表单

我们来看看如何显示 CreditCard 关系。
这种情况与 Cabin.java
和 Ship.java
之间的关系(多对一)相似,在处理多对一关系时会通过一个按钮打开模态表单。但是,因为 Customer-CreditCard 的关系是一对一,即每个信用卡号只与一个客户相关联,所以打开对话框来选择现有实例是不合适的。而是应该打开一个 CreditCard 表单 对话框,可以在其中输入新信用卡的完整信息(见 图 6):
图 6. 填写信用卡详细信息

Customer-Phone
生成一个网格,用户可以在其中输入新电话号码的详细信息。按 Add Row 按钮就会创建一个用于输入新电话号码的新行。
Add.. 按钮在列表框中添加一个新预订,可以用一个选择器模态对话框收集所选的预订。图 7 显示完成后的表单,其中已经添加了电话号码和预订:
图 7. 填写客户电话信息

目前,您可能不喜欢生成的窗口的外观。有两个主要问题:字段标签显示 Java 属性名,而且它们出现的次序看起来很乱。可以用 jpa2web 工具的定制注解解决这些外观问题。例如,在 Cabin 类(清单 4)中添加 MemberField
注解,会让表单有更好的外观(见 图 8)。对比修饰后的版本和原来的版本(图 3)。标题现在包含有意义的名称,字段的次序现在也更有意义了。
图 8. 修饰后的船舱表单

清单 4.
Cabin.java
中的修饰注解
@Entity public class Cabin implements java.io.Serializable { @Id @GeneratedValue @MemberField(order=1,showName="ID") public int getId() { return id; } public String getName() { return name; } @MemberField(order=2,showName="Cabin Name") @MemberField(order=2,showName="Number of Beds") public int getBedCount() { return bedCount; } @MemberField(order=3,showName="Deck Level") public int getDeckLevel() { return deckLevel; } @ManyToOne @MemberField(showName="Ship") public Ship getShip() { return ship; } .... } |
其他修饰性特性包括当某个字段可以接受一系列预定义值时,生成组合框而不是文本框。这也要使用 MemberField 注解来实现。还可以定义许多其他注解来定制生成的窗口的外观和感觉。
现在已经了解了 jpa2web 应用程序的性质,接下来看看它的运行情况。按照以下步骤用 Apache Tomcat 运行 jpa2web 应用程序:
- 从 Sourceforge 下载 jpa2web。
- 将下载文件解压到磁盘上的一个文件夹(下面将这个位置称为 [dir])。
- 从 Apache 站点下载 Tomcat 并安装在一个文件夹中(下面将这个位置称为 [tomcatdir])。
- 将 [dir]/zklibs 和 [dir]/jboss_libs 中的 JAR 文件复制到 [tomcatdir]/lib。
- 下载适当的 JDBC 驱动程序并将 .jar 文件复制到 [tomcatdir]/lib。
- 在 [dir]/modelsrc 文件夹下面编写领域模型源文件(带 JPA 注解的 bean)。
- 修改 [dir]/templates/hibernate-cfg.xml 文件以连接数据库(参见以下代码示例):
<hibernate-configuration> <session-factory name="thefactory"> <property name="hibernate.connection.driver_class"> net.sourceforge.jtds.jdbc.Driver </property> <property name="hibernate.dialect"> org.hibernate.dialect.SQLServerDialect </property> <property name="hibernate.hbm2ddl.auto">update</property> <property name="hibernate.connection.url"> jdbc:jtds:sqlserver://127.0.0.1:1433/test</property> <property name="hibernate.connection.username">sa</property> <property name="hibernate.connection.password">*</property> <property name="hibernate.max_fetch_depth">0</property> <#list windows as win><mapping class="${win.mainClass.name}" /> </#list> </session-factory> </hibernate-configuration> |
- 修改 [rootdir]/jpa2web-loader.xml 文件,包含希望生成的文件的包。
- 在 build-tomcat.xml ANT 文件中修改 Tomcat 的路径并执行默认目标:
$ ant -buildfile build-tomcat.xml
。 - 现在,在 [dir]/dist 文件夹中会生成 Web 应用程序的 Web 文件夹,还将它复制到 Tomcat 的 webapps 目录中。可以通过 http://localhost:8080/sampleapp.war(或相似的 URL)访问这个应用程序。
尽管详细解释 jpa2web 生成器的工作方式超出了本文的范围,但是下面会简要讨论一下生成器引擎背后的主要思想和方法。
输入很简单:一系列带 JPA 注解的类。读取这些源文件并构建 bean 及其相互关系的内部表示(称为装载器)。jpa2web 只实现了 EntityLoader(它读取 JPA 注解),但是可以实现其他装载器(例如,读取 XML 映射配置文件的装载器)。然后,将这个结构用作生成器的输入,生成器还以一系列 FreeMarker 模板作为输入(关于 FreeMarker 模板的更多信息参见 参考资料)。使用模板使 jpa2web 能够以非常灵活的方式生成窗口。图 9 说明这个过程的各个阶段。生成器的输出是一个 Web 应用程序的 WAR 文件夹,其中包含与每个带注解的 bean 对应的窗口,以及用来选择窗口的索引。
图 9. 生成过程

经过简化的生成器伪代码如下:
生成器算法
for each bean in the model: -create a form window called fully.classified.className.zul: for each simple field if (field is String) render textbox if (field is int/byte/short) render integerbox if (field is boolean) render checkbox if (field is double/float) render doublebox/floatbox for each OneToOne member: render a button that will open a form to create an instance of the destiny class for each ManyToOne member: render a button that will open a list to choose an instance of the destiny class for each OneToMany member: render a table with the details of the destiny class and buttons to add and delete rows for each ManyToMany member: render a listbox with the elements mapped, and a button that will open a list to choose an instance of the destiny class -create a window called list.fully.classified.className.zul which lists all of the instances -add the annotated class to the mapping area of hibernate.cfg.xml -add links to a menu bar with the two windows generated for this bean |
以上算法只是建立映射的一种方法,还有许多其他方法和解决方案,适用于不同的场景。通过使用其他注解,可以在运行时调整生成过程本身。
还有其他一些工具能够根据某种输入生成 Web 应用程序代码,其中许多是模型驱动体系结构(MDA)的完整实现。jpa2web 工具是一种轻量的注解驱动的代码生成工具,而不是 MDA 的完整实现。
![]() |
|
要想开发能够在大型部署中使用的生成器,开发人员必须解决几个问题。对于产生复杂的方向图的类模型,jpa2web 显然无法处理,尤其是在存在复杂的循环引用的情况下。到编写本文时,jpa2web 不支持一对多关系内的一对多和多对多(但是这个实现是可行的,jpa2web 发展计划可能会包含这个特性)。无论如何,必须进一步研究如何为复杂的模型生成窗口。不适合使用 jpa2web 的最明显的情况包括,创建事务性更强的 Web 应用程序,以及用户界面不与类模型直接对应的场景。
尽管存在一些限制,在许多情况下 jpa2web 仍然是一个有用的工具。通过使用 jpa2web,只需提供带注解的 bean,就能够为非事务性元素快速生成 Web 界面。还可以用它在数据库中创建必要的对象实例以执行测试,从而避免编写复杂的实体创建脚本。jpa2web 的输出只能作为需要进一步开发的原型,不应该作为最终的应用程序。生成的代码相当简单,修改其行为以及添加检验功能和额外的处理也很容易。但是,对于更大型的应用程序,jpa2web 的适用范围受到事务、安全性、并发性和多层约束的限制,需要做一些额外的集成工作。
计划实现的改进包括支持元素间关系更复杂的领域模型,添加定制的注解以便更好地控制生成的用户界面,以及从其他 Ajax 工具箱(比如 Google 的 GWT、OpenLaszo 或 Echo2)生成代码。还可能扩展生成器的输入类型,可能使用 XML 文件输入模型。如果您有兴趣帮助扩展 jpa2web 或希望进一步了解它的功能,请与作者联系。
学习
- 您可以参阅本文在 develperWorks 全球网站上的 英文原文。
- 了解关于 POJO(Plain Old Java Object) 的更多信息。
- 了解 Hibernate。
- 了解 ZK。
- 阅读 Bill Burke 和 Richard Monson-Haefel 所写的 “Enterprise JavaBeans, 3.0” 一书(O'Reilly,第 5 版,2006 年)。
- 了解 FreeMarker 模板。
- 了解关于 MDA(Model Driven Architecture) 的更多信息。
- 了解 JSR-000220 Enterprise JavaBeans 3.0。
- 了解 ZK 框架。
- 查看 ZK Live Demo。
- 了解 Red Hat Middleware, Hibernate hbm2ddl。
- 阅读 AndroMDA, an Open Source MDA Generator。
- 熟悉 Java 注解。
- 了解 Apache Struts Framework。
- 了解 JavaServer Faces 技术。
- 了解强大的 Spring Framework。
- Google Web Toolkit 可以帮助开发人员用 Java 语言构建 Ajax 应用程序。
- 了解 OpenLaszlo,这是一种用来构建富 Internet 应用程序的开放源码平台。
- 了解 Echo2。