Spring 迁移到 Java EE,第 3 部分

作者:David Heffelfinger

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

2012 年 4 月发布

下载:

下载Java EE

下载NetBeans IDE

简介

在本系列文章的第 1 部分第 2 部分中,我们使用 JavaServer Faces (JSF) 2.0、Enterprise JavaBeans (EJB) 3.1 和 Java Persistence API (JPA) 2.0 从 Spring 的 Pet Clinic MySQL 模式生成了一个完整的 Java EE 应用程序。我们只用几次单击,即开发出了一个应用程序,其功能等同于该示例 Spring 应用程序所提供的功能。

我们还分析了生成的代码,注意到它使用了一些很好的 JSF 特性(如 Facelets 模板技术、数据模型和转换器),以及一些高级 JPA 特性(如可用于重新生成表的批注属性),同时保留了诸如最大允许长度和字段是否可为空之类的信息。它还包括 bean 验证支持。

在本文中,我们将稍稍调整该应用程序以使其对用户更加友好。生成的应用程序在有些页面上显示主键,这些键是代理主键,这意味着它们没有业务价值,只是用作唯一标识符,因此没有理由让它们对用户可见。此外,我们将修改一些生成的标签以使它们对用户更友好。

调整用户界面

以下屏幕截图显示了所生成应用程序的外观。

图 1:生成的应用程序

我们可以进行一些小改进。首先,我们要更改页面的通用标题。

查看生成的 index.xhtml 文件,可以见到以下标记:

    <h:head>

        <title>Facelet Title</title>

        <h:outputStylesheet name="css/jsfcrud.css"/>

    </h:head>

JSF 2.0 <h:head> 标记类似于标准 HTML <head> 标记。可以通过修改 <title> 标记内的文本来更改页标题,如下所示:

<title>Pet Clinic</title>

修改此页面时,还可以进行一些其他的简单修改:添加一个含页标题的 <h1> 标记,并使用对用户更友好的措辞修改各个链接文本。

进行这些简单更改之后,应用程序现在如下所示:

图 2:修改后的索引页面

在应用程序中导航,可以看到有些标签明显是从 JPA 实体中的对应属性生成的。例如,单击 Display All Owners 链接将转到显示数据库中所有宠物主人的页面。

图 3:生成的 Owner 表

如果查看生成上表的页面的标记代码,可以看到这些标签(大多数其他标签也是)来自于名为“bundle”的资源包。

注意,名字和姓氏标题分别为 FirstNameLastName。这是因为这些标签来源于 JPA Owner 实体中的对应属性。

清单 1. 标签资源包

<h:dataTable value="#{ownerController.items}" var="item" border="0"

             cellpadding="2" cellspacing="0"

             rowClasses="jsfcrud_odd_row,jsfcrud_even_row"

             rules="all" style="border:solid 1px">

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListOwnerTitle_id}"/>

        </f:facet>

        <h:outputText value="#{item.id}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListOwnerTitle_firstName}"/>

        </f:facet>

        <h:outputText value="#{item.firstName}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListOwnerTitle_lastName}"/>

        </f:facet>

        <h:outputText value="#{item.lastName}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListOwnerTitle_address}"/>

        </f:facet>

        <h:outputText value="#{item.address}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListOwnerTitle_city}"/>

        </f:facet>

        <h:outputText value="#{item.city}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListOwnerTitle_telephone}"/>

        </f:facet>

        <h:outputText value="#{item.telephone}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value=" "/>

        </f:facet>

        <h:commandLink action="#{ownerController.prepareView}"

                       value="#{bundle.ListOwnerViewLink}"/>

        <h:outputText value=" "/>

        <h:commandLink action="#{ownerController.prepareEdit}"

                       value="#{bundle.ListOwnerEditLink}"/>

        <h:outputText value=" "/>

        <h:commandLink action="#{ownerController.destroy}"

                       value="#{bundle.ListOwnerDestroyLink}"/>

    </h:column>

