Spring 迁移到 Java EE,第 1 部分

作者:David Heffelfinger

CTO 和热情的 Java EE 迷 David Heffelfinger 演示了使用 Java EE、JPA 和 NetBeans IDE 代替 Spring Framework 开发应用程序数据层是多么轻松。

2011 年 10 月发布

下载:

下载Java EE

下载NetBeans IDE

简介

Spring Framework 的支持者声称他们的出类拔萃的框架比 Java Platform, Enterprise Edition (Java EE) 好用得多。我是一个 Java EE 迷,这不是一个什么大秘密,我写了多本关于这一技术的书。然而,正如大多数开发人员一样,我并不总是有机会选择自己的技术体系,而且在某些情况下,我不得不使用 Spring 来开发项目。

每次开发 Spring 项目时,我都不免私下嘀咕。我知道自己将被迫浏览冗长、复杂的 XML 文件来确定项目究竟进展如何。我还知道,这个项目将有大约 10,000 个依赖关系,生成的 WAR 文件将是一个巨无霸。

使用 Java EE 时,所需要的大多数服务均由应用服务器提供。因此,所需的依赖关系的数量非常少。大多数情况下,Java EE 按例外情况提供配置,这意味着只要进行很少的配置,而对于绝大多数情况使用合理的默认值。当需要配置时,通常是通过批注来完成的,这样我只要看一下源代码就可以了解整个情况,而无需在 XML 配置文件和源代码之间来回倒换。

除了上一段中所述全部优点之外,在 Java EE 项目中工作时,我可以利用 NetBeans 提供的高级工具。如果我运气好,使用的是 GlassFish Server 开源版或 Oracle GlassFish Server 作为应用服务器,我就可以利用“保存时部署”特性,这意味着每次我在项目中保存文件时,更新后的版本就会在后台自动部署到 GlassFish 服务器。我只需在浏览器中重新加载该页,更改就会立即反映出来。这会节省大量时间,每次我被迫回到编辑-保存-部署-刷新循环时,都觉得像在工作时一只手绑在背后一样束手束脚。

在这个系列文章中,我们将使用 Java EE 重写随 Spring 提供的示例 Pet Clinic 应用程序。在这第 1 篇文章中,我将阐述如何利用 NetBeans 提供的出色 Java EE 工具快速开发与 Spring 版应用程序具有同等功能的应用程序。该 Java EE 版应用程序的用户界面使用 JavaServer Faces (JSF) 来开发,数据访问对象 (DAO) 使用 Enterprise JavaBeans (EJB) 3.1 会话 bean 来实现,数据访问则由 Java Persistence API (JPA) 2.0 提供。

在这第一部分,我们开始开发该 Java EE 版应用程序,首先我们通过现有数据库生成持久性层。在第 2 部分,我们将了解 NetBeans 如何帮助我们生成用作 DAO 的 EJB 3.1 会话 bean,以及 JSF 2.0 用户界面。

建立项目

这里我们假定本地工作站上安装了 MySQL,且 petclinic 数据库已经存在。(通过运行 Pet Clinic 中包含的 setupDB ANT 目标可以轻松创建此数据库。)

首先需要做的是创建一个新的 Web 项目,如图 1 所示。

图 1. 创建新项目

然后需要指定项目的名称和位置,如图 2 所示。通常,默认位置和文件夹就是合理的默认值。

图 2. 指定新项目的名称和位置

此时,可以选择添加该应用程序将使用的任何框架。由于我们的应用程序将使用标准 Java EE 框架,所以我们应选择 JavaServer Faces,如图 3 所示。

图 3. 选择 JavaServer Faces 作为框架

现在需要选择 Java EE 服务器和 Java EE 版本,如图 4 所示。默认值就很适合我们的项目,默认上下文路径足以满足我们的目的。

图 4. 选择服务器和 Java EE 版本

此时,我们单击 Finish 创建项目,如图 5 所示。

图 5. 新建的项目

