Spring 迁移到 Java EE,第 2 部分

作者:David Heffelfinger

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

2011 年 12 月发布

下载:

下载Java EE

下载NetBeans IDE

简介

本系列文章的第 1 部分,我们开始用 Java Platform, Enterprise Edition (Java EE) 完整开发重写了 Spring 的 Pet Clinic 示例应用程序。我们使用 Java Persistence API (JPA) 2.0 开发了应用程序的持久层,并看到了 NetBeans 如何帮助我们从现有数据库模式生成大多数持久层。

我们还分析了生成的代码,注意到它使用了一些高级 JPA 特性(包括可用于重新生成表的批注属性),并保留了如最大允许长度和字段是否可为空以及 bean validation 支持等信息。

在这一部分,我们将继续重写 Pet Clinic 应用程序,再次充分利用 NetBeans 中提供的 Java EE 工具。我们将开发充当数据访问对象 (DAO) 的 Enterprise JavaBeans (EJB) 3.1 会话 bean 以及 JavaServer Faces (JSF) 托管 bean 和页面。

:本文所述示例的源代码可从这里下载。

生成会话 Bean、托管 Bean 和 JSF 页面

我们已经有了 JPA 实体,现在需要开发 JSF 页面、JSF 托管 bean 和一些 DAO。我们可以使用 NetBeans 的 JSF Pages from Entity Classes 向导一次全部生成它们。可以通过转到 File | New,选择 Persistence 类别并选择 JSF Pages from Entity Classes 文件类型访问此向导,如图 1 所示。

jsf

图 1. JSF Pages from Entity Classes 向导

然后需要选择一个或多个现有 JPA 实体。可以通过单击 Add All 按钮全部选择它们,如图 2 所示。

图 2. 选择 JPA 实体

下一步,需要为生成的会话 bean 和 JSF 托管 bean 选择一个程序包,如图 3 所示。

图 3. 选择程序包

输入所有需要的信息之后,NetBeans 将生成几个 JSF 页面。这些页面包含对项目中 JPA 实体的所有 CRUD(创建、读取、更新、删除)操作的用户界面,如图 4 所示。

图 4. NetBeans 生成的 JSF 页面

此外,NetBeans 会生成一些 JSF 托管 bean 形式的控制器(MVC 中的“C”),以及充当 DAO 的会话 bean,加上一些实用程序类。参见图 5。

图 5. NetBeans 生成的控制器

此时,我们有了使用 JSF 2.0、EJB 3.1 会话 bean 和 JPA 2.0 托管 bean 的完整 Java EE 6 应用程序。

生成的应用程序的实际运行

只需右键单击项目并选择 Run 即可运行应用程序,如图 6 所示。

图 6. 运行应用程序

此时,将启动默认浏览器并可查看运行中的应用程序,如图 7 所示。

图 7. 运行中的应用程序

可以看到,生成的索引页面上有管理项目中所有实体的链接。

单击其中任何一个链接,可以看到一个显示对应的数据库数据的页面,如图 8 所示。

图 8. 查看对应的数据库数据

可以看到,NetBeans 向导生成一个创建新项目的链接,以及 View(读取)、Edit(更新)和 Destroy(删除)每项的链接。这些链接为我们的应用程序提供了 CRUD 功能。

单击任何一项的 View 链接即可查看对应项的所有信息,如图 9 所示。

图 9. 查看对应项的信息

在图 9 中,可以看到其中一个宠物的信息。注意图中显示了主键。此信息与用户无关;因此应删除它。而且,与用户友好的文本表示不同,可以看到对应类型和所有者的主键。可以轻松修改生成的代码以进行这些更改(稍后详述)。

从视图页或从列表页单击 Edit 链接,即可修改数据库中的现有行,如图 10 所示。

图 10. 修改数据库中的现有行

生成的页面可以使用一些小修改。例如,生成的 JPA 实体使用 JPA 主键生成;因此不需要主键字段。列表中的 Type 和 Owner 选项标记不完全是用户友好的。这些问题可以通过对生成的代码进行一些小修改(稍后详述)来轻松解决。