</h:dataTable>

查看该应用程序的 faces-config.xml 文件可以发现该“bundle”映射到了一个名为 Bundle.properties 的属性文件。

清单 2. 修改标签

<faces-config version="2.0"

    xmlns="http://java.sun.com/xml/ns/javaee"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

    http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

    <application>

        <resource-bundle>

            <base-name>/Bundle</base-name>

            <var>bundle</var>

        </resource-bundle>

    </application>

</faces-config>

通过修改 Bundle.properties(这里未显示,因为它太长了,它作为本文代码下载的一部分显示)中的标签,可以使该应用程序对用户更友好。在该文件中进行一些简单替换之后,Owners 页面现在如下所示:

图 4:修改后的 Owner 表

页标题和页眉现在为 Owners,而不是先前的通用值 List。我们还将名字和姓氏标题更改为了 First NameLast Name,并将删除主人的链接从 Destroy 更改为了 Delete

现在标签已修改好,下面我们要从表中删除 Id 列,因为主键不会为用户提供任何有用的信息。这只需通过从 JSF 数据表删除第一个 <h:column> 标记即可完成。

清单 3. 从表中删除 Id 列

<h:dataTable value="#{ownerController.items}" var="item"

             border="0" cellpadding="2" cellspacing="0"

             rowClasses="jsfcrud_odd_row,jsfcrud_even_row"

             rules="all" style="border:solid 1px">

    <h:column>

        <f:facet name="header">

            <h:outputText

                value="#{bundle.ListOwnerTitle_id}"/>

        </f:facet>

        <h:outputText

            value="#{item.id}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText

                value="#{bundle.ListOwnerTitle_firstName}"/>

        </f:facet>

        <h:outputText value="#{item.firstName}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText

                value="#{bundle.ListOwnerTitle_lastName}"/>

        </f:facet>

        <h:outputText value="#{item.lastName}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText

                value="#{bundle.ListOwnerTitle_address}"/>

        </f:facet>

        <h:outputText value="#{item.address}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText

                value="#{bundle.ListOwnerTitle_city}"/>

        </f:facet>

        <h:outputText value="#{item.city}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText

                value="#{bundle.ListOwnerTitle_telephone}"/>

        </f:facet>

        <h:outputText value="#{item.telephone}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value=" "/>

        </f:facet>

        <h:commandLink

            action="#{ownerController.prepareView}"

            value="#{bundle.ListOwnerViewLink}"/>

        <h:outputText value=" "/>

        <h:commandLink

            action="#{ownerController.prepareEdit}"

            value="#{bundle.ListOwnerEditLink}"/>

        <h:outputText value=" "/>

        <h:commandLink

            action="#{ownerController.destroy}"

            value="#{bundle.ListOwnerDestroyLink}"/>

    </h:column>

</h:dataTable>

经过这一简单更改之后,Owners 列表现在如下所示:

图 5:修改后的 Owner 表,无 ID

我们从表中删除了 Id 列,但在创建或编辑主人时,Id 域还是可见。

清单 4:从 Create 页面中删除 Id 域