现在我们可以开发应用程序了。

开发应用程序

NetBeans 将为我们生成开发该 Java EE 应用程序所需的大多数代码。它可以帮助我们生成 JPA 实体,以及 DAO、JSF 页和 JSF 托管 bean。

我们首先要做的是开发 JPA 实体。大多数 JPA 实现都能够从 JPA 实体自动生成数据库表;不过,反之并不正确。JPA 并不能从现有数据库表生成 JPA 实体。

因此,使用现有模式时,大多数情况下需要手动编写 JPA 实体代码,添加适当的批注、属性、getter、setter 等。不过,我们使用的是 NetBeans,它能够从现有模式自动生成 JPA 实体。只需选择 File | NewPersistence 类别,然后选择 Entity Classes from Database 文件类型,如图 6 所示。

图 6. 从数据库选择实体类

此时,需要选择一个数据源。如果尚未建立好数据源,NetBeans 允许我们即时创建一个,如图 7 所示。

图 7. 创建数据源

要即时创建新的数据源,只需输入其 Java 命名和目录接口 (JNDI) 名称。由于数据源尚不存在,我们现起一个名字。然后选择一个数据库连接,如图 8 所示。

图 8. 选择数据库连接

同样,如果未建立数据库连接以便连接到所需数据库,可以通过向导即时创建该连接。创建新的数据库连接时,首先需要选择适合于我们数据库的 Java 数据库连接 (JDBC) 驱动程序,如图 9 所示。

图 9. 选择驱动程序

在下一个屏幕中,需要输入主机、端口、数据库和用户凭证,如图 10 所示。单击标记为 Test Connection 的按钮以确保所有值都正确是个不错的主意。如果一切顺利,应看到消息“Connection Succeeded”。

图 10. 指定其他详细信息并测试连接

使用 MySQL 时,模式等同于数据库。因此,Select schema 列表为灰显,如图 11 所示。

图 11. 选择模式

此时,单击 Finish 创建数据库连接,然后继续单击 OK 直至返回 New Entity Classes from Database 屏幕,如图 12 所示。

NetBeans 尝试通过检查数据库表的名称来猜出我们的实体类的期望名称。petclinic 数据库使用复数名称作为表名(如 owners、pets、specialties 等)。不过,我们希望相应的实体名称为单数名词(Owner、Pet、Specialty 等)。为方便起见,NetBeans 允许我们在这一步修改建议的 JPA 实体类名称,我们只需双击名称,然后根据需要进行修改。

此时,我们可以选择为 JPA 实体中的每个字段生成命名查询,生成 Java API for XML Binding (JAXB) 批注,并创建持久性单元。大多数情况下,生成命名查询并创建持久性单元是个不错的主意。我们可能不需要 JAXB 批注,但有它也无妨。因此,在本示例中,我们选择生成 JAXB 批注。

图 12. 指定实体类

单击 Next 之后,可以指定映射选项,如图 13 所示。

Association Fetch 列表中,可以选择如何加载关联实体。默认行为是立即提取一对一和多对一关系,延迟提取一对多和多对多关系。我们可以选择默认行为,也可以指定将立即或延迟提取所有关系。大多数情况下,默认行为是最明智的办法。

图 13. 指定如何加载关联实体

选择 Fully Qualified Database Table Names 复选框的结果是,生成的 JPA 实体中的 @Table 批注设置有 catalog 和 schema 属性。从 JPA 实体生成数据库时将使用这些属性。

选择 Attributes for Regenerating Tables 复选框的结果是,在生成的 JPA 实体中的 @Column(某些情况下还有 @Table)批注中添加其他属性。选中此复选框时,将从数据库获取元数据,使用元数据及所获取的值向 JPA 批注添加其他属性。

