Spring 迁移到 Java EE,第 4 部分

作者:David Heffelfinger

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

2012 年 4 月发布

下载:
下载Java EE
下载Java EE 6 教程

简介

本文是介绍如何从 Spring Framework 迁移到 Java EE 的一系列文章的第 4 部分也是最后一部分。

在前面两部分(Spring 迁移到 Java EE,第 1 部分Spring 迁移到 Java EE,第 2 部分),我们使用 Java EE(JavaServer Faces [JSF]、JSF Facelets 特性、Enterprise JavaBeans [EJB] 3.1 和 Java Persistence API [JPA])代替 Spring 编写了一个与 Spring Framework 捆绑的 Pet Clinic 示例应用程序版本。在这一过程中,我们介绍了 NetBeans 中包含的强大工具如何使我们能够以前所未有的速度开发完整的 Java EE 应用程序。然后我们分析了生成的代码,指出向导所利用的几个极好的 Java EE 特性,如 Facelets 模板技术、JSF 数据模型、自定义 JSF 转换器、JPA 批注属性(利用这些属性,我们可以完全重新生成数据库模式,同时保留最大长度及可为空和不可为空值等信息),等等。

第 3 部分,我们对生成的 Java EE 应用程序进行了少许修改使其对用户更友好。修改了一些生成的标签以使名称更易于理解,并使用对用户有意义的值替换了用户界面中显示的一些代理主键。在第 3 部分,还比较了应用程序的 Spring 和 Java EE 版本的依赖关系数量、每个版本所需的配置行数、每个版本所需的代码和标记行数,记住,我们在本系列文章中开发的应用程序的 Java EE 版本不是 Spring 版本的直接移植,该版本实际上有比 Spring Framework 捆绑版本更多的功能。

第 4 部分比较 Java EE 和 Spring 中的同等功能,主题涵盖 MVC 设计模式实现、数据访问、事务管理和依赖注入等。

模型-视图-控制器实现

无论使用 Java EE 还是 Spring,使用模型-视图-控制器 (MVC) 设计模式是设计企业 Java 应用程序的事实标准方式。两种框架都提供了帮助应用程序开发人员使用此设计模式开发应用程序的方法。

MVC 模式中的模型通常是一个在应用程序层之间传递的数据对象。

Spring 中的 MVC 模型

就 Spring 来说,模型为通过其 addAttribute() 方法添加到 Spring 特有的 model 接口实现的传统 Java 对象 (POJO),如 Spring 版本的应用程序中 AddOwnerForm.java 的 setupForm() 方法(及其他位置)中所示。

 @RequestMapping(method = RequestMethod.GET)

  public String setupForm(Model model) {
      Owner owner = new Owner();
      model.addAttribute(owner);
      return "ownerForm";
  }

这么做之后,即可通过 JSP 表达式语言从视图访问模型类和属性。

<form:form modelAttribute="owner">
  <table>
    <tr>
      <th>
        First Name: <form:errors path="firstName" cssClass="errors"/>
        <br/>
        <form:input path="firstName" size="30" maxlength="80"/>
      </th>
    </tr>
.
.
.
</form>

使用 Spring 时,需要将模型属性名称指定为 Spring <form:form> 标记的 modelAttribute 属性的值,然后使用相应属性名称作为 Spring 的 form 标记库生成的各种输入域的 path 属性的值。

Java EE 中的 MVC 模型

使用 Java EE 和 JSF 时,只需要使用 @ManagedBean 批注对类进行批注,此后即可通过统一表达式语言从视图使用该类及其属性。在 JSF 版本的 Pet Clinic 应用程序中,使用 JPA 实体作为模型。每个控制器不是直接访问它们,而是有一个 getSelected() 方法来返回对应的 JPA 实体;例如,OwnerController.java 有一个 getSelected() 方法返回 Owner 的一个实例。

 public Owner getSelected() {
  if (current == null) {
    current = new Owner();
    selectedItemIndex = -1;
  }
  return current;
}

然后可以使用统一表达式语言通过调用 getSelected() 方法从视图访问 Owner JPA 实体。

<h:inputText id="firstName" value="#{ownerController.selected.firstName}"
           title="#{bundle.CreateOwnerTitle_firstName}" />

视图

MVC 设计模式中的视图负责向用户显示用户界面。

Spring 视图

Spring MVC 使用的典型视图技术是带 JavaServer Pages Standard Tag Library (JSTL) 和自定义 Spring JSP 标记的 JSP。而使用 JSF 时,Facelets 是首选的视图技术。尽管 JSP 是成熟技术,但 Facelets 可以提供卓越的开发体验。对于初学者,Facelets 页面是带有自定义命名空间的标准 XHTML 页面;此外,JSF 还针对常用功能提供相应组件。

作为演示,我们来看一看在 Spring 版本的应用程序中生成 owner 列表的清单。