<h:panelGrid columns="2">

    <h:outputLabel value="#{bundle.CreateOwnerLabel_id}" for="id" />

    <h:inputText id="id" value="#{ownerController.selected.id}"

                 title="#{bundle.CreateOwnerTitle_id}" required="true"

                 requiredMessage="#{bundle.CreateOwnerRequiredMessage_id}"/>

    <h:outputLabel value="#{bundle.CreateOwnerLabel_firstName}"

                   for="firstName" />

    <h:inputText id="firstName" value="#{ownerController.selected.firstName}"

                 title="#{bundle.CreateOwnerTitle_firstName}" />

    <h:outputLabel value="#{bundle.CreateOwnerLabel_lastName}" for="lastName" />

    <h:inputText id="lastName" value="#{ownerController.selected.lastName}"

                 title="#{bundle.CreateOwnerTitle_lastName}" />

    <h:outputLabel value="#{bundle.CreateOwnerLabel_address}" for="address" />

    <h:inputText id="address" value="#{ownerController.selected.address}"

                 title="#{bundle.CreateOwnerTitle_address}" />

    <h:outputLabel value="#{bundle.CreateOwnerLabel_city}" for="city" />

    <h:inputText id="city" value="#{ownerController.selected.city}"

                 title="#{bundle.CreateOwnerTitle_city}" />

    <h:outputLabel value="#{bundle.CreateOwnerLabel_telephone}"

                   for="telephone" />

    <h:inputText id="telephone" value="#{ownerController.selected.telephone}"

                 title="#{bundle.CreateOwnerTitle_telephone}" />

</h:panelGrid>

删除以上高亮显示行之后,Create 页面现在如下所示:

图 6:修改后的 Owner 表,无 ID 2

但我们无法再成功创建新的主人,因为 JPA Owner 实体中的 Id 字段使用 @NotNull 批注进行了修饰。

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    @Basic(optional = false)

    @NotNull

    @Column(name = "id", nullable = false)

    private Integer id;

纠正这一问题非常简单:只需从 Id 字段删除 @NotNull 批注。现在,可以创建新的宠物主人而无需显式输入主键。

图 7:修改后的 Create 页面

保存了新创建的主人项之后,该项将列出在 Owners 表中。

图 8:表中的新主人

还可以使用 NetBeans 的内置数据库工具在数据库中查看该项。

图 9:数据库中的新主人

JPA 主键生成器自动在 Id 字段中添加了一值。

至此,我们已通过修改自动生成的标签以及删除不必要的输入字段隐藏与用户无关的信息,提高了生成的应用程序的可用性。

该应用程序中的许多表都存在一对多的关系。当查看关系的“一”这一侧的相关信息时,可以看到关系的“多”那一侧的一些数据 — 例如,宠物与主人之间存在一对多关系(宠物只能有一个主人,但主人可以有多个宠物)。我们来看一下列出系统中所有宠物的页面。

图 10:生成的宠物列表

注意,TypeOwner 列的值是对应于关系另一端的主键的数字。我们应修改该页面以便代之以显示文本描述。

从生成宠物列表的页面的标记代码中可以看出这些值是如何获得的。

清单 5. 获取 Type 和 Owner 列的值

<h:dataTable value="#{petController.items}" var="item" border="0"

cellpadding="2" cellspacing="0"

rowClasses="jsfcrud_odd_row,jsfcrud_even_row"

rules="all" style="border:solid 1px">

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListPetTitle_name}"/>

        </f:facet>

        <h:outputText value="#{item.name}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListPetTitle_birthDate}"/>

        </f:facet>

        <h:outputText value="#{item.birthDate}">

            <f:convertDateTime pattern="MM/dd/yyyy" />

        </h:outputText>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListPetTitle_type}"/>

        </f:facet>

        <h:outputText value="#{item.type}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value="#{bundle.ListPetTitle_owner}"/>

        </f:facet>

        <h:outputText value="#{item.owner}"/>

    </h:column>

    <h:column>

        <f:facet name="header">

            <h:outputText value=" "/>

        </f:facet>

        <h:commandLink action="#{petController.prepareView}"

        value="#{bundle.ListPetViewLink}"/>

        <h:outputText value=" "/>

        <h:commandLink action="#{petController.prepareEdit}"

        value="#{bundle.ListPetEditLink}"/>

        <h:outputText value=" "/>

        <h:commandLink action="#{petController.destroy}"

        value="#{bundle.ListPetDestroyLink}"/>

    </h:column>

</h:dataTable>

这里的列显示 Pet 类的 TypeOwner 属性的字符串表示。以此推理,我们会认为这些对象的 toString() 方法将返回其 Id。下面我们来看一看 Owner 的 toString() 实现。

