内容


用 jpa2web 生成 Ajax J2EE Web 应用程序

重用 JPA 注解并创建 Web 用户界面

Comments

jpa2web 是什么?

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、PhoneReservation)同时形成一对一、多对多和一对多关系。

清单 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 关系。

Customer-CreditCard:一对一

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

图 6. 填写信用卡详细信息
填写信用卡详细信息
填写信用卡详细信息

Customer-Phone:一对多

Customer-Phone 生成一个网格,用户可以在其中输入新电话号码的详细信息。按 Add Row 按钮就会创建一个用于输入新电话号码的新行。

Customer-Reservation:多对多

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 应用程序的运行情况

现在已经了解了 jpa2web 应用程序的性质,接下来看看它的运行情况。按照以下步骤用 Apache Tomcat 运行 jpa2web 应用程序:

  1. 从 Sourceforge 下载 jpa2web
  2. 将下载文件解压到磁盘上的一个文件夹(下面将这个位置称为 [dir])。
  3. 从 Apache 站点下载 Tomcat 并安装在一个文件夹中(下面将这个位置称为 [tomcatdir])。
  4. 将 [dir]/zklibs 和 [dir]/jboss_libs 中的 JAR 文件复制到 [tomcatdir]/lib。
  5. 下载适当的 JDBC 驱动程序并将 .jar 文件复制到 [tomcatdir]/lib。
  6. 在 [dir]/modelsrc 文件夹下面编写领域模型源文件(带 JPA 注解的 bean)。
  7. 修改 [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>
  1. 修改 [rootdir]/jpa2web-loader.xml 文件,包含希望生成的文件的包。
  2. 在 build-tomcat.xml ANT 文件中修改 Tomcat 的路径并执行默认目标:$ ant -buildfile build-tomcat.xml
  3. 现在,在 [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 或希望进一步了解它的功能,请与作者联系。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology, XML
ArticleID=285453
ArticleTitle=用 jpa2web 生成 Ajax J2EE Web 应用程序
publish-date=02042008