<table>
  <tr>
  <thead>
    <th>Name</th>
    <th>Address</th>
    <th>City</th>
    <th>Telephone</th>
    <th>Pets</th>
  </thead>
  </tr>
  <c:forEach var="owner" items="${selections}">
    <tr>
      <td>
          <a href="owner.do?ownerId=${owner.id}">
              ${owner.firstName} ${owner.lastName}</a>
      </td>
      <td>${owner.address}</td>
      <td>${owner.city}</td>
      <td>${owner.telephone}</td>
      <td>
        <c:forEach var="pet" items="${owner.pets}">
          ${pet.name}  
        </c:forEach>
      </td>
    </tr>
  </c:forEach>
</table>

如您所见,使用 Spring,需要借助于使用嵌套 JSTL 标记的 HTML 表来生成列表。现在来看一看 Java EE 版本中的同等页面

Java EE 视图

Java EE 使用 JSF 和 Facelets 作为其默认视图技术。以下清单演示了如何使用此技术实现一个显示动态数据的表。

<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_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>

在带 Facelets 的 Java EE 版本中,我们可以利用 JSF dataTable 组件,该组件负责动态构建表格数据。一般来说,使用 JSF 和 Facelets 开发 Web 应用程序在混搭 HTML 标记与自定义技术标记上花费的时间更少,并且许多功能是现成提供的,例如上面所说的根据动态数据生成表。

控制器

MVC 中的控制器负责处理用户输入并在视图之间进行导航。

Spring 控制器

使用 Spring(2.5 版及更高版本)时,要使用 @Controller 批注对控制器进行批注。为使这正常工作,需要在 Spring MVC 配置文件中添加以下代码行:

<context:component-scan base-package="org.springframework.samples.petclinic.web"/>

该代码行是必需的,这样才能使 Spring 容器知道它需要在 base-package 属性指定的软件包中扫描 Spring 组件。尽管此方法肯定比先前的 Spring 版本更容易,且不必在 Spring XML 配置文件中对控制器进行配置,但我们仍需记得在配置文件中添加以上代码行以便能够正常进行组件扫描。

在 Spring 2.5 及更高版本中,我们通常使用 @RequestMapping 批注对控制器中的几个方法进行批注。其中一个方法将处理 HTTP GET 请求,另一个将处理 HTTP POST 请求。

<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_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>

@RequestMapping 批注的 method 属性的值指定所批注的方法将处理何种类型的 HTTP 请求。通常,处理 HTTP GET 请求的方法负责显示一个将由用户填写的表单,处理 HTTP POST 请求的方法负责处理用户输入的数据。这两个方法中的每一个必须返回一个字符串,用以指定在处理完成之后如何调度请求。

如果需要显示一个登录页面,则需要在 Spring XML 配置文件中配置相应 JSP 的解析方式,如下所示:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
    p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>

如果需要向另一个控制器发送请求,则接收控制器必须有一个使用 @RequestMapping 批注修饰的方法,并将该批注的 value 属性设置为希望处理的 URL。

在 Pet Clinic 的 Spring 版本中,ClinicController.java 有一个 ownerHandler() 方法批注如下:

@RequestMapping("/owner.do")
public ModelMap ownerHandler(@RequestParam("ownerId") int ownerId) {
  return new ModelMap(this.clinic.loadOwner(ownerId));
}

因为 @RequestMapping 批注的值为“/owner.do”,所以当它完成时控制将被交给之上的 processSubmit() 方法。

Java EE/JSF 控制器

为使用 JSF 实现 MVC 控制器,只需使用 JSF @ManagedBean 批注对控制器类进行批注。无需使用批注将类标记为控制器,也无需进行任何 XML 配置。

在 JSF 中处理 HTTP POST 请求的方法不能有输入参数,必须返回一个字符串。例如,OwnerController.java 中的以下方法将更新一个现有 owner:

public String update() {
    try {
        getFacade().edit(current);
        JsfUtil.addSuccessMessage(ResourceBundle.getBundle("/Bundle").
                getString("OwnerUpdated"));
        return "View";
    } catch (Exception e) {
        JsfUtil.addErrorMessage(e, ResourceBundle.getBundle("/Bundle").
                getString("PersistenceErrorOccured"));
        return null;
    }
}

注意,在此方法上不必使用任何批注。

可以通过添加一个统一表达式语言表达式作为组件的 action 属性的值来从 JSF 命令组件(通常是从一个命令按钮)调用控制器方法。

<h:commandLink action="#{ownerController.update}"
     value="#{bundle.EditOwnerSaveLink}"/>

记住,默认情况下,JSF 托管 bean 的名称是其类名,首字母小写。圆点后面的文本是将调用的方法的名称,根据约定我们知道它将是公开的,没有输入参数,并返回一个 string

