开始之前
近十年来,构建健壮、可维护的服务器端 Java 应用程序的 “正确” 方式一直是 Java 2 企业版(J2EE)平台的专属领域。J2EE 应用程序用企业 JavaBean(EJB)技术构建,运行在方便部署并提供丰富的容器服务(例如数据库连接和池的管理)的服务器之上。这些服务器还通过对重要特性(例如安全性和事务)提供部署时声明式控制带来了附加价值。虽然功能丰富,J2EE 开发过程包含了许多烦琐和重复的任务,还要创建和维护大量的源代码文件。
许多轻量级 Java 框架声称简化了服务器应用程序开发,但论及成熟和流行的程度,Spring 框架无可匹敌(请参阅 参考资料)。Spring 目前的版本是 Spring 2,从第 1 天起,其设计目的就在于简化服务器应用程序的构建过程。Spring 的开发没有采用一体化的容器角度,而是为应用程序的需求提供恰到好处的支持,因此不会因包罗万象的容器环境造成负担。Spring 消除了代码膨胀:完全可以在容器之外编写和测试业务对象,从而让业务对象代码保持简单、可测试、可维护和可重用。
随着 Java EE 5 和 EJB 3.0 的出现,J2EE 社区准备好了迎接 Spring 开发人员社区。EJB 3.0 支持轻量级 POJO(Plain Old Java Objects,老式普通 Java 对象)作为 EJB 组件的概念,并引入了 Java 持久性 API (JPA),JPA 是可以在容器外部运行的持久性机制。这种持久性机制自动实现业务对象和外部关系数据库之间的信息移动。Spring 框架的版本 2 继续了自己的发展,也利用 JPA 作为持久性机制。
在这份教程中,您将使用 Spring 2 和 JPA 持久性。将用 Spring 2 框架创建一个服务器应用程序,完成时可以访问 DB2 Express-C 数据库。Eclipse IDE 可以方便 Java 应用程序的开发并促进对 Spring 2 框架的研究。
关于本教程
本教程采用按编码学习这种纯粹、简单的方式,目的是在尽可能短的时间内,指导您了解 Spring 2 框架的使用和应用程序。您将在 Spring 2 框架的协助下,从头开始逐步构建一个 Web 应用程序。
这份教程并不想试图覆盖 Spring 2 的全部特性和选项。而是重点关注了使用 Spring 开发服务器应用程序的一种切实有效的方式。鼓励您参考其他 Spring 2 资源获得与这个框架有关的更高级的应用程序和技术(请参阅 参考资料)。
您将经历一个完整的 “从概念到应用程序” 的周期,包括:
- 执行域分析
- 为业务对象和服务编写代码
- 对业务对象进行单元测试
- 使用 Sprint JPA 在不带来过高成本的情况下为业务对象添加数据访问代码
- 用 Spring DAO(数据访问对象)实现服务
- 针对 DB2® Express-C 为服务编写集成测试代码
- 为基于 Spring 模型-视图-控制器(MVC)的用户界面创建控制器
- 为用户界面设计视图
- 创建应用程序的可部署 WAR 文件
- 在 Apache Tomcat 服务器上配置和部署应用程序
学完本教程后,您应能够理解 Spring 2 框架的工作原理以及它能为创建高度组件化、可维护的 Web 应用程序带来怎样的帮助。在构建这样一个应用程序的过程中,您将获得实践经验,还能把这里学到许多技术应用到日常的开发任务中。
先决条件
您应当熟悉基本的面向对象设计概念和使用 Java SE 5 的 Java 开发,包括泛型。您还应当熟悉关系数据库概念,对于如何在 DB2 Express-C 中设置新数据库也应当有基本的了解。
您还应熟悉测试术语,包括单元测试和集成测试。最好有测试框架(例如 JUnit)方面的实际经验,但并非必需。
您应当有 Eclipse 方面的实际经验,能够在 Eclipse 内创建新 Java 项目、编译 Java 代码和调试项目。
系统需求
要试验这份教程中的工具和示例,硬件配置需求为:至少带有 512MB 内存(推荐 1GB)的系统。
需要安装以下软件:
Spring 2 框架操作概述
这一节介绍 Spring 2 框架,简要说明了它在常规服务器应用程序开发中的优势。
用 Spring 构建应用程序
从传统 API 的意义上来说,Spring 并不是编程框架。多数情况下,框架由 API 和可以在应用程序中使用的代码主干的集合组成。
Spring 2 被设计成非侵入的。实际上,它允许您编写对象和业务逻辑,就像 Spring 不存在一样。在编写和测试这些对象之后,可以添加 Spring 2 支持特性。在某些情况下,添加这些特性时甚至不需要重新编译源代码。
例如,可以先创建和测试一个 Java employee 对象,然后添加 Spring 2 支持,把对象的实例保存到关系数据库中。也可以先编写更新银行账户的代码,然后应用 Spring 2 的事务功能来确保数据的完整性。
图 1 显示了典型的基于 Web 的服务器应用程序。用户通过用户界面与应用程序交互。应用程序逻辑作为一组业务对象上的操作来执行,构成应用程序的域模型。业务对象通常由保存在关系数据库中的数据支持。
图 1. 典型的基于 Web 的应用程序的架构
如果要从头开发这个应用程序,需要构建图 1 中的每个组件,并编写定制代码来实现对数据库的访问。
在使用 Spring 构建应用程序时,可首先关注完善域模型。可以用简单的 Java POJO 为系统中的对象建模,并把系统中的服务定义成标准的 Java 接口。这样的设计使您可以独立于 Spring 或者其他框架/库来创建和测试域模型。
然后即可把额外的 Spring 特性应用到应用程序,以测试过的域模型为基础。例如,可以用 Spring 的 JPA 支持添加对象持久性 —— 把数据保存到关系数据库和从关系数据库检索数据的能力。
装入时增强
Spring 框架通过在类装入的时候提供附加价值来保证非侵入性。在正常运行 Java 应用程序时,JVM 只在需要类时才通过一组类装入器装入类。这是非常透明的,通常在不知觉的情况下发生。对于一定复杂的软件,如 Eclipse IDE,可以这样装入数千个类。使用 Spring 框架,通过告诉 Spring 引擎(也叫作 Spring 容器)您的类如何搭配在一起、要给这些类添加什么特性,对框架进行编程。Spring 引擎根据您的调配来构建类。图 2 以图表的方式显示了这个过程:
图 2. Spring 框架操作
图 2 显示出:因为 Spring 引擎拥有对类的低级访问,所以可以根据特定的配置,用额外特性增强它们。图 2 还显示了一种可以向 Spring 引擎提供指令的机制。典型情况下,这些指令以 XML bean 描述符文件的形式存在,在某些情况下,也可能以 Java 5 注释的形式存在于源代码内。
例如,可以创建代表员工的简单 Java 对象,然后让 Spring 引擎增强这些类,使得这些对象可以动态地保存到关系数据库表中,或从关系数据库表检索这些对象。本教程将介绍如何实现这些目的。
经典 API 支持
除了类装入时增强,Spring 2 还提供了经典 API 支持,以封装复杂和烦琐的操作。图 3 显示了这份教程的示例应用程序使用的 Spring 支持库。请对比图 3 和 图 1:
图 3. Spring 2 增加的价值
可以把 Spring 2 的 JPA 支持与 Spring DAO API 结合使用,简化对关系数据库的访问。将用 Spring MVC 轻松地为应用程序添加基于 Web 的用户界面。
准备 Spring
在这一节,要开始构建一个利用 Spring 2 框架的示例员工信息应用程序。要确定和编码应用程序的业务对象、生成 setter 和 getter、编写服务接口、并对类进行单元测试。应用程序开发周期的这个阶段的执行独立于 Spring 框架。
在域模型分析中确定 POJO
这份教程的应用程序设计方式的第一步就是 域模型分析 —— 通常叫作 “确定 POJO”。在这种情况下,POJO 代表 Plain Old Java Object。它们也代表应用程序中的 业务对象,这份教程交替使用这两个术语。
确定应用程序域的业务对象,与初学面向对象设计时所做的练习相同。目的是确定要创建的系统中的对象和它们之间的交互关系。更具体地讲,就要是发现:
- 维护状态的对象(和需要维护的状态)
- 这些对象之间的关系
- 这些对象之间的交互作用(如果有的话)
- 在应用程序中需要在这些对象上执行的操作
以在这份教程中要创建的简单服务器应用程序为例。这是个显示员工信息的系统。顾名思义,这个系统中有一个对象是公司的员工。员工信息可以很丰富,但在这个简单的应用程序中,只需要知道以下信息:
- 员工编号
- 姓名
- 中间名缩写
- 姓氏
- 分机号码
- 职务
- 教育程度
- 性别
- 工资
- 奖金
- 提成
- 地址
- 录用日期
- 出生日期
在这个域中能够确定的另一个业务对象就是代表地址的对象。地址之所以从员工信息中分离出来,是因为系统中的其他实体(如果想扩展系统的话)可能也有地址,这样就可以把所有地址放在一起了。为简单起见,假设地址对象只包含街道号码和街道名称。
在这个简单的系统中,每个员工都有一个地址,每个地址也只属于一个员工。这叫作 1 对 1 关系。其他可能的关系包括 1 对多(例如,项目与员工的关系)、多对 1 (例如员工与部门的关系)和 多对多(例如员工与 HR 福利的关系)。其他这些关系模型在 Spring 2 应用程序中都可以建立,但是超出了这份初级教程的范围。
在这个系统中,在员工和地址对象上要有以下操作:
- 创建新员工(和新地址对象)
- 删除员工(和相关的地址对象)
- 更新员工信息
- 查找公司的全部员工
- 根据员工编号查找公司的特定员工
- 根据姓氏查找公司员工
- 根据街道查找公司员工
- 查找工资超过指定数额的员工
- 查找提成超过指定数额的员工
典型情况下,可以通过考虑系统需要的用户界面和系统中需要实现的业务逻辑来确定这些操作。
这个示例中的操作特意被保持简单,以便使教程重点突出。在典型的生产场景中,很可能会有跨多个确定的对象集执行的更复杂的操作,还可能会存在对象之间的直接交互。
确定了业务对象、交互和操作之后,就可以编码和测试了。
为业务对象编写代码
编写的第一组对象是构成系统的 POJO。可以用自己喜欢的开发编辑器或 IDE 创建这些对象。本教程假设您采用的是 Eclipse。
请在 Eclipse 中创建名为 Spring2Tutorial 的新 Java 项目,然后创建名为 com.ibm.dw.spring2.Employee 的类,如清单 1 所示:
package com.ibm.dw.spring2;
public class Employee {
private long empid;
private String empno;
private String firstName;
private String midInitial;
private String lastName;
private String phoneNumber;
private String job;
private int educationLevel;
private char sex;
private double salary;
private double bonus;
private double commission;
private Address addr;
private Date hiredate;
private Date birthdate;
}
|
清单 1 定义了在 Employee 对象实例中保存的全部信息。这些字段全被定义成 private,所以需要创建一些 getter 和 setter 方法,以便支持对这些信息的外部访问。
用 Eclipse 生成 getter、setter 和构造函数
在 Eclipse 中生成 getter 和 setter 方法。请在编辑器中右击,并选择 Source->Generate Getter and Setters...。在图 4 所示的弹出对话框中,单击 Select All 然后再单击 OK:
最后,必须为对象创建一些构造函数。在 Eclipse 中,只需右击并选择 Source-> Generate Constructor。然后需要编辑生成的构造函数。请看清单 2:
public Employee(String empno, String firstName, String midInitial, String lastName,
String phoneNumber, String job, int educationLevel, char sex, double salary,
double bonus, double commission, Address addr, Date hiredate, Date birthdate) {
this.empno = empno;
this.firstName = firstName;
this.midInitial = midInitial;
this.lastName = lastName;
this.phoneNumber = phoneNumber;
this.job = job;
this.educationLevel = educationLevel;
this.sex = sex;
this.salary = salary;
this.bonus = bonus;
this.commission = commission;
this.addr = addr;
this.hiredate = hiredate;
this.birthdate = birthdate;
}
public Employee() {}
|
需要添加清单 2 所示的这个小小的构造函数,因为日后在单元测试阶段要使用它。这就完成了 Employee 业务对象的编码工作。
遗漏的 empid 字段
您可能注意到清单 2 的构造函数中遗漏了 empid 。后面要用这个字段来包含 JPA 生成的与 Employee 实例关联的主键。这个键由 JPA 管理,不应当被应用程序修改。现在应当从源代码删除 setter 方法 setEmpid() 。
编写 Address POJO 的代码
每个 Employee 对象都引用一个 Address 对象。只要 Employee 对象 保存到数据库,就需要保存 Address 对象。
Address 对象的代码如清单 3 所示,其中也包含 getter、setter 和构造函数:
package com.ibm.dw.spring2;
public class Address {
private long id;
private int number;
private String street;
public long getId() {
return id;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public Address( int number, String street) {
this.number = number;
this.street = street;
}
public Address() {}
}
|
现在就完成了域模型中 POJO 的编码,接下来需要从服务接口的角度实现程序程序的 POJO 上的操作。
创建服务接口
根据前面执行的域分析,应用程序要求一组在业务对象上的操作。
把需求集合转换成代码 —— 更准确地说,是转换成 Java 接口,结果将类似于清单 4。Employee 对象上的每个操作都成为 EmployeeService 接口中的一个方法。
package com.ibm.dw.spring2;
import java.util.List;
public interface EmployeeService {
// create a new employee
public Employee save(Employee emp);
// removing an employee
public void delete(Employee emp);
// update the information on an employee
public Employee update(Employee emp);
// find all the employees in the company
public List<Employee> findAll();
// find an employee by the employee number
public List<Employee> findByEmployeeNumber(String empno);
// find an employee by his name
public List<Employee> findByEmployeeLastName(String lastName);
// find an employees living on a street
public List<Employee> findByAddressStreetName(String streetName);
// find an employee by the internal unique id
public Employee findById(long id);
// find employee over a certain salary
public List<Employee> findEmployeeWithSalaryOver(double sal);
// find employee with a certain commission income
public List<Employee> findEmployeeWithCommissionOver(double comm);
}
|
请注意在清单 4 中,代码是对域分析得到操作列表的直接转化。这个接口不包含任何特定于 Spring 的编码。在这个接口中,目前只知道需要在对象上进行 的操作,对于如何做还一无所知。很快您就会看到如何实现这个接口中的方法,但是首先要进行 POJO 单元测试。
在容器外测试业务对象
在编码完域模型中的独立 POJO 和服务之后,即可为 POJO 编写单元测试。至此为止,仍然不需要执行任何特定于 Spring 的步骤。实际上,您可能会不加修改地使用您在其他应用程序(或同一应用程序的其他部分)中得到、经过测试的 POJO。
清单 5 显示了 POJOUnitTest.java。这个 JUnit 测试用例测试 Employee 和 Address POJO。
package com.ibm.dw.spring2;
import java.text.SimpleDateFormat;
import java.util.Date;
import junit.framework.TestCase;
public class POJOUnitTest extends TestCase {
private Employee emp1, emp2;
private Address addr1, addr2;
protected void setUp() throws Exception {
addr1 = new Address(10, "Walker Street");
addr2 = new Address();
addr2.setNumber(20);
addr2.setStreet("Walker Street");
emp1 = new Employee("0001", "Joe", "R","Smith",
"4853", "Engineer", 3, ‘M‘,
20000.00, 0.00, 0.00,
addr1
,makeDate("08/08/2006") , makeDate("02/04/1972"));
emp2 = new Employee();
emp2.setEmpno("0002");
emp2.setFirstName("John");
emp2.setMidInitial("T");
emp2.setLastName("Lockheed");
emp2.setPhoneNumber("4333");
emp2.setAddr(addr2);
emp2.setJob("Sales");
emp2.setHiredate(makeDate("01/01/2005"));
emp2.setBirthdate(makeDate("10/8/1966"));
}
public void testEmployee() throws java.text.ParseException {
assertEquals("0001", emp1.getEmpno());
assertEquals("Joe", emp1.getFirstName());
assertEquals("R", emp1.getMidInitial());
assertEquals("Smith", emp1.getLastName());
assertEquals(10, emp1.getAddr()。getNumber());
assertEquals("Walker Street", emp1.getAddr()。getStreet());
assertEquals (makeDate("08/08/2006"),emp1.getHiredate());
assertEquals("0002", emp2.getEmpno());
assertEquals("John", emp2.getFirstName());
assertEquals("T", emp2.getMidInitial());
assertEquals("Lockheed", emp2.getLastName());
assertEquals(20, emp2.getAddr()。getNumber());
assertEquals("Walker Street", emp2.getAddr()。getStreet());
assertEquals (makeDate("01/01/2005"),emp2.getHiredate());
}
public void testAddress() {
assertEquals(10, addr1.getNumber());
assertEquals("Walker Street", addr1.getStreet());
assertEquals(20, addr2.getNumber());
assertEquals("Walker Street", addr2.getStreet());
}
private Date makeDate(String dateString) throws java.text.ParseException {
return (new SimpleDateFormat("MM/dd/yyyy"))。parse(dateString);
}
}
|
|
侧重测试的设计文化
在研读已有的 Spring 文献时,您会发现大量适合基于 Spring 的系统开发的方法论和哲学。这些方法论之间的一个公共元素,就是以测试为中心的文化。因为 Spring 允许在容器之外迅速地测试域模型 POJO,所以频繁的单元测试(和集成测试)可以成为设计过程的基石。
|
|
清单 5 中的测试代码非常易于理解。这个测试根据先构造函数、后逐个字段的方式设置 Employee 和 Address 的字段,最后对值进行检验。这些测试并未面面俱到;没有测试每个字段,但是清单 5 确实表现了如何在麻烦的容器之外对模型进行单元测试。
要在 Eclipse 内运行单元测试,请在导航器视图中右击 POJOUnitTest.java 文件,然后选择 Run as... -> JUnit Test。
Spring 中以 POJO 为中心的开发允许在容器之外独立测试业务对象。与其他方式不同,采用以 POJO 为中心的设计,可以非常迅速地执行单元测试,所以,可以更频繁地执行此类测试(例如,作为构建过程的一部分)。
通过 Spring 2 的 JPA 支持获得数据访问
在这一节,我们将利用 Spring 2 对 Java 持久性 API(JPA)的支持把数据库访问添加到 Employee 和 Address。
通过 JPA ORM 的对象持久性
为提供前面定义的服务接口的实现,需要利用 Spring 2 的部分特性和支持库。当然,存在替代方法。可以用标准的 Java 数据库访问技术,例如 JDBC,一个方法一个方法地开始实现接口。但是,看到 Spring 2 如何用 JPA 实现这一任务之后,您就会认识到,把这项工作委托给 Spring 实际上要更容易。
在 Spring 2 中,集成了来自 EJB 3.0 和 Java EE 5 规范的 JPA 持久性栈(请参阅 参考资料),使之成为 Spring 支持数据库访问的最简单而且也是标准的方式。
Spring 框架一直通过其他对象到关系的映射(ORM)技术支持持久性,但是这类映射任务要求对第三方非标准的技术性库有相当精妙和深入的了解。随着 JPA 的到来,大批供应商开始支持 JPA 标准,因而对非标准的第三方持久性库的支持的重要性有所降低。
Spring 2 支持 JPA,这使得为关系数据库编写、读取、搜索、更新和删除对象(POJO)的烦琐工作变得透明。可以继续使用 Java 语言面向对象的语法处理 POJO,JPA ORM 层负责数据库表的创建、查询、更新代码和删除代码。
除了透明的数据库操作,Spring 2 的 JPA 支持还把各种五花八门的特定于数据库厂商的异常转换成一套定义良好的异常,使得异常处理代码大为简化。图 5 演示了 Spring 2 的 JPA 支持:
|
与 Java SE 5 注释一道,JPA 还支持通过外部 XML 文件的映射线索规范。如果您使用的不是 Java SE 5,那么这项技术是必要的,但相关内容已超出了本教程的讨论范围。
|
|
在图 5 中,您将自己的对象反馈给 Spring 引擎,同时还有一些关于您希望如何把它们映射到关系数据库表的线索(元数据)。Spring JPA 支持负责处理剩下的工作。可以用 Java 5 注释的形式或外部 XML 定义文件(为了与 JDK 1.4 兼容)的形式向 JPA 引擎提供映射线索。
因为各种 ORM 产品和数据库都存在 JPA 实现,所以您的实现代码是可以在不同厂商的解决方案之间移植的(如果必要)。
对映射对象的操作通过 JPA 实体管理器来执行。例如,用叫作 em 的实体管理器把一个相关对象树写入关系数据库,代码应当是:
em.persists(myObjectTree);
|
JPA 实体管理器然后检查您所提供的映射线索,并通过 myObjectTree 把对象树的所有映射字段保存到关系数据库。
您很快就会看到(在 用 Spring DAO 实现域服务 一节中),Spring 走得更远,它还简化了使用 JPA 实体管理器的任务。
提供 JPA ORM 映射元数据
要提供如何把 Employee 对象保存到数据库的提示,可以向 Employee.java 源代码添加 Java SE 5 注释。这些线索通常叫作元数据,因为它们是描述数据的数据。
清单 6 显示了注释版本的 Employee 对象,其中的注释以粗体突出显示:
package com.ibm.dw.spring2;
import java.util.Date;
import javax.persistence.CascadeType;
...
import javax.persistence.TemporalType;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private long empid;
@Column(length = 6)
private String empno;
@Column(name = "FIRSTNME")
private String firstName;
@Column(name = "MIDINIT")
private String midInitial;
private String lastName;
@Column(name = "PHONENO")
private String phoneNumber;
@Column(length = 8)
private String job;
@Column(name = "EDLEVEL")
private int educationLevel;
@Column(length = 1)
private char sex;
@Column(precision=12, scale=2)
private double salary;
@Column(precision=12, scale=2)
private double bonus;
@Column(name = "COMM", precision=12, scale=2)
private double commission;
@OneToOne(cascade = CascadeType.ALL)
private Address addr;
@Temporal(TemporalType.DATE)
private Date hiredate;
@Temporal(TemporalType.DATE)
private Date birthdate;
...
|
这个示例中的所有注释都在字段级。这是 JPA 注释的最常见用法。也可以给与字段对应的 getter 方法加注释。如果想要保存到数据库中的值是计算得来的,而不是对象的字段时,这可能是必要的。
Employee POJO 的 JPA 注释
表 1 描述了 清单 6 中每个字段的注释,以及提供给 Spring 2 引擎的持久性线索:
字段/元素 |
注释 |
说明 |
Employee |
@Entity |
指出这是要保存到数据库的类。默认情况下类名称被用作表名。 |
empid |
@Id |
指出这是表的主关键字段。 |
empid |
@GeneratedValue(strategy = GenerationType.TABLE) |
指定持久性引擎用来分配唯一主关键 ID 的策略。GenerationType.TABLE 指出应当使用可移植的基于表的 ID 序列。其他特定于数据库的选项也可使用,但可能无法跨多个数据库工作。 |
empno |
@Column(length = 6) |
这个字段包含员工编号,由公司分配。请注意这不是主键。在这个应用程序中,主键是由引擎生成和管理的。@Column() 标记指定字段应当是六个字符长。指定合适的字段长度会有助于限制生成的表的大小。 |
firstName |
@Column(name = "FIRSTNME") |
指定这个字段在数据库表中应当使用的字段名。 |
midInitial |
@Column(name = "MIDINIT") |
指定这个字段在数据库表中应当使用的字段名。请注意,它与 Java 字段名不同。 |
lastName |
|
没有注释,所以将使用字段名 "LASTNAME" ,与 Java 字段名匹配。 |
phoneNumber |
@Column(name = "PHONENO") |
指定在数据库表中使用的字段名。 |
job |
@Column(length = 8) |
指定数据库字段的长度。 |
educationLevel |
@Column(name = "EDLEVEL") |
指定数据库字段名。 |
sex |
@Column(length = 1) |
指定数据库字段的长度。 |
salary |
@Column(precision=12, scale=2) |
指定浮点数据库字段的算术精度。 |
bonus |
@Column(precision=12, scale=2) |
指定浮点数据库字段的算术精度。 |
commission |
@Column(precision=12, scale=2) |
指定浮点数据库字段的算术精度。 |
addr |
@OneToOne(cascade = CascadeType.ALL) |
指定这个表和另一个映射表中的 Address 对象之间的关系。cascade=ALL 指明添加、修改、删除和刷新应当全部级联到关联表。 |
hiredate |
@Temporal(TemporalType.DATE) |
指定字段是日期字段(而不是时间或时间戳字段)。 |
birthdate |
@Temporal(TemporalType.DATE) |
指定字段是日期字段(而不是时间或时间戳字段). |
请注意 Employee 和 Address 实例是一对一的关联(由 @OneToOne(casecade=CascadeType.ALL) 注释指定)。这个注释指定 Employee 对象上的所有实体管理器操作应级联到关联的 Address 对象。这意味着新增任何 Employee 记录都会在 RDBMS 中创建一个对应的 Address 记录。它还意味着对 Employee 记录所做的任何更新或删除,都会级联到关联的 Address 记录。这是经常在 RDBMS 中发现的级联删除完整性约束的扩展。在实践中,会发现在执行级联选项时,Java 代码被极大地简化了:不再需要协调跨越多个表的操作。
有注释的 Employee 源代码是 JPA 实体管理器管理 Employee 或 Address 对象的持久性实例的蓝本。您应发现,与容易出错的编写创建和操作实际 RDBMS 表的实际代码相比,这样的注释要简单得多。。
Employee 对象的表可以选择性地基于上述注释生成,其模式类似于清单 7:
CREATE TABLE EMPLOYEE (
EMPID INTEGER NOT NULL,
EDLEVEL INTEGER,
SEX CHAR(1),
FIRSTNME VARCHAR(255),
SALARY DOUBLE,
LASTNAME VARCHAR(255),
BONUS DOUBLE,
JOB VARCHAR(8),
COMM DOUBLE,
MIDINIT VARCHAR(255),
HIREDATE DATE,
EMPNO VARCHAR(6),
BIRTHDATE DATE,
PHONENO VARCHAR(255),
ADDR_ID INTEGER,
PRIMARY KEY (EMPID),
FOREIGN KEY (ADDR_ID)
)
;
|
|
基于字符串的字段的长度
请注意,任何没有用 @Column(length=?) 注释的 String 字段默认是 VARCHAR(255) 。这可以表示短字段浪费的存储空间(每行)。在生产场景中,可能想更严格地控制底层管理的表的空间分配。
|
|
请比较清单 7 与 清单 6 中注释的 Employee ,查看注释在对 Spring 引擎创建的表上的效果。
如果想得到所有可用的 JPA 注释的详细解释和说明,请参考 JSR 220,它是企业 JavaBeans 3.0 规范的最终发行文档(请参阅 参考资料 )。
Address 对象的 JPA 注释
Address POJO 以相似的方式进行注释,如清单 8 所示:
package com.ibm.dw.spring2;
import javax.persistence.Column;
...
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private long id;
@Column(name = "NUM")
private int number;
@Column(name = "STNAME", length=25)
private String street;
...
|
迄今为止,所有注释对您来说都有着某些意义。毫不奇怪,清单 8 中的注释生成的表具有如清单 9 所示的模式:
CREATE TABLE ADDRESS (
ID INTEGER NOT NULL,
NUM INTEGER,
STNAME VARCHAR(25),
PRIMARY KEY (ID)
)
;
|
Spring 2 和 Java EE 5 的关系
JPA 持久性是 EJB 3.0 的一部分,后者又是 Java EE 5 规范的一部分,这意味着所有兼容的 Java EE 5 服务器(商业的、开放源码的或其他)都有符合规范的实现。这实际上就保证了在不久的将来,将有健壮、高质量的 JPA 实现可以使用。
请注意,虽然 Spring 2 利用了 EJB 3.0 规范的 JPA 持久性,但未强迫 Spring 2 的用户利用 EJB 3.0 或 Java EE 5 规范的其他元素。
从最初起,JPA 就被设计成可以在传统 EJB 容器之外独立使用。作为一个具体的示例,本教程中的应用程序受益于 JPA ,但绝对不是 EJB 3.0 或 Java EE 5 应用程序。
用 Spring DAO 实现域服务
在这一节,您将用 Spring DAO(数据访问对象)API 实现员工信息应用程序的服务接口。
实现 EmployeeService 接口
一旦 Spring 2 引擎知道了如何保持 Employee 和 Address 对象的实例,实现 EmployeeService 接口的任务就变得非常简单。
可以在服务实现中利用 Spring DAO API。Spring DAO 实现了著名的 DAO 设计模式(请参阅 参考资料)。在这个模式中,DAO 提供了一致的数据访问外观。通过传输对象执行数据提取和修改。DAO 封装了实际的数据源,并提供了操作传输对象的方法。
从架构上说,DAO API 隐藏了操作实际数据持久性 API 调用的复杂性。(除了 JPA 之外,Spring 还支持其他 ORM 技术,例如 JDO、Hibernate、iBATIS SQL Maps 和 Apache OJB。)。使用 Spring 的 DAO,可以编写能够轻松适应这些持久性 API 的数据访问代码。
除了对数据持久性 API 的抽象,Spring 的 DAO 支持把各种特定于厂商的数据访问异常映射到一套归档良好的 Spring 数据访问异常。
Spring DAO API 还提供了便于扩展的支持类。通过扩展它们,您可不必再编写烦琐而易于出错的 ORM 数据访问代码。所需的全部编码都封装在基类和支持库中,而且经过全面测试。这些类封装了通常与应用程序逻辑混杂在一起的连接和事务管理代码。在 JPA 支持类的情况下,对 JPA 实体管理器的使用完全封装在支持类中,因此您不必考虑实体管理器和实体管理器工厂的处理。
一些真实的代码可以为您展现 Spring DAO API 的多功能性。清单 10 是 EmployeeService 接口的实现,称为 EmployeeDAO ,它使用了 Spring 2 的 JpaDaoSupport 类:
import java.util.List;
import org.springframework.orm.jpa.support.JpaDaoSupport;
public class EmployeeDAO extends JpaDaoSupport implements EmployeeService {
public Employee findById(long id) {
return getJpaTemplate()。find(Employee.class, id);
}
public List<Employee> findAll() {
return getJpaTemplate()。find("select e from Employee e");
}
public List<Employee> findByEmployeeNumber(String empno) {
return getJpaTemplate()。find(
"select e from Employee e where e.empno = ?1", empno);
}
public List<Employee> findByAddressStreetName(String street) {
return getJpaTemplate()。find(
"select e from Employee e where e.addr.street = ?1", street);
}
public List<Employee> findByEmployeeLastName(String lastName) {
return getJpaTemplate()。find(
"select e from Employee e where e.lastName = ?1", lastName);
}
public List<Employee> findEmployeeWithSalaryOver(double sal) {
return getJpaTemplate()。find(
"select e from Employee e where e.salary > ?1", sal);
}
public List<Employee> findEmployeeWithCommissionOver(double comm) {
return getJpaTemplate()。find(
"select e from Employee e where e.commission > ?1", comm);
}
public Employee save(Employee emp) {
getJpaTemplate()。persist(emp);
return emp;
}
public Employee update(Employee emp) {
return getJpaTemplate()。merge(emp);
}
public void delete(Employee emp) {
getJpaTemplate()。remove(emp);
}
}
|
在清单 10 中,最值得注意的就是各方法实现中编码极其简单。JpaDaoSupport 类处理了大多数烦琐的日常工作。JpaTemplate 助手类能:
- 隐藏底层的 API 差异
- 转换异常
- 管理 JPA 实体管理器
- 打包事务处理
- 把数据访问标准化成对少数一致(跨全部 Spring DAO 实现)和定义良好的方法的访问
表 2 总结了清单 10 中的 JpaTemplate 方法,这是一种常用的方法:
方法 |
说明 |
find(Class <T> cls, Object id); |
通过实例的主键找到保持的实例。 |
find(String query); |
使用查询字符串找到保持的对象。这个强大的查询语言是 EJB QL 的扩展版本,在 JSR-220 中有完整描述(请参阅 参考资料)。 |
persist(Object obj); |
保存实例到数据库。用 JPA 的说法,它用 JPA 实体管理器保持实例。 |
merge(Object obj); |
用所提供的实例中的信息更新保存的对象实例。 |
remove(Object obj); |
从数据库中删除保持的实例。 |
在幕后,JpaTemplate 辅助类利用 JPA 实体管理器处理全部操作。辅助类处理数据访问期间例行的实体管理器检索和关闭操作。
在处理某些具体需求时,JpaTemplate 类中的其他方法可能有所帮助。请参考 Spring DAO API 的 JavaDoc 获得更多细节(请参阅 参考资料)。
有了保持 Employee 和 Address 实例的能力和 EmployeeService 的具体实现,接下来就可以根据真实关系数据库进行完整的测试了。
连接 Spring bean
至此,对于 Spring 框架什么时候和如何获得机会去实际处理 POJO,仍然不清楚。谜题的数据访问部分解决了,但仍有两个问题存在:Spring 2 引擎怎么知道要做什么,如何指定要使用哪个关系数据库?
立即就会解决这两个问题;将看到如何向 Spring 引擎提供 bean 连接模板。秘密在于叫作 dwspring-service.xml 的 XML bean 描述符文件。这个 bean 描述符文件是 Spring 2 框架操作概述 中提过的连接蓝本。它描述了 Spring 应用程序中不同的 bean 之间的关系。清单 11 显示了这个文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www./schema/beans"
xmlns:xsi="http://www./2001/XMLSchema-instance"
xsi:schemaLocation="http://www./schema/beans
http://www./schema/beans/spring-beans.xsd">
<bean id="employeeService" class="com.ibm.dw.spring2.EmployeeDAO">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="entityManagerFactory" class=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
<property name="databasePlatform"
value="oracle.toplink.essentials.platform.database.HSQLPlatform"/>
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver"/>
</property>
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:dwspring"/>
<property name="username" value="sa" />
<property name="password" value=" " />
</bean>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
|
要测试 EmployeeDAO 实现的动作,可以使用叫作 HSQLDB 的内存内(请参阅 参考资料)。HSQLDB 的可执行文件是 “有依赖项的 Spring 2” 下载的一部分。
在清单 11 中,专门配置 HSQLDB 实例的行用黑体表示。稍后(在 编写针对 RDBMS 的 DAO 集成测试)中,将看到如何修改这些行,来针对 DB2 Express-C 运行集成测试。
请记住 EmployeeDAO 实际上扩展了 JpaDaoSupport 类。这个类希望在装入的时候被 JPA EntityManagerFactory “插入”。然后它可以用这个工厂得到所有数据访问操作的 JPA EntityManager 。
图 6 以图形方式显示 bean 在 dwspring2-service.xml 文件中如何连接在一起:
实际上,清单 11 是需要由 Spring 2 引擎创建的对象的连接计划,图 6 是这些对象的图表。在清单 11 和图 6 中要注意的一个重要项目就是 EmployeeDAO 如何通过叫作 employeeService 的bean 获得。这个实例把自己的 entityManagerFactory 属性设置成另一个名为 entityManagerFactory 的 bean:
<bean id="employeeService" class="com.ibm.dw.spring2.EmployeeDAO">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
|
ref="" 标志是对上下文中定义的另一个 bean 的引用 —— 通常是在同一个文件中。
依赖性注入
用外部创建的对象来填充属性,就像刚才做的那样,这叫作 注入——更具体地讲,叫作依赖性注入 (DI),因为被注入的对象通常是接收对象进行正确操作所依赖的事物。DI 在 Spring 中框架中使用得很多。使用 DI,编写组件代码时不需要主动查询或查找依赖服务(例如,查询 EntityManagerFactory )。相反,可以就像依赖服务已经存在一样地编写组件代码,Spring 引擎会在代码执行之前把实际的依赖性注入组件。
依赖性注入的应用程序
如果查看 清单 11 中一直到 entityManagerFactory 连接的部分,您会注意到 Spring 注入了以下依赖项:
dataSource
jpaVendorAdapter
loadTimeWeaver
dataSource bean 是 org.springframework.jdbc.datasource.DriverManagerDataSource 的实例,用 HSQLDB RDBMS 的内存中实例进行了配置。
jpaVendorAdapter 属性通过连接到 Spring 应用程序实际 JPA 实现的 bean 注入。在这个示例中,使用的是 JPA 引用实现,通过 org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter 类来访问。这个类接着需要用 databasePlatform property 进行配置。这个属性被设置成 oracle.toplink.essentials.platform.database.HSQLPlatform ,此配置支持对 HSQLDB RDBMS 的访问。这个 bean 的 generateDdl 属性控制着是否生成和执行数据定义语言脚本。如果这个属性设为 true ,那么这个 bean 每次装入时,都会重新建立数据库中的方案。为了集成测试的目的,应当保持这个属性为 true 。
在 dataSource bean 的配置中,创建了 org.springframework.jdbc.datasource.DriverManagerDataSource 的实例。它的参数设置有:
- HSQLDB 数据库驱动程序
- 创建内存中数据库的 JDBC UR(JDBC URL 的 mem 部分)
- 用户名和口令(对 HSQLDB,默认分别是 sa 和 "")
最后一个 transactionManager bean 是为后面的集成测试配置的。不需要连接这个 bean,因为后面要用的测试基类会按类型查找这个 bean。
至此,您应已对 Spring 2 如何连接 bean 有了一定的连接。您还应了解如何把数据库从 HSQLDB 转换到 DB2 Express-C,这一步要在下一节进行) 编写针对 RDBMS 的 DAO 集成测试)。
编写针对 RDBMS 的 DAO 集成测试
在这一节,编写和运行一个集成测试,根据数据库测试员工信息应用程序。
测试 EmployeeService 的 EmployeeDAO 实现
现在还剩下的唯一问题是:如何和在什么时候调用 Spring 2 引擎,它怎么知道该如何使用 dwspring2-service.xml 配置文件?
看一下 EmployeeServiceIntegrationTest.java 中的集成测试源代码,答案就一目了然了。这个集成测试针对实际的 RDBMS 测试 EmployeeService 的 EmployeeDAO 实现。请参阅清单 12 中的代码片断:
package com.ibm.dw.spring2;
import java.util.Date;
...
import org.springframework.test.jpa.AbstractJpaTests;
public class EmployeeServiceIntegrationTest extends AbstractJpaTests {
private EmployeeService employeeService;
private long JoeSmithId = 99999;
public void setEmployeeService(EmployeeService employeeService) {
this.employeeService = employeeService;
}
protected String[] getConfigLocations() {
return new String[] { "classpath:/com/ibm/dw/spring2/dwspring2-service.xml" };
}
|
这套集成测试是在 Spring 2 库的 AbstractJpaTests 类的帮助下编写的。通过实现清单 12 中强调的 getConfigLocations() 方法,可以提供一个或多个需要由 Spring 2 引擎解析的 bean 配置文件。可以有多个配置文件,因为分离后端和用户界面 bean 配置文件是一种常见实践。
清单 12 中的 setEmployeeService() 是依赖性注入的示例。当 Spring 2 引擎装入这个 EmployeeServiceIntegrationTest 类时(派生自 AbstractJpaTests ),它发现一个没有填充的依赖项 —— 类型为 EmployeeService 的属性。引擎就查找 dwspring2-service.xml 文件,查找配置好的类型为 EmployeeService 的bean,并通过 setEmployeeService() 方法注入。
按类型自动连接
您可能注意到,在 dwspring2-service.xml 文件中,对于 employeeService 的注入缺少明确的注入指令。实际上,这个注入是自动发生的。在 Spring 的术语中,这叫作自动连接。
AbstractJpaTests 基类派生自 AbstractDependencyInjectionSpringContextTests 类。 AbstractDependencyInjectionSpringContextTests 通过把 Spring 的按类型自动连接特性设为默认,简化了测试。如果在应用程序的上下文(在这个示例中,由 dwspring2-service.xml 文件配置)中发现相同类型的 bean,那么它的子类(EmployeeServiceIntegrationTest 就是这样一个子类)的任何依赖项(公共属性)就被自动注入。
集成测试设置
AbstractJpaTests 针对测试的一个有用特性,就是在事务中执行测试,然后在测试后回滚所有效果的能力。这切实地加快了测试,因为在每次测试运行期间,不再需要删除和重建数据了。
清单 13 显示了执行每个测试的初始设计的代码。这个设置代码用三个 Employee 和它们的相关 Addresse 填充数据库。必须在与每个测试相同的事务内执行这个代码。否则,就不会看到注入的数据,因为事务总会被回滚。要在相同事务中执行设置,要覆盖 onSetUpInTransaction() 方法,如清单 13 所示:
protected void onSetUpInTransaction() throws Exception {
Employee emp1 = new Employee("0001", "Joe", "R","Smith",
"4853", "Engineer", 3, ‘M‘,
20000.00, 0.00, 0.00,
new Address(10, "Walker Street")
, new Date(), new Date());
Employee emp2 = new Employee("0002", "John","T","Lockheed",
"4333", "Sales", 2, ‘M‘,
40000.00, 0.00, 5000.00,
new Address(20, "Walker Street")
, new Date(), new Date());
Employee emp3 = new Employee("0003", "Mary","M","Johnson",
"4383", "Admin", 3, ‘F‘,
60000.00, 0.00, 390.00,
new Address(123, "Booth Ave")
, new Date(), new Date());
employeeService.save(emp1);
employeeService.save(emp2);
employeeService.save(emp3);
JoeSmithId = emp1.getEmpid();
}
|
请注意,通过基于 JPA 的 employeeService 创建并保持 Employee 实例有多简单。因为 save() 方法调用 JPA 的 persist() 操作,而且操作会从 Employee 级联到 Address 对象(在 Employee POJO 的 JPA 注释指定这个),所以也可以依靠 JPA 在地址表中创建新记录。
各测试每次执行时,都会执行 onSetUpInTransaction( ) 中的设置代码。这确保了在每次测试之前,都有三个员工。
作为测试示例,清单 14 显示了 EmployeeDAO 的 update() 方法的测试方法 testModifyEmployee() :
public void testModifyEmployee() {
String oldLastName = "Lockheed";
String newLastName = "Williams";
Employee emp = employeeService
.findByEmployeeLastName(oldLastName)。get(0);
emp.setLastName(newLastName);
Employee emp2 = employeeService.update(emp);
assertEquals(newLastName, emp2.getLastName());
List<Employee> results = employeeService
.findByEmployeeLastName(oldLastName);
assertEquals(0, results.size());
results = results = employeeService
.findByEmployeeLastName(newLastName);
assertEquals(1, results.size());
}
|
清单 14 中的测试用例 testModifyEmployee() 用 EmployeeService 的 update() 把员工“John Lockheed”的姓氏从“Lockheed”改成“Williams”。然后它调用 findByEmployeeLastName("Williams") ,检验最后有一个员工“Williams”。它还检验没有姓氏为“Lockheed”的员工存在。
EmployeeServiceIntegrationTest 中还有其他许多测试。可以研究这些代码查看如何利用 EmployeeService 实现上的方法操纵实际数据(请参阅 下载)。
接下来,设置根据 RDBMS 运行这些集成测试的环境。
下载和安装 JPA 引用实现
JPA 是 EJB 3.0 规范的一部分。EJB 3.0 又是 Java EE 5 规范的核心组件。因为这点,JPA 的开源引用实现成为了 GlassFish Java EE 5 引用服务器的一部分。
如果还没有做,那么请立即下载和安装 JPA 实现来执行集成测试(请参阅 参考资料)。
准备类路径
要在代码中访问 JPA 功能,在构建和运行时类路径中必须有 JPA 引用实现:
- 在 Eclipse 中,在导航器面板中右击项目名称,并选择 Properties。
- 选择 Java Build Path,然后在对话框中,选择 Libraries 选项卡。
- 单击 Add External JARs 按钮,并确保包含了 JPA 下载的 toplink-essentials.jar。
要成功地编译和运行集成测试,在构建和运行时类路径中必须有依赖的库。表 3 显示了在构建和运行时类路径中必须有的 JAR 文件。可以在 Eclipse 中设置这些路径:
JAR 文件 |
原始下载 |
commons-logging.jar |
spring-framework-2.x-with-dependencies.zip |
db2cc.jar |
IBM DB 2 Express-C 发布 |
db2cc_licence_cu.jar |
IBM DB2 Express-C 发布版 |
hsqldb.jar |
spring-framework-2.x-with-dependencies.zip |
junit.jar |
spring-framework-2.x-with-dependencies.zip |
log4j-1.x.xx.jar |
spring-framework-2.x-with-dependencies.zip |
persistence.jar |
spring-framework-2.x-with-dependencies.zip |
spring.jar |
spring-framework-2.x-with-dependencies.zip |
spring-core.jar |
spring-framework-2.x-with-dependencies.zip |
spring-jpa.jar |
spring-framework-2.x-with-dependencies.zip |
spring-mock.jar |
spring-framework-2.x-with-dependencies.zip |
toplink-essential.jar |
JPA 引用实现下载 |
包含 persistence.xml 文件
虽然在这个 Spring JPA 集成场景中, persistence.xml 文件没有提供配置信息,但 JPA 规范要求这个文件。
persistence.xml 文件是持久性单元 的说明。在 JPA 术语中,持久性单元包含 EntityManagerFactory (和相关的配置信息),它创建的 EntityManagers 和这些 EntityManager 管理的类(以及这些类的元数据,以注释或 XML 形式)。
在这个示例中,persistence.xml 文件非常简单,如清单 15 所示:
<persistence xmlns="http://java./xml/ns/persistence" version="1.0">
<persistence-unit name="dwSpring2Jpa" transaction-type="RESOURCE_LOCAL"/>
</persistence>
|
在某些厂商的配置中,persistence.xml 文件可以包含相关的说明信息,但是 Spring/JPA 集成中没有。只需要确定具有一份 META-INF/persistence.xml 的副本即可。
运行 Spring 集成测试
构建路径配置之后,即可运行集成测试。
要在 Eclipse 中运行集成测试,请在 Navigator 面板中选中 EmployeeServiceIntegrationTest.java 文件,然后右击选中 Run As -> JUnit Test。这就开始了集成测试。请记住这个测试是针对一个内存中的数据库运行的,非常快。在执行期间,应当在 Eclipse 的控制台窗口看到表创建、SQL 插入和查询的日志消息。请参阅图 7 查看集成测试的运行示例:
把数据源从内存中的数据库切换到 DB2 Express-C
一般来说,在使用 Spring DAO 和 DAO 设计模式时,对应用程序代码来说,数据源是完全封装并隐藏的。实际上,示例中的代码完全独立于使用的关系数据库,而且独立于使用的 JPA 厂商。这种灵活性允许您在执行得快的数据库(例如内存中的数据库)上测试应用程序代码,然后把同样的代码部署到健壮和可伸缩的商业级数据库(例如 IBM DB2 Express-C)上。
要在不同的数据库之间切换,只需要修改 dwspring2-service.xml bean 描述符文件。清单 16 显示了从 HSQLDB 切换到 IBM DB2 Express-C 需要的修改(用黑体强调):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www./schema/beans"
xmlns:xsi="http://www./2001/XMLSchema-instance"
xsi:schemaLocation="http://www./schema/beans
http://www./schema/beans/spring-beans.xsd">
<bean id="employeeService" class="com.ibm.dw.spring2.EmployeeDAO">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="entityManagerFactory" class=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
<property name="databasePlatform"
value="oracle.toplink.essentials.platform.database.DB2Platform"/>
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver"/>
</property>
</bean>
<bean id="dataSource" class=
"org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.ibm.db2.jcc.DB2Driver"/>
<property name="url" value="jdbc:db2://192.168.23.36:50000/dwspring"/>
<property name="username" value="bill" />
<property name="password" value="lotus123" />
</bean>
<bean id="transactionManager" class=
"org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
|
The modifications highlighted 清单 16 中突出的修改假设在 192.168.23.36 的端口 50000 (默认的)上有一个 DB2 Express-C 实例。需要创建叫作 dwspring 的数据库,并向应用程序用户(在清单 16 中是 bill ;请把它改成自己的特定用户)提供完整访问。
现在再次运行集成测试。测试连接到 DB2 Express-C 数据库,创建表,并执行全部测试。这次的执行稍微慢了一些 —— 因为测试要跨网络执行,还要使用磁盘上的数据存储——您会注意到在执行中有些小变化。图 8 显示了在 Eclipse 中针对 DB2 Express-C 的典型运行:
应用程序的数据层现在已经完成并经过了完整测试。这一层可以用于各种不同的用户界面。例如,可以在上面放置一个命令行接口层,或者创建使用它的 GUI 胖客户。在用户界面和数据访问层之间的解耦非常重要,因为它使您能够轻松重新设计和独立地应用经过测试的数据层代码。
对于这个练习,要使用 Spring MVC 为应用程序创建基于 Web 的用户界面。
理解 Spring MVC
在这一节,您将使用 Spring MVC 为员工信息应用程序创建基于 Web 的用户界面。
用 Spring 实现 MVC 设计模式
模型-视图-控制器(MVC)设计模式是把 Web 层和数据层组件连接在一起,创建 Web 应用程序的最好方式。几乎所有的现代应用程序框架都提供了对构建 MVC 模式应用程序的支持。
MVC 模式提供的一个关键优势就是应用程序中数据和表示之间清晰的隔离。这种清晰的隔离让数据和表示能够相互独立地发展。在生产系统中,这非常有价值,因为数据和表示随着时间的变化和需求的不同会有所变化。
对于 MVC 设计模式的深入介绍,请参阅 参考资料。以下讨论仅为概述,仅适用于我们的示例应用程序。
应用程序的模型部分包含的元素封装了数据和数据上的操作。实际上,已经看到了示例应用程序中的模型 —— 域模型。整个域模型创建和测试时,没有整合任何表示元素(用户界面、报表,等等)。MVC 设计模式允许在独立于视图的情况下创建和测试模型。
应用程序的视图部分完全是用 Java Server Pages(JSP)技术和 JSP Standard Tag Library(JSTL)创建的。您很快就会发现,可以独立于模型创建视图。在生产中这非常重要,因为 Web 页面设计人员(而不是 Java 开发人员)可以处理视图的创建和测试。
控制器是模型和视图之间的关键连接。Spring MVC 是构建基于 Web 的应用程序的框架。在 Spring MVC 中,控制器处理传入的 HTTP Web 请求。
有一组功能丰富的控制器库组件可以用来开发子类。所有 Spring MVC 控制器库类都实现了 org.springframework.web.servlet.mvc.Controller 接口。这个接口只有一个方法:
ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response)
throws Exception
|
控制器检查和处理传入的请求,然后返回 ModelAndView 对象。Spring 有一个叫作 org.springframework.web.servlet.mvc.AbstractController 的抽象类,它实现了这个接口并处理大多数缓存和会话管理特性。在实践中,所有专门的 Spring 控制器都派生自这个抽象类。表 4 描述了在示例应用程序中使用的两个 Spring 控制器类:
控制器类 |
说明 |
AbstractController |
抽象控制器基类。在控制器不需要处理传入的命令/参数或处理表单时有用。在示例应用程序中,它呈现初始页面,列出数据库中的全部员工。 |
AbstractCommandController |
处理传入命令的抽象控制器类。这个类解析传入的 HTTP 请求并把指定的 Java 对象实例绑定到请求参数,从而实现在控制器逻辑中对参数的轻松处理。 |
关于 Spring 中可以使用的其他控制器基类的更多信息,请参阅 参考资料。
创建 MainController
这个应用程序中的 MainController 处理进入应用程序的初始进入请求。这是在用户通过 URL http://localhost:8080/dwspring/index.cgi 进入应用程序的主页时发生的。
这个控制器显示系统中的所有员工,并允许用户单击员工号上的连接,得到员工细节。MainController 的代码如清单 17 所示:
package com.ibm.dw.spring2.web;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import com.ibm.dw.spring2.Employee;
import com.ibm.dw.spring2.EmployeeService;
public class MainController extends AbstractController{
public ModelAndView handleRequestInternal(HttpServletRequest req,
HttpServletResponse resp) {
List <Employee> emps = employeeService.findAll();
return new ModelAndView("home", "employees", emps);
}
private EmployeeService employeeService;
public void setEmployeeService(EmployeeService employeeService) {
this.employeeService = employeeService;
}
}
|
在清单 17 中,请注意 MainController 没有显示任何用户界面。相反,它访问数据层,得到员工列表。为了得到列表,它调用 EmployeeService 实现的 findAll() 方法。这个实现通过 Spring 的依赖性注入被 “注入” 控制器,是 EmployeeDAO 的实例。马上就会在 dwspring2-servlet.xml bean 描述符中配置它(请参见 连接 Spring MVC bean)。
要呈现用户界面,控制器创建一个 ModelAndView 对象并返回它。ModelAndView 对象创建时有三个参数,如表 5 所述:
位置 |
参数 |
说明 |
1 |
视图名称 |
这是命名视图的字符串。这个字符串被传递给视图解析器组件,解析为特定视图/表示组件 —— 本例中是 JSP。视图解析器在运行时由 Spring 引擎连接 。在 dwspring2-servlet.xml bean 描述符文件中配置视图解析器。 |
2 |
模型名称 |
命名从域模型提取的数据的字符串。数据本身在第三个参数中传递。这个数据通常由视图在执行期间呈现。对于 JSP,这个名称是变量的名称。变量值可以用 JSP 命令或 JSTL 标记呈现。 |
3 |
模型对象 |
这是视图呈现的模型数据。在视图中,可按照参数 2 提供的名称引用。 |
要掌握 Spring MVC 的操作,重要的是理解请求流程。图 9 显示了通过 Spring MVC 组件的请求流:
在图 9 中,虚线矩形中的是 Spring 组件。进入请求先由 Spring 的 DispatcherServlet 处理。这个 servlet 通过连接的 URL 映射器组件对请求的进入 URL 进行映射。这个映射器组件提供了控制请求的实际控制器。一旦控制器处理请求,就把视图名称传递回 Spring MVC。然后 Spring MVC 调用连接的视图解析器组件,传递进要呈现的模型数据。视图解析器组件把视图名称解析为视图对象。这个视图对象被传递给模型数据,并向用户呈现。
Both the URL 映射器和视图解析器都是 Spring 2 提供的组件,可以在 bean 描述符 XML 文件中指导 Spring 引擎连接它们。
连接 Spring MVC bean
在 Spring MVC 中,Web 应用程序是在 Web 应用程序上下文 中执行的。这个上下文由 bean 描述符连接 XML 文件配置,就像数据层代码一样。
默认情况下,用以下规则形成配置文件的名称:采用 Spring DispatcherServlet 的 servlet 名称,后面加上 _servlet.xml。
Spring DispatcherServlet 类通过 Web 容器(在这个示例中是 Tomcat)接受传入的 Web 请求,并把它分配到某个控制器。
在这个示例中,DispatcherServlet 用 dwspring2 名称配置,因此,配置文件应当在 dwspring2_servlet.xml。这个配置文件如清单 18 所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www./schema/beans"
xmlns:xsi="http://www./2001/XMLSchema-instance"
xsi:schemaLocation="http://www./schema/beans
http://www./schema/beans/spring-beans.xsd">
<bean id="mainController" class="com.ibm.dw.spring2.web.MainController">
<property name="employeeService">
<ref bean="employeeService"/>
</property>
</bean>
<bean name="empDetailsController" class="com.ibm.dw.spring2.web.EmpDetailsController">
<property name="employeeService">
<ref bean="employeeService"/>
</property>
</bean>
<bean id="controllermap" class=
"org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/home.cgi">mainController</prop>
<prop key="/empdet.cgi">empDetailsController</prop>
</props>
</property>
</bean>
<bean id="viewResolver" class=
"org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/jsp/" />
<property name="suffix" value=".jsp"/>
</bean>
</beans>
|
在清单 18 中,可以看到不同 bean 的连接。首先,mainController bean 是 MainController 类的实例,被注入了一个到 employeeService bean 的引用。当然这个 bean 是在 dwspring2-service.xml 中在数据层定义的 EmployeeDAO 的实例。
另一个控制器,叫作 empDetailsController ,也通过引用注入了相同的 employeeService 实例。在这一节后面将看到这个控制器的代码。
被连接的第三个 bean 是 controllermap 。这是 org.springframework.web.servlet.handler.SimpleUrlHandlerMapping 的实例。这个 Spring 提供的 bean 能根据映射属性把进入的 URL 请求映射到不同的控制器。在这个示例中,/home.cgi 被映射到 MainController ,/empdet.cgi 被映射到 empDetailsController 。
连接的最后一个 bean 是视图解析器。这个 Spring 提供的 bean 把视图名称映射到实际视图资源。这个示例中使用的是 org.springframework.web.servlet.view.InternalResourceViewResolver 的实例。这个视图解析器根据进入的视图名称,添加前缀和后缀,创建资源 URL。在清单 18 中,前缀被配置为 /jsp,后缀是 .jsp。例如,名为 home 的视图被映射到 /jsp/home.jsp。这意味着 URL /jsp/home.jsp 呈现名为 home 的视图。
配置 Spring DispatcherServlet
WEB-INF/web.xml 部署描述符包含 DispatcherServlet 的配置。清单 19 显示了相关的代码片断:
<servlet>
<description>
Spring MVC Dispatcher Servlet</description>
<display-name>
DispatcherServlet</display-name>
<servlet-name>dwspring2</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dwspring2</servlet-name>
<url-pattern>*.cgi</url-pattern>
</servlet-mapping>
|
指定 <load-on-startup> 确保在第一次启动 Web 应用程序时,装入 servlet。这还会触发 dwspring2-servlet.xml 配置文件的解析。
<servlet-mapping> 标签告诉 Tomcat 对 for *.cgi 资源的全部 Web 请求路由到 DisplatcherServlet 。* 号代表通配符匹配。所以,任何 http://host:port/dwspring/*.cgi 形式的请求,都被转发到 DispatcherServlet 。
用 Spring MVC 创建基于 JSP/JSTL 的用户界面
MainController 类把 Employee 的 List 传递给 home 视图。 home 视图由 InternalResourceViewResolver 解析成 /jsp/home.jsp。清单 20 是 home.jsp 的代码。这个页面只显示在 ModelAndView 中以 employees 传递过来的员工列表。
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://java./jsp/jstl/core" prefix="c" %>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link rel="stylesheet" type="text/css" href="css/dwstyles.css"/>
<title>dW Spring 2 Employee Data from DB2 via JPA</title>
</head>
<body>
<h1>Spring 2 JPA Employee List</h1>
<br>
<table>
<tr>
<th>Employee number</th>
<th>Name</th>
</tr>
<c:forEach var="emp" items="${employees}">
<tr>
<td><a href="empdet.cgi?empID=${emp.empid}">${emp.empno}</td>
<td>${emp.firstName} ${emp.midInitial} ${emp.lastName}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
|
在清单 20 中,可以看到用 JSTL 的 <c:forEach> 标记迭代 ${employees} 列表中的每个员工。代码在 ${emp.empno} 信息周围创建链接。例如,围绕 Joe Smith 这行生成的 URL 是 http://localhost:8080/dwspring/empdet.cgi?empID=1。
在点击这个 URL 链接时,映射到 empdat.cgi 的资源被激活。根据 web.xml 配置,Tomcat 知道要把全部 *.cgi 资源请求发送给 Spring 的 DispatcherServlet ,而且 Spring 的 DispatcherServlet 根据 dwspring2-servlet.xml 中的配置,也知道用 SimpleUrlHandlerMapping 。SimpleUrlHandlerMapping 告诉 Spring 引擎把请求引导到 EmpDetailsController 。
处理命令的控制器
EmpDetailsController 的代码如清单 21 所示:
package com.ibm.dw.spring2.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;
import com.ibm.dw.spring2.Employee;
import com.ibm.dw.spring2.EmployeeService;
public class EmpDetailsController extends AbstractCommandController{
public EmpDetailsController() {
setCommandClass(EmployeeDetailsCommand.class);
}
private EmployeeService employeeService;
public void setEmployeeService(EmployeeService employeeService) {
this.employeeService = employeeService;
}
protected ModelAndView handle(HttpServletRequest req,
HttpServletResponse resp, Object cmd, BindException ex) throws Exception {
EmployeeDetailsCommand ecmd = (EmployeeDetailsCommand) cmd;
Employee emp = employeeService.findById(ecmd.getEmpID());
return new ModelAndView("empdetails", "emp", emp);
}
}
|
EmpDetailsController 是 AbstractCommandController 的子类。 AbstractCommandController 是个有用的控制器,在处理根据按钮或 URL 点击需要执行的命令时,可以从它派生子类。
在这个示例中,用户点击员工编号 URL,命令被用来显示员工的详细信息。
AbstractCommandController 主要的增值是:它解析进入的请求,得到请求参数,并把参数绑定到所定义的进入命令类的实例。进入请求参数的名称与命令类的属性名称匹配。命令类叫作 EmployeeDetailsCommand 。EmployeeDetailsCommand 的代码如清单 22 所示:
package com.ibm.dw.spring2.web;
public class EmployeeDetailsCommand {
private long empID;
public long getEmpID() {
return empID;
}
public void setEmpID(long empID) {
this.empID = empID;
}
}
|
在清单 22 中,可以看到 EmployeeDetailsCommand 命令类的简单结构。它只有一个属性,叫作 empID 。AbstractCommandController 查询叫作 empID 的进入请求参数,并把它绑定到 EmployeeDetailsCommand 类的实例。然后把它传递给 EmployeeDetailsController ,在这里调用 handle() 方法(请参阅 清单 21)。
一般来说,可以从 AbstractCommandController 派生子类,处理任意数量的进入请求参数。需要做的全部工作就是用对应的属性定义命令类。
在 清单 21 中,EmployeeDetailsController 把传递过来的 cmd 对象的类型转换回 EmployeeDetailsCommand 实例,并提取 empID 。然后用 empID 查询特定 Employee 实例的 EmployeeService 。这是通过 DAO 的 employeeService.findById() 方法进行的。
在 清单 21 中,生成的 Employee 记录作为 作为 emp ,被传递给视图。指定的视图是 empdetails 视图。同样,Spring 引擎查看 InternalResourceViewResolver 来解析视图。这个视图解析器加上适当的前缀和后缀,并返回 /jsp/empdetails.jsp 作为视图处理器。
empdetails.jsp 的代码如清单 23 所示:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://java./jsp/jstl/core" prefix="c" %>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link rel="stylesheet" type="text/css" href="css/dwstyles.css"/>
<title>dW Spring 2 Employee Detail Information</title>
</head>
<body>
<h1>Spring 2 JPA Employee Details</h1>
<br>
<table>
<tr>
<th colspan="2">Employee Information</th>
</tr>
<tr>
<td>Employee No.</td><td>${emp.empno}</td>
</tr>
<tr>
<td>Name</td><td>${emp.firstName} ${emp.midInitial} ${emp.lastName}</td>
</tr>
<tr>
<td>Address</td><td>${emp.addr.number} ${emp.addr.street}</td>
</tr>
<tr>
<td>Phone</td><td>${emp.phoneNumber}</td>
</tr>
<tr>
<td>Salary</td><td>${emp.salary}</td>
</tr>
<tr>
<td>Bonus</td><td>${emp.bonus}</td>
</tr>
<tr>
<td>Commission</td><td>${emp.commission}</td>
</tr>
</table>
<br/>
<br/>
<a href="home.cgi">Go back to employee list</a>
</body>
</html>
|
在清单 23 中,进入的 ${emp} 变量被用来显示 HTML 表格中的员工详细信息。
请注意,页面底部的链接映射回员工清单页面。URL 是 home.cgi。如果点击这个链接,映射过程就再次开始,并传递到 MainController 、然后 /jsp/home.jsp,依次类推。
添加层级样式表
home.jsp 和 empdet.jsp 用 css/dwstyles.css 样式表格式化它们的 HTML。样式表代码如清单 24 所示:
h1 {
font-family: arial;
font-size: 28;
align: left;
font-weight: bold;
font-style: italic;
color: green;
}
h2 {
font-family: serif, times;
font-size: 18;
align: left;
color: blue;
}
th {
font-family: verdana, arial;
font-size: 13;
font-weight: bold;
align: left;
background-color: black;
color: white;
}
td {
font-family: verdana, arial;
font-size: 12;
font-style: italic;
text-align: left;
}
table {
border-style: solid;
border-width: thin;
}
.label {
font-size: 16;
font-weight: bold;
font-style: normal;
text-align: right;
}
.name {
font-size: 24;
font-weight: bold;
font-style: italic;
font-family: serif, times roman;
}
|
创建 Eclipse WTP 动态 Web 项目
要查看完整应用程序的效果,需要:
- 编译代码。
- 构建可部署的 WAR 文件。WAR 文件是用标准格式创建的 JAR 文件,用于在 J2EE 兼容的 Web 层容器(例如 Tomcat)中部署。
- 把 WAR 文件部署到 Tomcat 服务器。
用 File->New->Project... 在 Eclipse 中创建新的动态 Web 项目。在 New Project 向导中,在 Web 分类下选择 Dynamic Web Project,把项目命名为 spring2Web. See Figure 10:
接下来,添加源文件,并如图 11 所示安排它们的位置。如果已经下载了源文件发布(请参阅 下载),可以在文件管理器中拖放文件,把这些文件添加到 src 文件夹。
添加应用程序库
与数据层代码的集成测试环境不同,WAR 文件中的 Web 应用程序必须包含它需要的全部库 JAR。图 12 显示了应当拖放到 WEB-INF/lib 目录的 JAR 文件:
在图 12 中,请注意可以在有依赖项的 Spring 下载的 lib\j2ee 下找到 jstl.jar 和 servlet-api.jar。 standard.jar 标记库来自 lib\jakarta-taglibs 目录。
除了库 JAR 文件,图 12 还显示了其他配置文件的位置。请确保在继续之前找到了需要的文件。
从 Eclipse 项目导出 WAR 文件
要创建能部署到 Tomcat 的 WAR 文件,需要构建项目并把它导出为 WAR 文件。
可以在导航器的 spring2Web 项目上右击,选择 Build Project,为 Tomcat 构建项目。
把项目导出成 WAR 文件,请选择 File->Export... 。在导出向导中,选择 WAR File。Export 向导对话框如图 13 所示。把导出的 WAR 文件命名为 dwspring.war 并单击 Next。
Tomcat 5.5 作为 Spring MVC 应用程序的宿主
这一节介绍如何配置 Apache Tomcat,这是个开源的 Web 层容器,它与 Spring 协作,容纳示例应用程序。
Tomcat 5.5 中的 JSP 和 servlet 支持
毫无疑问,Tomcat 是目前为止最流行和最成熟的开源的 Web 层服务器。作为 Web 层服务器,Tomcat 可以运行和执行包含 JSP 和 servlet 的 Web 应用程序。将要使用的 Tomcat 5.5.x 支持 Servlet 2.4 和 JSP 2.0 标准(请参阅 参考资料)。
如果您还未安装,请立即下载和安装最新版的 Tomcat 5.5.x 来运行示例(请参阅 参考资料)。请从服务器上选择 ZIP 文件下载,并把它解压缩到选中的目录。
这一节提供一些 Tomcat 服务器的通用操作指南。请参阅 Tomcat 文档获得更多细节。
基本 Tomcat 5.5 操作
对 Tomcat 服务器执行得最频繁的操作有:
启动和停止 Tomcat 服务器
在解压缩 Tomcat 服务器的可执行文件后,可以进入服务器的 bin 子目录,运行 startup.bat 脚本启动服务器。弹出另一个运行服务器的控制台窗口。
要关闭服务器,请进入 bin 子目录并运行 shutdown.bat 脚本。
部署 应用程序到 Tomcat
要容易地部署应用程序到 Tomcat,可以用内置的管理器 Web 应用程序或直接把 WAR 文件拷贝到 Tomcat 服务器的 webapps 子目录。
如果想用管理器 Web 应用程序,需要给一个用户提供“manager”角色,启用访问。在开始 Tomcat 服务器之前,请查看 conf 子目录中叫作 tomcat-users.xml 的文件。在这个文件中,查看:
<user username="tomcat" password="tomcat" roles="tomcat "/>
|
把这行改成:
<user username="tomcat" password="tomcat" roles="tomcat, manager"/>
|
然后可以通过 http://localhost:8080/manager/html 访问管理器应用程序。
如果通过把 WAR 文件直接拷贝到 webapps 目录来部署,可能要在 Tomcat 服务器检测到更新并部署新 WAR 文件之前稍等一会。
如果重新部署失败
与使用示例应用程序时的体验一样,可能需要把代码的新版本重新部署到 Tomcat 服务器。
根据使用的 Tomcat 发行版,在部署/重新部署 dwspring.war 文件时,有时会遇到部署问题。如果在向服务器部署时遇到问题,部署 WAR 的解决问题的途径是:
- 停止 Tomcat 服务器。
- 进入 webapps 目录,确保删除了 dwspring.war 文件。
- 删除 dwspring 目录。
- 把新的 dwspring.war 文件复制到 webapps 目录。
- 重新启动 Tomcat 服务器。
为 Spring 2 准备 Tomcat
在可以向 Tomcat 成功部署 dwspring.war 文件之前,需要进行一些服务器设置。
在这一节执行的主要过程有:
- 把 Spring 2 类装入器添加到 Tomcat
- 把 Spring 2 上下文装入器侦听器添加到 Tomcat
- 把 DB2 JDBC 驱动程序复制到 Tomcat
- 为 Tomcat 配置 JNDI DB2 数据源
把 Spring 2 类装入器添加到 Tomcat 服务器
当 Spring JPA 应用程序在 Tomcat 上运行时,要让 JPA 支持正常工作,需要在类装入期间进行字节码“连接”。来自 Tomcat 的标准类装入器不支持这个。需要用特定于 Spring 的类装入器实现这个功能。
要把这个特定于 Spring 的类装入器安装到 Tomcat 服务器,首先要把 spring-tomcat-weaver.jar 拷贝到 Tomcat 的 server/lib 子目录。这个目录包含的库属于 Tomcat 服务器私有。可以在 Spring 2.0 下载的 dist/weaver 目录下找到 spring-tomcat-weaver.jar 库。
接下来,必须让 Tomcat 知道对于示例应用程序,应当替换标准类装入器。可以在 WAR 文件的 META-INF/context.xml 文件中指定这点。清单 25 中的粗体代码配置类装入器:
清单 25. 在 META-INF/context.xml 文件中配置类装入器
<Context>
<Loader loaderClass="org.springframework.instrument.
classloading.tomcat.TomcatInstrumentableClassLoader"/>
...
</Context>
|
把 Spring 2 的上下文装入器侦听器添加到 Tomcat
Spring 2 要求挂接到 Tomcat 的上下文装入管道。可以在 WAR 文件的 WEB-INF/web.xml 文件添加以下行进行这个配置:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
|
在 web.xml 文件中,这必须在 <servlet> 和 <servlet-mapping> 定义之前。
把 DB2 JDBC 驱动程序拷贝到 Tomcat 服务器
Tomcat 是 Web 层容器,所以能够管理自己的数据库连接,也能做连接池。数据库源由 Tomcat 管理,可以通过标准的 Java 命名和目录接口 (JNDI)查询机制访问。员工系统在 Tomcat 内作为 Web 应用程序运行,应当通过 Tomcat 的 JNDI 得到数据源。
要让 Tomcat 在 Web 应用程序部署期间找到 JDBC 驱动程序,需要把 JAR 文件拷贝到 Tomcat 的系统库目录。在启动 Tomcat 服务器之前,请把两个 JDBC 驱动程序 JAR 文件从 DB2 发布拷贝到 Tomcat 的 common\lib 目录。这两个文件的名称是 db2cc.jar 和 db2cc_licence_cu.jar。
放在 common\lib 目录中的库可以供 Tomcat 服务器和 Web 应用程序共同使用。
在 Tomcat 5 上配置 DB2 数据源管理和 JNDI
可以把一个应用程序可以访问的 DB2 数据源配置成 Web 应用程序上下文的 JNDI 资源。方法是把清单 26 中突出的代码放在自己 WAR 的 META-INF/context.xml 文件中:
清单 26. 在 META-INF/context.xml 中配置 JNDI 资源
<Context>
...
<Resource name="jdbc/dwspring2" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000"
username="bill"
password="lotus123" driverClassName="com.ibm.db2.jcc.DB2Driver"
url="jdbc:db2://192.168.23.36:50000/dwspring"/>
</Context>
|
需要替换清单 26 中的 DB2 Express-C 服务器主机、用户名和口令,反映自己的 DB2 Express-C 安装情况。
清单 27 的配置通过名称 java:comp/env/jdbc/dwspring2 提供了 JNDI 数据源。
还必须向部署描述符 web.xml 文件添加资源引用。请把这个 <resource-ref> 元素添加到 web.xml 文件的末尾,如清单 27 所示:
清单 27. 添加数据源的 JNDI 资源引用
...
<resource-ref>
<description>DB Connection</description>
<res-ref-name>jdbc/dwspring2</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
|
清单 27 的配置使得可以在 Web 应用程序中使用容器 JDBC 管理的数据源。
为 Tomcat 部署配置 Spring 2 应用程序
要为 Tomcat 部署配置数据层代码,仍然需要两个细节:
- 告诉 Spring 2 引擎关于 bean 描述符配置文件的位置
- 把 Spring 2 数据源配置的连接改为通过 JNDI 使用 Tomcat 的数据源管理和池管理
告诉 Spring 2 引擎配置文件的位置
要让 Spring 2 引擎连接需要的数据层 bean,首先必须中找到并处理数据层的 bean 描述符配置文件。
在这个示例中,文件叫作 dwspring-service.xml。需要在部署的时候,在提供给 Tomcat 服务器的上下文参数中指定这个文件的位置。
这个上下文参数需要是 WEB-INF/web.xml 部署描述符的第一个元素,在清单 28 中用粗体表示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java./xml/ns/j2ee"
xmlns:xsi="http://www./2001/XMLSchema-instance"
xsi:schemaLocation="http://java./xml/ns/j2ee
http://java./xml/ns/j2ee/web-app_2_4.xsd">
<display-name>
spring2web</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dwspring2-service.xml</param-value>
</context-param>
...
|
dwspring2-service.xml 是前面在测试时使用的配置文件的改动版本。
这个配置文件被修改成通过 JNDI 使用 Tomcat 服务器的 JDBC 连接管理,而不是自己的连接管理。
修改 Spring 2 连接查找 Tomcat JNDI 数据源
dwspring2-service.xml 中使用 Tomcat 5 得到 JNDI 数据源需要进行的修改在清单 29 中已突出显示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www./schema/beans"
xmlns:xsi="http://www./2001/XMLSchema-instance"
xsi:schemaLocation="http://www./schema/beans
http://www./schema/beans/spring-beans.xsd">
<bean id="employeeService" class="com.ibm.dw.spring2.EmployeeDAO">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="entityManagerFactory" class=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<property name="showSql" value="true"/>
<property name="generateDdl" value="false"/>
<property name="databasePlatform" value=
"oracle.toplink.essentials.platform.database.DB2Platform"/>
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver"/>
</property>
</bean>
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/dwspring2" />
</bean>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
|
清单 29 中用于创建数据源的 bean 现在是 org.springframework.jndi.JndiObjectFactoryBean 。可以用这个 bean 执行对特定的容器管理资源的 JNDI 查询。在这个示例中,Tomcat 管理的 DB2 Express-C 数据源的名称被配置成 java:comp/env/jdbc/dwspring2 。
在清单 29 中,请注意 jpaVendorAdapter 的 GenerateDdl 属性被设置成 false 。这是必需的,因为不想让 Spring 2 在每次启动应用程序时都删除和重建所有数据库表。只有在集成测试期间,这个属性才应设置成 true 。
试用 Spring 2 Web 应用程序
在这一节,将以编程方式向员工数据库添加一些数据,并试用员工信息应用程序。
在事务内添加数据到 DB2 Express-C
在可以试用应用程序之前,需要在数据库中有些员工数据。因为集成测试在每次运行时都会删除所有表(通过回滚事务),所以数据库不包含任何已经可以使用的数据。
当然,可以用 DB2 Express-C 工具手工输入数据。但在这里将学习如何用编程的方式添加数据。
集成测试基于 AbstractJpaTests 的一个精彩特性,是测试完成时数据库的所有变化都被回滚掉这一事实,这就允许下一步骤迅速执行。如果每个步骤都在事务中运行到结束,那么数据修改会在 RDBMS 中持久,这就需要在启动下一测试之前删除它们。所以在理论上,如果想向数据库添加数据,这个类一点用也没有。
幸运的是,在基于 AbstractJpaTests 的集成测试中修改的任何数据,都可以在测试中调用 setComplete() 方法,提交给 RDBMS。这个方法不是回滚数据,而是提交数据,从而让修改持久。
FillTableWithEmployeeInfo 类是 Spring2Tutorial 项目的一部分,如清单 30 所示。这个类利用这个功能把六个员工的信息保持到 DB2 Express-C。
package com.ibm.dw.spring2;
import java.util.Date;
import java.util.List;
import org.springframework.test.jpa.AbstractJpaTests;
public class FillTableWithEmployeeInfo extends AbstractJpaTests {
private EmployeeService employeeService;
public void setEmployeeService(EmployeeService employeeService) {
this.employeeService = employeeService;
}
protected String[] getConfigLocations() {
return new String[] { "classpath:/com/ibm/dw/spring2/dwspring2-service.xml" };
}
public void testTableFiller() {
Employee emp1 = new Employee("0001", "Joe", "R","Smith",
"4853", "Engineer", 3, ‘M‘,
20000.00, 0.00, 0.00,
new Address(10, "Walker Street")
, new Date(), new Date());
Employee emp2 = new Employee("0002", "John","T","Lockheed",
"4333", "Sales", 2, ‘M‘,
40000.00, 0.00, 5000.00,
new Address(20, "Walker Street")
, new Date(), new Date());
Employee emp3 = new Employee("0003", "Mary","M","Johnson",
"4383", "Admin", 3, ‘F‘,
60000.00, 0.00, 390.00,
new Address(123, "Booth Ave")
, new Date(), new Date());
Employee emp4 = new Employee("0004", "Mike","S","Lee",
"4322", "Sales", 3, ‘M‘,
30000.00, 0.00, 20000.00,
new Address(7, "Wilard Drive")
, new Date(), new Date());
Employee emp5 = new Employee("0005", "Joan","K","Winfry",
"4113", "Marketing", 2, ‘F‘,
40000.00, 0.00, 0.00,
new Address(1293, "Davis Blvd")
, new Date(), new Date());
Employee emp6 = new Employee("0006", "Steve","L","Bingham",
"4632", "Marketing", 3, ‘M‘,
50000.00, 0.00, 0.00,
new Address(5, "Booth Ave")
, new Date(), new Date());
employeeService.save(emp1);
employeeService.save(emp2);
employeeService.save(emp3);
employeeService.save(emp4);
employeeService.save(emp5);
employeeService.save(emp6);
setComplete();
}
}
|
在 Eclipse 的导航器视图,右击 FillTableWithEmployeeInfo.java 并选择 Run As... JUnit Test,用数据填充数据库。
使用员工信息应用程序
要在部署到 Tomcat 之后访问应用程序的主页,请在浏览器中输入以下 URL:http://localhost:8080/dwspring/home.cgi。这假设正在本地机器上运行 Tomcat 服务器。如果使用网络上的其他服务器,只要替换 URL 中的主机名即可。
应用程序的第一页,MainController 和 home.jsp,如图 14 所示:
每个员工编号都是一个可以点击的 URL 链接,点击之后显示包含选中员工的员工细节页面(图 15):
图 15 的页面由 EmpDetailsController 创建,通过 empdet.jsp 视图呈现。
结束语
Spring 2 是种功能全面的框架。可以用运行时通过 XML 配置文件动态连接起来的软件组件,创建基于 Web 的服务器端应用程序。
可以用 POJO 实现业务对象。POJO 易于在容器之外创建和测试,也可以在其他应用程序或同一应用程序的其他部分重用。
Spring 2 的 JPA 集成允许在 POJO 源代码内通过 JPA 注释把数据库持久性元数据添加到 POJO。
有了 Spring DAO 支持,可以创建在 POJO 上操作的服务,并用 JPA 负责持久性 —— 不需要显式地编写烦琐的实体管理器和事务管理代码。
Spring 的 JPA 测试支持允许在支持 JPA 的 POJO 和 DSO 上创建集成测试。可以在内存中运行的数据库或外部服务器上的数据上真正的 RDBMS 上执行这些测试。
可以用 Spring MVC 把基于 Web 的用户界面添加到测试过的数据层栈。Spring MVC 可以与各种视图技术一起工作,包括本教程中使用的 JSP 和 JSTL。通过保持模型(域模型)代码与用户界面代码的清晰分离, Spring 简化了 Web 应用程序的维护。
可以把基于 Web 的 Spring 应用程序打包到 WAR 文件,在 Tomcat 5 服务器上进行生产部署。
下载
描述 |
名字 |
大小 |
下载方法 |
source code |
j-spring2code.zip |
20KB |
HTTP |
|