还可以通过单击 Create New Pet 链接向数据库添加新行。(该用户界面与 Edit 页面基本相同,因此不在此显示。)通过单击 Destroy 链接可以从数据库删除一行。

检查生成的代码

NetBeans 向导为我们生成许多代码,包括 JSF 页面、JSF 托管 bean、实用程序类和 EJB 3.1 会话 bean。

生成的 JSF 页面是相当简单的标准 JSF 页面,因此不在此详述。值得一提的是,生成的 JSF 页面利用了 Facelets 模板技术,从而可以根据需要轻松修改板内所有布局。生成的模板被相应地命名为 template.xhtml。参见清单 1。

<?xml version='1.0' encoding='UTF-8' ?> 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title><ui:insert name="title">Default Title</ui:insert></title>
        <h:outputStylesheet name="css/jsfcrud.css"/>
    </h:head>
    <h:body>
        <h1>
             <ui:insert name="title">Default Title</ui:insert>
        </h1>
        <p>
             <ui:insert name="body">Default Body</ui:insert>
        </p>
     </h:body>
</html> 

清单 1. 生成的 Facelets 模板

毫不奇怪,生成的模板使用 Facelets 的 <ui:insert> 标记来标记将在使用该模板的所有页面中发生更改的区域。

可以通过打开几乎任何一个生成页面看到对应的 <ui:define> 标记,如清单 2 所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets" 
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

  <ui:composition template="/template.xhtml">
        <ui:define name="title">             
             <h:outputText value="#{bundle.ViewPetTitle}"></h:outputText>
        </ui:define>
        <ui:define name="body">
             <!-- Content omitted for brevity -->
        </ui:define>
  </ui:composition>
</html>

清单 2.“View Pet”JSF 页面

还值得一提的是页面样式由自动生成的 CSS 样式表来确定。生成的 JSF 页面使用 JSF 2.0 中引入的标准资源目录特性。CSS 样式表通过 Facelets 模板中的以下标记来包含:

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

默认情况下,JSF 实现将查找一个名为 resources 的目录,它在 META-INF 下或 WAR 文件的根下。然后它将在 resources 目录下查找一个名为 css 的子目录,然后查找该目录下的指定 CSS 样式表。通过在 NetBeans 项目视图中展开对应的目录,可以看到目录结构,如图 11 所示。

图 11. 查看目录结构

除了页面之外,该向导还会生成一些 JSF 托管 bean,在我们的应用程序中充当控制器。参见图 12。

图 12. 生成的 JSF 托管 Bean

控制器中的大多数方法基本上都是传递式方法,它们调用会话 bean EntityManager facade 中的对应方法。不过,其中有几个值得研究一下。参见清单 3。

package com.ensode.petclinicjavaee.managedbean;

import com.ensode.petclinicjavaee.entity.Pet;
/* Imports Omitted */

@ManagedBean(name = "petController")
@SessionScoped
public class PetController implements Serializable {

    private Pet current;
    private DataModel items = null;
    @EJB
    private com.ensode.petclinicjavaee.session.PetFacade ejbFacade;
    private PaginationHelper pagination;
    private int selectedItemIndex;

    public PetController() {
    }

   /* getSelected() method omitted */

   /* getFacade() method omitted */

    public PaginationHelper getPagination() {
        if (pagination == null) {
            pagination = new PaginationHelper(10) {

                @Override
                public int getItemsCount() {
                    return getFacade().count();
                }

                @Override
                public DataModel createPageDataModel() {
                    return new ListDataModel(getFacade().findRange(new int[]{
                                getPageFirstItem(), getPageFirstItem() +
                                getPageSize()}));
                }
            };
        }
        return pagination;
    }

    /* prepareList() method omitted */

    public String prepareView() {
        current = (Pet) getItems().getRowData();
        selectedItemIndex = pagination.getPageFirstItem() + getItems().
                getRowIndex();
        return "View";
    }

    public String prepareCreate() {
        current = new Pet();
        selectedItemIndex = -1;
        return "Create";
    }

    /* create method omitted */