回到上述方法,注意,如果未捕获异常,它将返回 string 值“View”— 根据约定,JSF 将查找一个名为 View.xhtml 的文件,并在该方法完成处理时导航到该页面。同样,无需在任何地方为其进行配置 — 这就是 JSF 的工作方式。

这说明 JSF 非常依赖于约定,极少需要配置。

数据访问和事务管理

在开发需要从数据库访问数据的应用程序时,习惯的做法是使用数据访问对象 (DAO) 设计模式。在此模式下,数据访问对象 (DAO) 封装了与数据库的所有交互。

Spring DAO

使用 Spring 时,DAO 通常用 @Repository 批注进行批注。该批注将类标记为 DAO 并将对象关系映射工具(通常是 Hibernate)转换成 Spring 特有的 DataAccessException

在 Spring 版本的 Pet Clinic 中,HibernateClinic.java 是一个 DAO,因此用 @Repository 批注对其进行批注。

package org.springframework.samples.petclinic.hibernate;

/* imports omitted */

@Repository
@Transactional
public class HibernateClinic implements Clinic {

     @Autowired
     private SessionFactory sessionFactory;

     @Transactional(readOnly = true)
     @SuppressWarnings("unchecked")
     public Collection<Vet> getVets() {
          return sessionFactory.getCurrentSession().createQuery(
                "from Vet vet order by vet.lastName, vet.firstName").list();
     }

     @Transactional(readOnly = true)
     @SuppressWarnings("unchecked")
     public Collection<PetType> getPetTypes() {
          return sessionFactory.getCurrentSession().createQuery(
                "from PetType type order by type.name").list();
     }

     @Transactional(readOnly = true)
     @SuppressWarnings("unchecked")
     public Collection<Owner> findOwners(String lastName) {
          return sessionFactory.getCurrentSession().createQuery(
                "from Owner owner where owner.lastName like :lastName")
                     .setString("lastName", lastName + "%").list();
     }

     @Transactional(readOnly = true)
     public Owner loadOwner(int id) {
          return (Owner) sessionFactory.getCurrentSession().load(
            Owner.class, id);
     }

     @Transactional(readOnly = true)
     public Pet loadPet(int id) {
          return (Pet) sessionFactory.getCurrentSession().load(Pet.class, id);
     }

     public void storeOwner(Owner owner) {

          // Note: Hibernate3's merge operation does not reassociate the object
          // with the current Hibernate Session. Instead, it will always copy the
          // state over to a registered representation of the entity. In case of a
          // new entity, it will register a copy as well but will not update the
          // ID of the passed-in object. To still update the IDs of the original
          // objects too, we need to register Spring's
          // IdTransferringMergeEventListener on our SessionFactory.

          sessionFactory.getCurrentSession().merge(owner);
     }

     public void storePet(Pet pet) {
          sessionFactory.getCurrentSession().merge(pet);
     }

     public void storeVisit(Visit visit) {
          sessionFactory.getCurrentSession().merge(visit);
     }

}

和 Pet Clinic 中一样,使用 @Repository 对 DAO 进行批注之后,需要通过 <bean> 标记将其注册到 Spring 容器

<bean id="clinic" class="org.springframework.samples.petclinic.hibernate.HibernateClinic"/>

否则,Spring 容器需要配置成自动扫描组件,如前所述。

处理数据库时,通常需要方法是事务性的,这样就不会有最后数据库中数据不一致的风险。在 Spring 中,这可以通过使用 @Transactional 批注对类或各个方法进行批注来实现。

在转向 Java EE 之前,值得注意的是,在 Spring 领域有一个很常见的做法,就是使用 Spring 特定的对象关系映射模板(如 HibernateTemplate)来处理数据访问。尽管这些模板多少简化了数据访问代码,但它们将应用程序绑定到了 Spring API。

Java EE DAO

使用 Java EE 时,通常利用无状态会话 bean 作为 DAO。要将 Java 类转变为无状态会话 bean,只需使用 @Stateless 批注对其进行批注。Java EE 版本的 Pet Clinic 中的 DAO 实现了 Facade 设计模式,这意味着它们将提供一个比 JPA 的 EntityManager 接口更简单的接口。

package com.ensode.petclinicjavaee.session;

//imports omitted

@Stateless
public class OwnerFacade extends AbstractFacade<Owner> {
    @PersistenceContext(unitName = "PetClinicJavaEEPU")
    private EntityManager em;

    protected EntityManager getEntityManager() {
        return em;
    }

    public OwnerFacade() {
        super(Owner.class);
    }   

}