@Override

public String toString() {

    return "com.ensode.petclinicjavaee.entity.Owner[ id=" + id + " ]";

}

如您所见,toString() 并不返回 ID,那么这里发生了什么?在页面上显示出 ID 的原因在于,NetBeans 向导自动为所有 JPA 实体生成了 JSF 转换器。Owner 的转换器定义为 OwnerController.java 的内部类。

清单 6. Owner 的转换器定义

    @FacesConverter(forClass = Owner.class)

    public static class OwnerControllerConverter implements Converter {

        public Object getAsObject(FacesContext facesContext,

                UIComponent component, String value) {

            if (value == null || value.length() == 0) {

                return null;

            }

            OwnerController controller =

                    (OwnerController) facesContext.getApplication().

                    getELResolver().

                    getValue(facesContext.getELContext(), null,

                    "ownerController");

            return controller.ejbFacade.find(getKey(value));

        }

        java.lang.Integer getKey(String value) {

            java.lang.Integer key;

            key = Integer.valueOf(value);

            return key;

        }

        String getStringKey(java.lang.Integer value) {

            StringBuffer sb = new StringBuffer();

            sb.append(value);

            return sb.toString();

        }

        public String getAsString(FacesContext facesContext,

                UIComponent component, Object object) {

            if (object == null) {

                return null;

            }

            if (object instanceof Owner) {

                Owner o = (Owner) object;

                return getStringKey(o.getId());

            } else {

                throw new IllegalArgumentException("object " + object +

                        " is of type " + object.getClass().getName() +

                        "; expected type: " + OwnerController.class.getName());

            }

        }

    }

这里要特别注意的是 getAsString() 方法,它调用一个名为 getStringKey() 的方法,该方法只是将 Owner 对象的 Id 属性转换成一个字符串,然后返回该主人 ID 的字符串表示。我们可以将其替换为返回主人名字和姓氏的代码,如下所示:

清单 7. 返回主人的名字和姓氏

        public String getAsString(FacesContext facesContext,

                UIComponent component, Object object) {

            if (object == null) {

                return null;

            }

            if (object instanceof Owner) {

                Owner o = (Owner) object;

                return new StringBuilder(o.getFirstName()).append(

                           " ").append(o.getLastName()).toString();

            } else {

                throw new IllegalArgumentException("object " + object +

                        " is of type " + object.getClass().getName() +

                        "; expected type: " + OwnerController.class.getName());

            }

        }

这样更改并且对生成的所有转换器进行类似更改之后,宠物列表页面现在如下所示:

图 11:修改后的宠物列表

现在应用程序调整基本完成,但还需要再进行一个简单的更改。当一个正在修改或创建的实体与另一个实体具有一对多关系时,将显示一个下拉列表以便选择关系的“多”那一部分 — 例如,Edit Pet 页面有 TypeOwner 下拉列表。

图 12:生成的 Edit Pet 页

注意,下拉列表中显示的值并不完全是用户友好的。所显示的是生成的每个对应实体的 toString() 方法的返回值。通过遍历所有生成的 toString() 方法并修改它们以返回关于对象的恰当描述,可以使下拉列表对用户更友好。

对相应的 toString() 方法进行简单修改之后,Edit Pet 页面现在如下所示:

图 13:修改后的 Edit Pet 页面

此时,我们得到了一个功能完整、用户友好的 Pet Clinic 应用程序版本。它不是 Spring 版本的简单移植,但提供等同的功能。我们的版本让我们能够管理兽医或兽医专科医师,而 Spring 版本不能。另一方面,Spring 版本在单一页面中显示主人、宠物和就诊信息,而在我们的版本中,这些实体单独管理和显示。但是,这些应用程序基本是等同的。

比较 Pet Clinic 的 Spring 和 Java EE 版本

至此,对我们版本的 Pet Clinic 应用程序的调整已经完成,下面我们要将其与 Spring 版本进行比较。

