本文介绍如何利用 Spring Framework 和 Apache OpenJPA 来改进 J2EE 项目的工作效率和应用程序开发体验。我们的示例应用程序在 Web 应用程序的不同体系结构层中的多个场合使用了 Spring,主要集中在业务层,特别是服务和数据访问层。
引言
Spring 是一个简化 J2EE 开发的 Java™ 框架。它具有用于 J2EE 应用程序的所有各层的功能。它还不强制要求特定的编程模型,因此与运行时环境无关,意味着可以在 Java SE 环境以外的其他应用程序服务器中使用它。Spring 在近年来的流行也许可以(至少是部分地)归功于这些设计原则。有些 Spring 支持者甚至将该框架视为 J2EE 的替代者。在我们看来,使用 J2EE 并不排除使用 Spring 的可能性,反之亦然;相反,这些技术组件相当完美地互为补充。
OpenJPA 是一个 Java Persistence API (JPA) 实现,其根源可追溯到 SolarMetric Kodo Java Data Objects (JDO) 实现。Kodo 被 BEA 收购,后者对 Kodo 进行了扩展以实现 Java Persistence API,并最终将该代码库发展为开放源代码的 Apache OpenJPA。通过 BEA 和 IBM 以及其他各方对该项目的不懈努力,当前的 OpenJPA 已成为一个用于 Java 的可行的对象-关系映射工具。
示例应用程序
本文使用一个名为 Events 的基本 Web 应用程序,以演示各种使用 Spring 和 OpenJPA 来开发运行于 WebSphere Application Server 上的应用程序的技术。我们设计了一个简单的应用程序,以重点演示如何结合使用这些技术。该示例提供了三个简单的用例:添加事件、列出事件和编辑事件。用户的信息输入保存在关系数据库中。用户可以查看系统中存储的事件的列表(请参见图 1)。对于每个事件,事件列表显示了内部标识符以及事件标题。
图 1. 列出事件
用户可以通过单击列表视图中的 Add 按钮添加事件。添加事件时,用户需要填写标题、类别、开始时间和持续时间字段。必填字段使用星号来标记(请参见图 2)。添加事件时,Id 字段为非活动的。
用户还可以使用列表视图中的对应 Edit 链接修改事件数据。
图 2. 添加事件
体系结构概述
Events 应用程序使用 Java SE 5 和 J2EE 1.4,并运行于 WebSphere Application Server 6.1 上。它还使用 JavaServer Faces (JSF) 1.1 技术来实现用户界面。我们在整个应用程序体系结构中利用了 Spring Framework,并在数据访问层使用了 Java Persistence API。
图 3 显示了该应用程序的稍微有点简化的结构视图。JSF 页面使用 JSF 托管 Bean 来执行诸如加载和存储事件信息等操作。为了改进可用性,存在两种类型的托管 Bean:执行操作的 Bean (EventBacking) 和包含状态的 Bean (EventSession)。操作(或支持)Bean 界定在请求范围内,而包含状态的 Bean 则界定在会话范围内。如果您的应用程序中碰巧具有大量的视图,则这种分离使得跨不同的视图重用两种类型的 Bean 变得更加容易。支持 Bean 获得当前用户会话状态的句柄,因为该状态通过 JSF 托管 Bean 功能注入到了支持 Bean 中。
图 3. 体系结构关系图
支持 Bean 将添加事件请求处理工作委托给无状态会话 Bean EventServiceEJB,并将列表和保存事件委托给一个 EventService 实现。通常,您将在 Web 或 EJB 应用程序层中访问数据库数据,但不会同时在这两个层中进行访问。该示例应用程序通过服务层从两个层访问数据库,以演示如何在这两个层中使用 Spring。
EventServiceEJB 进一步将处理委托给一个 EventService 接口实现类。该服务实现类然后使用一个数据访问对象(Data Access Object,DAO)实现类与持久数据存储通信。服务层具有用于查找特定事件、创建、更新和删除事件以及列出所有事件的方法。DAO 使用 Java Persistence API EntityManager 访问数据库数据(请参见图 4)。
图 4. 服务层关系图
该体系结构包括多个层,其中包括服务层,其主要用途是使该体系结构模仿实际的应用程序。EventService 服务层实现的一个具体功能在于,它充当事务管理的插件点,稍后我们将会说明这一点。
该应用程序的域模型(请参见图 5)包括单个名为 Event 的 Java 类,我们将其实现为 JPA 实体。我们将该实体映射到单个包括对应列的数据库表。
图 5. 域模型
该应用程序组织为四个项目:
- events-ear:EAR 打包、共享库等等。
- events-ejb:业务逻辑层、EJB 会话 Bean
- events-service:服务层和数据访问
- events-war:Web 层
开发
要使用 IDE 开发该示例应用程序,您需要安装以下必备软件:
- Eclipse 3.4 for Java EE
- IBM WebSphere Application Server v6.1(v6.1.0.9 或更新的 6.1 版。该示例应用程序使用 v6.1.0.17 进行了测试)
- Java SE JDK 1.5(可以使用与 WebSphere Application Server 打包在一起的 JDK)
- IBM DB2(DB2 UDB 8.2 或 Apache Derby v10.4.2.0)
该示例应用程序使用了 Spring Framework v2.5.5 和 Apache OpenJPA v1.2.0(有关所使用的其他 API 和版本的列表,请参见 events-ear/docs/libraries.txt 文件)。
按如下方式设置应用程序项目:
- 下载 events.zip 包并提取其内容
- 将源代码树导入 Eclipse。选择 File 菜单下面的 Import 并选择 General / Existing Projects into Workspace。选择所提取的源代码树根目录作为导入根。Import 对话框应该与图 6 所示类似:
图 6. 导入项目
- 创建名为 WAS 6.1 J2EE 的用户库。选择 Window - Preferences,然后导航到 Java / Build Path / User Libraries,并单击 New 创建新的库。请注意,务必使用名称 WAS 6.1 J2EE,以便自动将该库添加到 events-ejb 和 events-war 项目构建路径(请参见图 7)。创建该库以后,将 j2ee.jar 文件从 WAS 6.1 安装添加到该库。您将在 ${ app_server_root }/lib/j2ee.jar 找到该文件(其中 app_server_root 指的是您的 Application Server 6.1 安装根目录)。
图 7. 创建用户库
- 根据 events-ear/docs/libraries.txt 中的描述,下载所需的类库并将它们放在项目树中的正确位置。
- 编辑 events-ear 目录中的 build.properties 文件。您应该设置 was-profile.root 变量值以反映您的 Application Server 6.1 安装路径。
- 构建项目 EAR 文件。打开 events-ear 下面的 build.xml Ant 构建文件。右键单击 Eclipse Outline 视图中的“dist”目标,并选择 Run As / Ant Build。构建完成后,您将在源代码树根目录下的 dist 目录中找到 EAR 文件。
部署
构建应用程序 EAR 包以后,使用下面描述的过程在应用程序服务器中部署 events.ear。您将在项目结构的根目录下的 dist 目录中找到 EAR 包。
- 为应用程序创建数据库模式或选择现有的模式。
- 执行 events.ddl DDL 语句以创建数据库表(在 events-service/setup 中)。
- 打开 WAS 控制台并设置连接到前面创建的数据库模式的 XA 数据源。对数据源使用 JNDI 名称 jdbc/eventDS,如图 8 所示。
图 8. 为 Bean 提供 JNDI 名称
- 部署应用程序。在 Application Server 控制台中导航到 Applications / Enterprise Applications,并单击 Install。部署向导随即启动。在提示输入新应用程序的路径时,选择 events.ear 文件的路径。
- 将 ejb/EventServiceEJB 资源引用映射到 ejb/EventServiceEJB,如图 9 所示:
图 9. 将 EJB 引用映射到 Bean
- 下一步,将 jdbc/eventDS 资源引用映射到 jdbc/eventDS JNDI 名称(请参见图 10)。
图 10. 将资源引用映射到资源
- 最后,当部署向导完成时,单击 Manage Applications 并选择 events-ear / Manage Modules / events-war。将类加载器顺序设置为 Classes loaded with application class loader first,然后单击 OK 和Save。
图 11. 管理 events-war 模块
- 从 Enterprise Applications 列表中启动该应用程序。应用程序启动后,Application Status 列下面应该可以看到一个绿色的箭头符号。
- 通过将浏览器指向以下地址访问该应用程序:
http://localhost:9080/events-war/faces/jsp/eventsList.jspx该 URL 应该反映您的 Application Server URL
使用 Spring Framework 和 OpenJPA
到目前为止,我们已从用例、开发和部署的角度介绍了该示例应用程序。下面让我们将注意力转向引导并使用 Spring 和 OpenJPA。在下一个部分中,我们将了解如何使用某些旨在简化 Java 企业开发人员工作的 Spring 机制,包括对松散耦合、事务管理、异常处理、数据访问和分发的支持。
容器实例化
Spring Framework 的基本原则之一在于,它允许开发人员声明将由该框架在轻量级的容器中进行管理的服务对象(Spring 用语中的 Bean)以及它们之间的相互依赖关系。容器负责管理所声明的 Bean 及其依赖项的生命周期。在对容器进行实例化时,Spring 将连接所有声明的协作对象。由于该框架负责确保依赖对象能够访问它们的合作者,而不是让依赖对象必须查找其合作者,因此 Spring 也称为控制反转(Inversion of Control ,IoC)容器。可以使用不同的机制声明 Bean,其中一种机制就是 XML 配置文件。还可以使用编程方式或基于注释的 Bean 声明。
取决于应用程序层,实例化容器的方式稍微有所不同。在 Web 层中,只需通过将清单 1 中的 XML 片段放在 /WEB-INF/web.xml 文件中即可实例化该容器:
清单 1. Listener 类
<listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> |
缺省情况下,此类会加载 /WEB-INF/applicationContext.xml 文件,其中预期包括 Spring Bean 声明。可以根据需要自定义缺省配置文件路径。Web 应用程序的 ServletContext 用作容器实例的绑定目标,以使得容器无需多次进行实例化即可供后续使用。
由于 EJB 中不存在像 J2EE 1.4 中的 Web 层初始化机制那样用于初始化应用程序的标准方法,您需要在这里以稍微不同的方式实例化 Spring 容器。Spring 包括几个用于创建和加载容器的不同实现类。由于实例化容器的开销非常大,我们应该避免在每次需要实例时对容器进行实例化。既然 EJB 规范没有用于共享容器实例的适当机制,使用基于单一实例的实例化策略通常是适当的。
要使用此方法,您通常需要创建名为 beanRefContext.xml(缺省文件名)的特定于 EJB 的 Spring 引导配置文件,该配置文件又加载一组其他 Bean 配置文件。您还应该改写 EJB 实现类中的 setSessionContext。EJB 层中的容器实例化不像在 Web 层中那样无干扰性地工作。一个明显的缺点就是您需要使用 Spring API 来显式地查找 Bean。
一种类似的方法是使用 Spring 的抽象 EJB 实现支持类之一作为 EJB 实现的基类。这些方便的类使得代码编写人员不必实现 EJB 组件接口方法,而且还负责实例化 Spring 容器。但是,您仍然必须创建 beanRefContext.xml 并实现 setSessionContext。此方法的一个缺点是您无法使用自己的 EJB 基类。
有时,您最终会遇到 ServletContext 不可用的情形,甚至是在 Web 层中。如果您扩展第三方应用程序或框架,并且希望在代码中使用 Spring Bean,但是该 API 没有向扩展类传递上下文,可能就会发生这种情况。在此情况下,您可以按照上面针对 EJB 层描述的类似方式实例化 Spring 容器。
依赖项注入
使用 Spring,通过一种称为依赖项注入(Dependency Injection,DI)的技术,容器将负责使得对协作对象的引用对依赖对象可用。依赖项注入与用于提供到协作对象的访问的标准 J2EE 机制不同。在 J2EE 中,您使用 JNDI 环境命名上下文(Environment Naming Context,ENC)作为机制,以便通过命名空间使协作对象可用并获得对协作对象的引用。通过使用 JNDI ENC,依赖对象可以显式地请求对某个协作者的引用。
使用 Spring DI 时,程序员请求容器通过使得协作者引用对依赖对象可用,并通过使用构造函数或 setter 注入变体,从而解析依赖项。使用构造函数注入 时,容器在构造函数调用中传递协作者,而在使用 setter 注入时,容器在创建依赖对象后使用 mutator 方法调用传递引用。在这两种情况下,您都需要声明依赖项(例如使用 Spring 配置文件),并向依赖对象类添加对应的构造函数或 mutator 方法。
有关设计模式的经典图书 Design Patterns:Elements of Reusable Object-Oriented Software 提倡“按接口而不是按实现来编程”的设计原则。即使您按接口编程,仍然需要在某个地方实例化实现类。可以简单地以编程方式对其进行实例化,但是这样的话,您的代码将依赖具体的实现类。另一种方法是创建用于实例化实现的工厂类,但是您的代码中仍然存在对实现类的依赖。务必注意的是,即使依赖项可能非常有限并且数量很少,源代码级别的依赖项仍然存在。
清单 2 中的 EventService 接口实现类演示了此方法。这里的服务实现仅具有对 EventDAO 接口而不是对 DAO 实现类的源代码级别的依赖性:
清单 2. EventService 接口实现
public class EventServiceImpl implements EventService { private EventDAO eventDAO; public void setEventDAO(EventDAO eventDAO) { this.eventDAO = eventDAO; } // … } |
依赖项在 Spring 配置中声明如下:
<bean id="eventDAO" class="events.service.EventDAOJPA"/> <bean id="eventService" class="events.service.EventServiceImpl"> <property name="eventDAO" ref="eventDAO"/> </bean> |
Spring 通过允许您在配置文件中声明依赖项,然后将协作者连接到依赖对象,从而提供松散耦合。这使得将调用者与实现类完全分离成为可能,从而使您的代码更加灵活。切换实现类现在成了一件非常简单的事情,只需修改 Spring 配置文件即可。
异常处理
近年来,出现了有关 Java API 如何使用 Java 异常模型的批评。许多人争论说,您作为程序员,不应该被迫处理预期在本质上很罕见的错误条件,以及由于系统或程序员错误而导致的无法合理恢复的错误。相反,您应该对此类条件使用未经检查的异常,以便能够可选地处理这些条件。这个学术流派认为,只有预期在正常操作期间发生的应用程序或用户错误才应该使用检查的异常来进行报告。随着许多框架和 API(包括 Spring)赞成这种思维方式,这种异常处理策略已变得日益流行。
服务层支持
拥有良好设计和实现的服务层可以对应用程序的可扩展性和可靠性产生积极的影响。可以证明,对于高度可重用的服务层来说,添加新的最终用户功能和修改现有的功能要简单得多。
如果实现方式不当,事务划分 会对服务层可重用性产生负面影响。这是一个挑战,因为您可能使用服务层来实现差别非常大的用例,从而导致服务层调用者在不一样的上下文中操作。调用者将会具有不同的事务需求,因此服务层应该允许调用者影响事务处理。
当应用程序的事务需求并不非常复杂时,编程方式的事务划分可能非常繁琐和容易出错。声明式事务划分 允许您为软件的事务行为声明规则,并让事务管理器自动执行这些规则。Spring 同时支持编程方式和声明式的事务划分。该示例应用程序使用清单 3 中的声明来为服务层定义事务属性:
清单 3. 事务属性
<tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="serviceMethods" expression="execution(* events.service.EventService.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/> </aop:config> |
此配置使用了 Spring 对面向方面的编程(Aspect Oriented Programming,AOP)的支持。在清单 3 中,我们定义了称为事务顾问对象 (transaction advisor object) 的内容,并将事务管理器绑定到该顾问。我们告诉事务顾问将 REQUIRED 事务属性应用于所有声明的方法。REQUIRED 事务属性的语义与 J2EE 中相同,这意味着该方法将始终在事务中执行。如果调用者在某个事务上下文中运行,则该方法将在调用者的上下文中执行。否则,它将创建新的事务上下文。
然后 aop:config 部分定义了一组方法,即我们对其应用事务声明的 events.service.EventService 接口中的所有方法。以事务方式建议的类不必实现特殊的组件接口;可以为传统 Java 对象(Plain Old Java Object,POJO)类指定事务属性。为了实现这一点,Spring 使用事务代理来包装原始服务对象。这里需要注意的一点在于,服务对象本地调用并不经过事务代理,因此将始终在调用者的事务上下文中进行。
Spring 在运行时使用一个事务管理器接口实现来执行实际的事务划分。可以对服务层进行配置,以根据目标环境的功能使用不同的事务管理器。例如,当您的服务层在 J2EE 应用程序服务器中运行时,可以告诉 Spring 使用应用程序服务器的事务管理器,如清单 4 所示:
清单 4. 事务管理器配置
<tx:jta-transaction-manager/> |
此配置将使 Spring 自动选取您的应用程序服务器的事务管理器。在 Java SE 环境中,您可以配置 Spring 使用 JPA API 的事务划分功能,如清单 5 所示:
清单 5. JPA 事务管理器声明
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="emFactory"/> </bean> |
此技术使得无需任何代码修改即可实现在 EJB 和 Web 层或 Java SE 环境中使用的服务层。在容器之外测试服务层也变得非常容易,从而极大地加速了开发周期。
可能会对服务层可重用性产生负面影响的另一个问题是您的异常处理策略。应该在整个应用程序中一致地处理异常。通常,您不应该被迫处理系统错误,但是仍然应该能够在需要时处理这些错误。通常,诸如 JDBC 和 JPA 等数据访问 API 仅提供有关异常条件的非常一般的信息,并且不支持有效地确定特定的问题根源。当服务层使用数据访问层访问存储在某个企业信息系统中的数据时,服务层应该不允许将任何特定于信息系统的异常传播到更高的层。
Spring 定义了一致的数据访问异常类层次结构(请参见图 12),您可以将其用作服务和 DAO 层异常的基础。Spring 的数据访问功能自动将数据访问异常转换为此异常类层次结构。如果需要,还可以使用自己的更加专用的异常来扩展此层次结构。正如前面提到过的,此层次结构中的异常未经检查。(查看此图的大图。
图 12. Spring 数据访问异常层次结构(取自 Spring 参考文档的图)。
DAO 支持
JPA 的一个重要概念是持久上下文。您与允许您对关系数据库执行数据访问操作的持久上下文交互。持久上下文负责将托管对象状态与数据库中存储的实体状态进行同步。可以通过 EntityManager 接口访问持久上下文。
当应用程序运行时环境不支持容器托管的持久上下文时,应用程序需要显式地管理该上下文的生命周期。这可能有点繁琐,幸运的是,Spring 提供了这方面的帮助。清单 6 中的代码行为 Spring 配置了一组 Bean 后处理器,这些后处理器增强了幕后的托管 Bean:
清单 6. 配置 Spring 容器
<context:annotation-config/> |
除了其他功能以外,这些处理器还处理 Spring Bean 中的注释,从而使得将 JPA 持久上下文注入 Java 类成为可能,例如类似于清单 7 所示的 DAO 实现类:
清单 7. JPA 持久上下文
@PersistenceContext private EntityManager em; |
此注释使得 Spring 将一个事务持久上下文注入到类实例中。由于 Spring 模拟了具有 POJO 的容器托管持久上下文时的样子,DAO 实现层就变得整洁多了。请注意,在 Java EE 5 中,您只能将持久上下文注入诸如 EJB 等托管对象,而不能注入 POJO。
还可以告诉 Spring 转换数据访问实现类的数据访问异常,只需将清单 8 中的行添加到 Spring 的配置文件即可:
清单 8. 转换数据访问异常
<bean class="org.springframework.dao.annotation. PersistenceExceptionTranslationPostProcessor"/> |
此外,必须使用 @Repository 对 DAO 实现类进行注释。
JavaServer Faces 支持
JSF 具有用于自定义 JSF EL 表达式中的顶级变量的解析的机制。Spring 附带了一个变量解析器实现类,允许您在 JSF 表达式中引用 Spring 托管 Bean。对于每个顶级变量名称,该类首先检查 Spring 中是否存在具有该 ID 的 Bean。如果存在相应的 Bean,该类将把引用解析到此 Bean。否则,它将咨询 JSF 缺省变量解析器。使用此解析器使您可以将 Spring Bean 注入 JSF 托管 Bean,或者在 JSF 页面的 EL 表达式中引用 Spring Bean。清单 9 显示了如何在 faces-config.xml 文件中配置变量解析器:
清单 9. 配置变量解析器
<variable-resolver> org.springframework.web.jsf.DelegatingVariableResolver </variable-resolver> |
访问 EJB Bean
使用 EJB 会话 Bean 会在调用者端产生相当冗长的代码。要查找和调用实现为远程无状态会话 Bean 的服务方法,典型的代码片段与清单 10 所示类似:
清单 10. 用于 EJB 会话 Bean 的冗长代码
try { Context ctx = new InitialContext(); Object homeObj = ctx.lookup("java:comp/env/ejb/EventServiceEJB"); EventServiceEjbHome eventHome = (EventServiceEjbHome) PortableRemoteObject.narrow(homeObj, EventServiceEjbHome.class); EventServiceEjb eventService = eventHome.create(); String msg = eventService.getGreeting("world"); } catch (NamingException e) { // handle exception } catch (CreateException e) { // handle exception } catch (RemoteException e) { // handle exception } |
查找代码和异常处理是导致清单 10 变得冗长的主要原因。克服这些问题的典型解决方案是实现 ServiceLocator 模式,其中将查找代码转移到单独的类中,服务用户调用该类以获取对服务实现类的引用。ServiceLocator 还可以将检查的异常(在 Bean 查找或创建过程中引发的异常)转换为未经检查的异常。您仍然需要在使用 EJB 时处理 RemoteException 异常。
同样,Spring 提供了针对此问题的极好解决方案。您可以在 Spring 配置中将 EJB Bean 指定为 Spring Bean,然后使用 Spring 的正常依赖项注入方法将它们作为协作者注入任何其他 Spring Bean。
清单 11. 将远程无状态会话 Bean 声明为 Spring Bean
<jee:remote-slsb id="eventServiceEjb" jndi-name="java:comp/env/ejb/EventServiceEJB" business-interface="events.service.EventService" home-interface="events.ejb.EventServiceEjbHome"/> |
如果目标字段类型指定了 EJB 业务接口类型,则调用者不需要关心调用某个 EJB 的任何细节。Spring 捕获诸如 NamingException 和 RemoteException 等异常,并将它们作为未经检查的异常重新引发,这样您就可以自由地显式处理这些异常。通过注入对代理对象的引用而不是注入实际的 EJB 远程接口存根,Spring 还可以捕获在业务方法调用过程中引发的异常。这样,代理就可以截获对 EJB 的调用,并根据情况转换异常。远程方法调用仍然使用按值调用 (call-by-value) 语义,您当然需要知道该语义。
结束语
Spring 可以简化许多传统 J2EE 编程挑战,以提高您的工作效率。由于 Spring 的无干扰性设计,很容易将其引入现有的代码库或新应用程序。您还可以挑选要部署的功能;如果只需要其中一小部分,您不必使用整个堆栈。Spring 还可以与 WebSphere Application Server 和 OpenJPA 很好地集成。本文仅介绍了如何使用 Spring 的一小部分功能,建议读者从下面的参考资料部分开始,继续探索其他可能对您的项目有益的功能。(责任编辑:A6)