上述 DAO(以及应用程序的 Java EE 版本中的所有其他 DAO)扩展了 AbstractFacade,后者包含处理该应用程序中所有 JPA 实体的通用方法(有关 AbstractFacade 的清单,参见本系列文章的第 2 部分)。值得注意的是,AbstractFacade 不是一个会话 bean,它用于说明会话 bean 可以扩展 POJO。在 Java EE 中,没有 Spring @Transactional 批注的等同物,因为默认情况下 EJB 中的所有方法是事务性的。

注:如果出于任何原因,需要在 EJB 中实现非事务性方法,我们可以使用 @TransactionAttribute 批注对其进行批注并将该批注的 value 属性设置为 NOT_SUPPORTED。有关详细信息,请参见
Java EE 6 教程的第 43 章。

综上所述,大多数情况下,要实现 DAO,只需向类添加 @Stateless 批注,这样我们就可以利用 EJB 容器提供的“免费”事务处理。大多数情况下,无需任何额外的批注或 XML 配置。

依赖注入

依赖注入是由容器自动注入某个类的依赖关系的设计模式。Spring 和 Java EE 都支持依赖注入。

Spring 依赖注入

使用 Spring,依赖注入通常通过 @Autowired 批注来完成。作为依赖注入的一个实际应用,我们可以看看应用程序的 Spring 版本中 Clinic DAO 接口的 Hibernate 实现。

package org.springframework.samples.petclinic.hibernate;

//imports omitted

@Repository
@Transactional
public class HibernateClinic implements Clinic {
     @Autowired
     private SessionFactory sessionFactory;
      .
      .
      .

}

为使该批注生效,需要在 Spring XML 配置文件中添加以下标记。

<context:annotation-config /> 

当 Spring 容器发现 @Autowired 批注时,就会将一个相应类型的 bean 注入所批注的属性。

Java EE 依赖注入

使用 JSF,可以通过 @ManagedProperty 批注将一个托管 bean 注入另一个托管 bean。例如,如果有两个名为 FooBar 的 JSF 托管 bean,且 Bar 有一个 Foo 类型的属性,则可以自动注入该依赖项,如下所示:

package com.ensode.foo;

//imports omitted

@ManagedBean
@RequestScoped
public class Bar {

    @ManagedProperty("#{foo}")
    private Foo foo;

    /** Creates a new instance of Bar */
    public Bar() {
    }

    public Foo getFoo() {
        return foo;
    }

    public void setFoo(Foo foo) {
        this.foo = foo;
    }

}

@ManagedPropertyvalue 属性的默认值必须是可解析到要注入的 bean 的统一表达式语言表达式。

如果 Java EE 应用程序使用上下文和依赖注入 (CDI),依赖注入甚至更简单。注入的属性需要使用 @Inject 批注进行批注,无需指定 bean 名称。

总结

在本系列文章中,我们开发了 Spring Pet Clinic 应用程序的一个 Java EE 版本。我们看到了 NetBeans 提供的高级工具如何使我们能够快速开发 Java EE 应用程序。之后,我们分析了生成的代码以确保我们理解这些代码。然后对应用程序进行了一些小修改,最终我们只编写很少的代码就得到了一个非常有用的应用程序。

构建好该应用程序的 Java EE 版本之后,我们将其与 Spring 版本进行了比较,注意到原始版本有一些依赖关系,而 Java EE 版本一个也没有,因为它利用了 Java EE 应用服务器提供的所有服务。

最后,我们比较了如何使用 Spring 和 Java EE 实现相同的功能,如 MVC 和 DAO 实现、事务管理和依赖注入。在使用 Spring 的每种情况中,除了向代码添加批注之外,还需要完成一些 XML 配置。Java EE 依赖于约定,在大多数情况下,无需 XML 配置即可实现这些服务。

尽管更新的 Spring 版本对显式 XML 配置的依赖已经比早期版本减少了许多,但是大多数 Spring 批注要想正常工作,总还需要在 XML 配置文件中一些地方添加几行,这违反了 DRY(不要重复你自己)原则。在使用 Java EE 的绝大多数情况中,无需配置,仅批注就足以完成任务。

此外,Spring 应用程序往往有多个依赖关系,因为它们的目标是在 Tomcat 或 Jetty 之类的轻型 Servlet 容器中运行,而这些容器并不能提供全部需要的功能。相反,Java EE 应用程序的目标是部署在功能全面的 Java EE 6 应用服务器中,如 Oracle GlassFish Server。Java EE 6 应用服务器提供了我们所需的大多数企业功能,这让我们不必管理多个外部依赖关系。

由于这些原因,我始终建议优先使用 Java EE 而不是 Spring 来进行企业应用程序开发。

另请参见

Spring 迁移到 Java EE,第 1 部分
Spring 迁移到 Java EE,第 2 部分
Spring 迁移到 Java EE,第 3 部分
Spring Framework
Java EE 6
Java EE 6 教程

关于作者

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

分享交流

请在 FacebookTwitterOracle Java 博客上加入 Java 社区对话!