文件大小和所需库

首先考虑生成的应用程序的大小。应用程序的 Java EE 版本只有不到 2 MB 大小。

图 14:Java EE War 文件

相比之下,Spring 版本为 17 MB。

图 15:Spring War 文件

应用程序的 Spring 版本是 Java EE 版本大小的 9 倍多。主要原因是 Spring 应用程序通常部署到 Tomcat 之类的 Servlet 容器,并需要包括一些库以实现事务、对象关系映射功能等。如果您使用的是 Java EE 兼容应用服务器,这些服务是现成提供的。

我们来看一看 Spring 版本的应用程序的 WEB-INF/lib 目录。

图 16:WEB-INF/lib 目录 

应用程序的 Spring 版本包含 34 个需要与该应用程序捆绑在一起的库。

现在我们来看一看 Java EE 版应用程序的 WEB-INF 目录。

图 17:Java EE WEB-INF/lib 目录

应用程序的 Java EE 版本在 WEB-INF 下甚至都没有 lib 目录 — 一切所需均由 Java EE 应用服务器提供,因此无需外部库。

为完全公平起见,值得注意的是,应用程序的 Spring 版本支持三种数据库访问方式(使用 Hibernate、JPA 或 JDBC),而应用程序的 Java EE 版本只支持 JPA,但这让 Spring 应用程序往往比其 Java EE 等同版本具有更多的依赖关系。

配置

我们已查看了开发应用程序的各个版本所需的库的数量,下面我们来看看所需的配置文件数量。

浏览应用程序的 Spring 版本,我们找到以下 XML 配置文件。

文件

行数

context.xml

7

web.xml

141

applicationContext-jpa.xml

101

Geronimo-web.xml

5

applicationContext-hibernate.xml

89

petclinic-servlet.xml

68

petclinic.hbm.xml

74

applicationContext-jdbc.xml

81

总行数

566

现在我们来看一看应用程序的 Java EE 版本的配置文件。

文件

行数

web.xml

24

faces-config.xml

16

persistence.xml

8

总行数

48

应用程序的 Spring 版本的配置行有几乎 12 倍之多。同样,为了公平起见,这些文件中有些只是用于选择特定的 API 以进行数据访问或用于部署到特定的应用服务器。假定我们要部署到 Tomcat 并使用 Hibernate 进行数据访问,则可以删除 geronimo-web.xmlapplicationContext-jpa.xmlapplicationContext-jdbc.xml,这会将应用程序 Spring 版本的总行数变成 462,但这仍然是应用程序 Java EE 版本所需配置的几乎 10 倍。

代码与标记

至此我们比较了文件大小、依赖关系数量以及配置量,下面来比较应用程序每个版本中的实际代码量。

应用程序的 Spring 版本的 JSP、级联样式表 (CSS) 和 Java 代码的总行数是 2,664 行,应用程序的 Java EE 版本的 XHTML、CSS 和 Java 代码的总行数是 3,768。

尽管应用程序 Spring 版本的总行数少很多,但我们要记住几件事情:

应用程序的 Java EE 版本不是 Spring 版本的简单移植。例如,Java EE 版本允许创建、更新和删除兽医及兽医专科医师,而应用程序的 Spring 版本只允许查看兽医及兽医专科医师。此外,Spring 版本有一个单一页面用于管理/查看主人、宠物和就诊,而在应用程序的 Java EE 版本中这些实体每个都有单独的页面。

另一件应记住的事情是,对于应用程序的 Java EE 版本,我们并没有实际编写许多代码和标记,因为大部分都由 NetBeans 向导生成了。

本系列文章的第 3 部分到此结束。下一部分将会比较 Java EE 和 Spring 的 API。

另请参见

Spring 迁移到 Java EE,第 1 部分

Spring 迁移到 Java EE,第 2 部分

Spring Framework

Java EE 6

关于作者

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