如果数据库不允许映射列为空,则在相应的 @Column 批注上添加 nullable 属性(值为 false)。对于 String 类型的属性,在 @Column 批注上添加 length 属性。此属性 (attribute) 指定相应属性 (property) 允许的最大长度。对于十进制类型,将在 @Column 批注上添加 precision 精度属性(数值的位数)和 scale 属性(小数点右边的位数)。如果有任何唯一性约束,则在 @Table 批注上添加 uniqueConstraints 属性。

如果选中 Use Column Names in Relationships 复选框,则关系中生成的字段名将根据该关系中“一”那一方的列名来命名。例如,如果我们有一个名为 CUSTOMER 的表,它与一个名为 ORDERS 的表存在一对多关系,并且 CUSTOMER 表中指向 ORDERS 表中主键的列名为 ORDER_ID,则 JPA 实体中生成的字段将命名为 orderId。如果取消选中此复选框,则生成的字段将命名为 order。以我的经验,大多数情况下取消选中此复选框将产生更合理的命名。

单击 Finish 之后,可以在项目中看到生成的 JPA 实体,如图 14 所示。

图 14. 生成的 JPA 实体

我们可以看到,NetBeans 自动生成了项目所需的所有 JPA 实体,为我们省去了不少工作。Andrew Hunt 和 Dave Thomas 在其杰作《The Pragmatic Programmer》中给出了这样的建议:“如果您不懂,就不要使用向导生成的代码。”这是一条非常好的建议。

在继续之前,我们先来看看其中一个生成的实体,以确保我们已经理解这段代码。

清单 1. 查看生成的实体

package com.ensode.petclinicjavaee.entity;
 
//imports omitted for brevity
 
@Entity
@Table(name = "owners", catalog = "petclinic", schema = "")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "Owner.findAll", query = "SELECT o FROM Owner o"),
    @NamedQuery(name = "Owner.findById",
        query = "SELECT o FROM Owner o WHERE o.id = :id"),
    @NamedQuery(name = "Owner.findByFirstName",
        query = "SELECT o FROM Owner o WHERE o.firstName = :firstName"),
    @NamedQuery(name = "Owner.findByLastName",
        query = "SELECT o FROM Owner o WHERE o.lastName = :lastName"),
    @NamedQuery(name = "Owner.findByAddress",
        query = "SELECT o FROM Owner o WHERE o.address = :address"),
    @NamedQuery(name = "Owner.findByCity",
        query = "SELECT o FROM Owner o WHERE o.city = :city"),
    @NamedQuery(name = "Owner.findByTelephone",
        query = "SELECT o FROM Owner o WHERE o.telephone = :telephone")})