    public String prepareEdit() {
        current = (Pet) getItems().getRowData();
        selectedItemIndex = pagination.getPageFirstItem() + getItems().
                getRowIndex();
        return "Edit";
    }

    /* update method omitted */

    /* destroy method omitted */

    /* destroyAndView() method omitted */

    /* performDestroy() method omitted */

    /* updateCurrentItem() method omitted */

    public DataModel getItems() {
        if (items == null) {
            items = getPagination().createPageDataModel();
        }
        return items;
    }

    private void recreateModel() {
        items = null;
    }

    public String next() {
        getPagination().nextPage();
        recreateModel();
        return "List";
    }

    public String previous() {
        getPagination().previousPage();
        recreateModel();
        return "List";
    }

    public SelectItem[] getItemsAvailableSelectMany() {
        return JsfUtil.getSelectItems(ejbFacade.findAll(), false);
    }

    public SelectItem[] getItemsAvailableSelectOne() {
        return JsfUtil.getSelectItems(ejbFacade.findAll(), true);
    }

    @FacesConverter(forClass = Pet.class)
    public static class PetControllerConverter implements Converter {

        public Object getAsObject(FacesContext facesContext,
                UIComponent component, String value) {
            if (value == null || value.length() == 0) {
                return null;
            }
            PetController controller = (PetController) facesContext.
                    getApplication().getELResolver().
                    getValue(facesContext.getELContext(), null, "petController");
            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 Pet) {
                Pet o = (Pet) object;
                return getStringKey(o.getId());
            } else {
                throw new IllegalArgumentException("object " + object +
                        " is of type " + object.getClass().getName() +
                        "; expected type: " + PetController.class.getName());
            }
        }
    }
}

清单 3. 控制器方法

注意,JSF 托管 bean 有一个使用 @FacesConverter 批注来批注的内部类。该批注是在 JSF 2.0 中引入的,用于将被批注的类标记为 JSF 转换器。

JSF 转换器将 Java 对象转换成字符串以及将字符串转换成 Java 对象。getAsObject() 方法接受一个字符串并将其转换成对应的对象。生成的该方法的实现接受 JPA 实体的 id 属性的字符串值,将其转换成对应的整数值,然后通过生成的会话 facade 查询数据库以获取对应的 JPA 实体对象。

生成的 getAsString()simply 以字符串的形式返回 JPA 实体的 id 属性的值。

还应注意一件事。生成的 JSF 托管 bean 使用 JSF DataModel 实现,如清单 4 所示。通过 JSF DataModel 界面可以轻松操作 JSF 数据表中显示的数据。生成的 JSF 托管 bean 还利用一个名为 PaginationHelper 的实用程序类。该类提供分页功能,可在页面上一次显示几个项目,同时提供“previous”和“next”链接以显示列表的其余部分。

package com.ensode.petclinicjavaee.managedbean.util;

import javax.faces.model.DataModel;

public abstract class PaginationHelper {

    private int pageSize;
    private int page;

    public PaginationHelper(int pageSize) {
        this.pageSize = pageSize;
    }

    public abstract int getItemsCount();

    public abstract DataModel createPageDataModel();

    public int getPageFirstItem() {
        return page * pageSize;
    }

    public int getPageLastItem() {
        int i = getPageFirstItem() + pageSize - 1;
        int count = getItemsCount() - 1;
        if (i > count) {
            i = count;
        }
        if (i < 0) {
            i = 0;
        }
        return i;
    }

    public boolean isHasNextPage() {
        return (page + 1) * pageSize + 1 <= getItemsCount();
    }

    public void nextPage() {
        if (isHasNextPage()) {
            page++;
        }
    }

    public boolean isHasPreviousPage() {
        return page > 0;
    }

    public void previousPage() {
        if (isHasPreviousPage()) {
            page--;
        }
    }

    public int getPageSize() {
        return pageSize;
    }
}

清单 4. PaginationHelper.java