public class Owner implements Serializable {     private static final long serialVersionUID = 1L;     @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @NotNull
    @Column(name = "id", nullable = false)
    private Integer id;     @Size(max = 30)
    @Column(name = "first_name", length = 30)
    private String firstName;     @Size(max = 30)
    @Column(name = "last_name", length = 30)
    private String lastName;     @Size(max = 255)
    @Column(name = "address", length = 255)
    private String address;     @Size(max = 80)
    @Column(name = "city",
length = 80)     private String city;     @Size(max = 20)
    @Column(name = "telephone", length = 20)
    private String telephone;     @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
    private Collection<Pet> petCollection;       public Owner() {     }       public Owner(Integer id) {         this.id = id;     }       //getters and setters omitted for brevity       @Override     public int hashCode() {         int hash = 0;         hash += (id != null ? id.hashCode() : 0);         return hash;     }       @Override     public boolean equals(Object object) {         // TODO: Warning - this method won't work in the case the id         // fields are not set         if (!(object instanceof Owner)) {             return false;         }         Owner other = (Owner) object;         if ((this.id == null && other.id != null) ||               (this.id != null && !this.id.equals(other.id))) {             return false;         }         return true;     }       @Override     public String toString() {         return "com.ensode.petclinicjavaee.entity.Owner[ id=" + id + " ]";     }   }

清单 1 中所列 JPA 实体的实际代码非常普通(我敢说有些“乏味”)。它只是一个标准的 JavaBean,带有一些私有属性和公共的 getter 和 setter。精彩部分在于批注。

该类明显标注有 @Entity 批注,因为这是对每个 JPA 实体的要求。

接下来,我们看看 @Table 批注。使用该批注的最常见原因是通过该批注的 name 属性将 JPA 实体映射到相应的表。只有当 JPA 实体的名称与表的名称不一致(本示例中就是这种情况)时,才需要这样做。

在这个特定示例中,我们的 @Table 批注还设置有 catalog 和 schema 属性。出现这种情况是因为我们在向导中选中了 Fully Qualified Database Table Names 复选框。MySQL 不能区分“模式”和“数据库”。因此,为 schema 属性生成的值为空。“catalog”的具体所指随数据库供应商的不同而异。在 MySQL 的情况下,catalog 就是数据库名称。因此,我们将在 catalog 属性中看到相应的值。

接下来,我们看看 @XmlRootElement 批注。JAXB 使用该批注将我们的实体映射到 XML。添加该批注是因为我们在向导中选中了 Generate JAXB Annotations 复选框。在本示例中我们不会用到此功能,但有它也无妨,特别是这是我们毫不费力地获得。

生成的 @NamedQueries 批注封装了所有生成的 @NamedQuery 批注。NetBeans 向导为实体中的每个字段生成一个 @NamedQuery 批注。JPA 的命名查询允许我们直接在相应的 JPA 实体中定义 Java 持久性查询语言 (JPQL) 查询,这意味着我们无需在代码中的其他地方硬编码查询。

@NamedQuery 批注中定义的 JPQL 查询可通过 JPA EntityManager 中的 createNamedQuery() 方法进行访问。以冒号 (:) 开头的标识符是命名参数。这些参数需要在执行查询之前由相应的值来替换,这通过对 Query 对象调用 setParameter() 方法来完成。

@Id 批注指定实体的 id 属性是其主键。NetBeans 向导检测到数据库中相应表中的主键自动递增,于是使用相应的 JPA 主键生成策略,该策略由 @GeneratedValue 批注表示。

@Id 字段的 @Column 批注的 nullable 属性设置为 false。NetBeans 向导检测到数据库中的相应列不接受空值,于是自动在批注中添加了此属性。类似地,各个 String 类型字段的各个 @Column 批注有一个 length 属性。该属性的值对应于数据库中相应列所允许的最大长度。

添加 nullable 和 size 属性是因为我们在向导中选中了 Attributes for Regenerating Tables 复选框。

@Basic 批注是特定于 JPA 的。将其 optional 属性设置为 false 可防止尝试持久保存该批注所修饰属性为空值的实体。

@NotNull 和 @Size 批注是 Java EE 6 中引入的新特性 Bean Validation 的一部分。

@Size 批注允许我们指定字段可以具有的最小(清单 1 中未显示)和最大长度。该实体中的值来源于相应的数据库列。@NotNull 批注使被批注的字段不能为空。@NotNull 和 @Size 批注是 Bean Validation 规范的一部分,总是由向导在合适时添加。@Column 的相应属性(nullable 和 length)只在选中了 Attributes for Regenerating Tables 复选框时才会添加。前者用于验证,后者用于从 JPA 实体重新生成数据库表。

总结

我们可以看到,使用 JPA 和 NetBeans 时,开发应用程序的数据层变得非常轻松,因为大部分代码实际上由 NetBeans 向导生成。本系列文章的第 2 部分将介绍 NetBeans 如何帮助我们生成应用程序的其他层。

另请参见

关于作者

David Heffelfinger 是总部位于大华盛顿特区的软件咨询公司 Ensode Technology, LLC 的首席技术官。从 1995 年开始,他就一直专业从事软件的架构、设计和开发工作,并且从 1996 年开始就使用 Java 作为他的主要编程语言。他参与过多家客户的许多大型项目,这些客户包括美国国土安全部、Freddie Mac、Fannie Mae 和美国国防部。他拥有南方卫理公会大学的软件工程硕士学位。