可以看到,PaginationHelper 是一个抽象类,它提供两个抽象方法:getItemsCount() 和 createPageDataModel()。每个生成的 JSF 托管 bean 提供一个匿名内部类,该类扩展 PaginationHelper 并提供这两个方法的具体实现,如清单 5 所示。

    public PaginationHelper getPagination() {
        if (pagination == null) {
            pagination = new PaginationHelper(10) {

                @Override
                public int getItemsCount() {
                    return getFacade().count();
                }

                @Override
                public DataModel createPageDataModel() {
                    return new ListDataModel(getFacade().findRange(new int[]{
                                getPageFirstItem(), getPageFirstItem() +
                                getPageSize()}));
                }
            };
        }
        return pagination;
    }  

清单 5. 扩展 PaginationHelper 的匿名内部类

getItemsCount() 实现只是调用生成的 EJB facade 上的 count() 方法,该方法返回 JPA 实体映射到的数据库表中的总行数。createPageDataModel() 方法更有趣一点。它实现延迟加载策略,即仅从数据库实时提取将在页面上显示的项。这种办法可节省内存,因为通常我们必须从数据库提取所有项,并在对不同项进行分页时将这些项保持在内存中。

我们已经查看了生成的 JSF 代码,现在来看看生成的 EJB 3.1 会话 bean。所有生成的会话 bean 扩展一个名为 AbstractFacade.java 的父类,如清单 6 所示。

package com.ensode.petclinicjavaee.session;

/* imports omitted */

public abstract class AbstractFacade {

private Class entityClass;

public AbstractFacade(Class entityClass) {
this.entityClass = entityClass;
}

protected abstract EntityManager getEntityManager();

public void create(T entity) {
getEntityManager().persist(entity);
}

public void edit(T entity) {
getEntityManager().merge(entity);
}

public void remove(T entity) {
getEntityManager().remove(getEntityManager().merge(entity));
}

public T find(Object id) {
return getEntityManager().find(entityClass, id);
}

public List findAll() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().
getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
return getEntityManager().createQuery(cq).getResultList();
}

public List findRange(int[] range) {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().
getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
javax.persistence.Query q = getEntityManager().createQuery(cq);
q.setMaxResults(range[1] - range[0]);
q.setFirstResult(range[0]);
return q.getResultList();
}

public int count() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().
getCriteriaBuilder().createQuery();
javax.persistence.criteria.Root rt = cq.from(entityClass);
cq.select(getEntityManager().getCriteriaBuilder().count(rt));
javax.persistence.Query q = getEntityManager().createQuery(cq);
return ((Long) q.getSingleResult()).intValue();
}
}

清单 6. AbstractFacade.java

注意,AbstractFacade 实现了 Facade 设计模式,即它为 JPA EntityManager 提供一个简化界面。生成的方法使用 JPA 2.0 criteria API。此 API 是对 JPA 规范的一个重要增补,允许动态构建类型安全的 JPA 查询。

由生成的会话 bean 完成的大多数工作将通过其父类执行。子类所做的只是实现一个构造器再加上一个使用 Java EE 依赖性注入机制注入的 EntityManager 实例,如清单 7 所示。

package com.ensode.petclinicjavaee.session;

/* imports omitted */

@Stateless
public class PetFacade extends AbstractFacade {

    @PersistenceContext(unitName = "PetClinicJavaEEPU")
    private EntityManager em;

    protected EntityManager getEntityManager() {
        return em;
    }

    public PetFacade() {
        super(Pet.class);
    }
}

清单 7. 注入 EntityManager

可以看到,子类中的构造器只是调用父类构造器,并传递该会话 bean 操作的 JPA 实体的类型。接着父类使用泛型将正确的类型动态添加到其 entityClass 实例变量。

只需通过使用 PersistenceContext 批注对 EntityManager 进行批注并传递永久单位名称(如 persistence.xml 中所定义)作为其 unitName 属性的值,即可注入 EntityManager 的实例。没有什么特别的地方。

总结

在本文中,我们看到生成的应用程序的实际运行,并深入查看了具体发生的情况。在本系列文章的下一部分,我们将修改生成的代码使其更加用户友好一些,并将比较 Pet Clinic 应用程序的 Java EE 和 Spring 版本。

另请参见

关于作者

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