对于数据库存取的支持,Spring提供了DAO框架,您可以不用接触底层数据库的技术细节。Spring对数据库存取时的一些异常加以重新封装,您可以选择是否处理特定的异常。在JDBC API的使用上,Spring也提供了JdbcTemplate等类以简化JDBC在API上的操作使用。关于事务的处理方面,Spring提供了编程式事务管理(Programmatic transaction management)与声明式事务管理(Declarative transaction management)。
总而言之,结合Spring IoC容器与AOP框架所提供的功能,可以简化JDBC的运用方式,并轻易获得事务等相关服务的功能。在这个章节中,将以一些实际的例子来介绍Spring于JDBC上的存取以及事务上的一些支持。
5.1 Spring持久层入门
Spring提供了DAO框架,让程序员在开发应用程序时无须耦合于特定数据库技术,这节先来简介Spring的DAO设计,并以实际的例子来入门Spring持久层。
5.1.1 Spring的DAO支持
Spring的DAO框架让您在进行数据库存取时,无须接触到所使用的特定数据库技术细节,DAO的全名为Data Access Object,在应用程序中,需要使用到数据存取时,是通过一个数据存取接口来操作的,而实际上进行数据库存取的对象要实现该接口,并在规范的方法之中实现存取时的相关细节。
举个DAO的例子,假设应用程序中有个User对象,在进行数据库存取时(例如select、insert、update、delete),应用程序不应依赖于一个实际的类实现,而可以让它依赖一个接口,例如一个IUserDAO接口:
package onlyfun.caterpillar;
public interface IUserDAO {
public void insert(User user);
public User find(Integer id);
public void update(User user);
public void delete(User user);
}
实际上进行数据库存取的类可以实现IUserDAO接口,例如定义一个简单的UserDAO类:
package onlyfun.caterpillar;
...
public class UserDAO implements IUserDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void insert(User user) {
String name = user.getName();
int age = user.getAge().intValue();
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement (
"INSERT INTO user (name,age) VALUES(?,?)");
stmt.setString(1, name);
stmt.setInt(2, age);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
finally {
if(stmt != null) {
try {
stmt.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
}
}
public User find(Integer id) {
...
return user;
}
public void update(User user) {
...
}
public void delete(User user) {
...
}
}
您的应用程序主流程在进行数据存取时,可以使用IUserDAO来声明操作接口,例如:
...
User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
IUserDAO userDao = getUserDAO();
userDao.insert(user);
...
由于依赖于接口,所以可以随时替换IUserDAO的实现类,而在IUserDAO接口声明的操作方法上,并没有任何底层数据库存取的技术细节,Spring的DAO框架正是基于这样的基本原理,将应用程序与底层存取技术隔离开来,如图5.1所示。
图5.1 DAO运作示意图
数据存取接口上只有与特定数据库存取技术无关的方法(例如update、insert、delete等),设计上依赖于接口,程序也易于测试,不让应用程序受限于只能使用某一数据库技术。
在之前的示范程序中,对于实际的数据库存取流程来说,有几个步骤是固定的,例如取得DataSource、取得Connection、处理异常等,对于不同的数据库技术,这些步骤大致上是相同的,只有少部分流程不同,就设计上而言,可以使用Template-Callback模式,将固定的流程编写于Template类之中,而对于不同的一些细节步骤,则委托Callback对象来完成,例如以下的程序片段中,非粗体字部分是固定的流程,对于其中有差异的部分,可以实现个别的Callback对象来执行或传回适当的对象:
...
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = callback.createPreparedStatement(conn);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
finally {
if(stmtCallback != null) {
try {
stmt.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
}
Spring运用了Template-Callback模式,将固定的流程编写于Template类之中(例如JdbcTemplate、HibernateTemplate类),而对于不同的一些细节步骤,则委托特定DAO支持对象来处理(可以自定义或由Spring自动产生),如图5.2所示。
图5.2 DAO Template与DAO Support
在异常处理方面,Spring也提供与特定技术无关的异常处理体系,让应用程序不会因处理特定异常(像是SQLException、HibernateException)而耦合于某种数据库或持久层技术。
首先来了解异常处理,Java的异常有Checked exception与Unchecked exception两种。Checked exception是编译时期在语法上必须处理的异常,因为这些异常通常是可以预期发生的,编译器要求一定要处理,因此必须在语法上以try...catch加以处理;Unckecked exception则是执行时期异常(Runtime exception),在异常继承架构上是java.lang.Run- timeException类的子类,通常是由于程序逻辑上的错误,在执行时期所引发的异常,异常真的发生时,可以选择使用try...catch来作处理,或是让异常直接丢出至应用程序最上层处理(例如用户图形接口)。
Checked exception的立意本来是好的,当这类异常发生时,希望的是程序设计人员可以加以处理至程序能回复正常运作,然而有时候,出现了Checked exception往往无力回复至正常运作,当这类异常在底层的数据库存取时发生(例如无法取得联机),最好的处理方式就是不处理,让异常传播至上层应用程序,由上层应用程序捕捉以显示相关消息,让用户得知问题出在哪边,而不是在底层的数据库存取程序中,作一些无能为力的处理(例如记录下无法联机的消息),然而面对Checked exception,由于编译器要求一定要处理,程序设计人员只好无可奈何的编写一些异常处理语法来处理掉这些异常。
直接使用throws在方法上声明异常,让异常可以向上层传播也不是一个好的主意,另一方面,有些程序或框架会自行继承相关的异常类,包括一些相关的异常消息,它们也会在定义接口时,于方法上声明throws某些类型的异常,然而如果在这些方法中发生了某些不是方法上声明的异常(可能由于使用的底层技术不同而有这种情况,像是JDBC或是Hibernate),就无法将之throw,只能自行编写一些try..catch来暗自处理掉,如果想要让这些异常丢出至上层,就要更多道的手续。
Spring的DAO框架并不丢出与数据库技术相关的异常,Spring所有的异常都是org.springframework.dao.DataAccessException的子类,一个与数据库技术无关的通用异常,而且DataAccessException是RuntimeException的子类,也就是说它是属于Unchecked exception,您不用被强迫使用try...catch来处理异常,而可以自己选择要不要处理,在不处理异常的情况下,也可以很简单地将异常传播至上层的应用程序。
对于JDBC存取,Spring将SQLException等转化为自己的DAO异常对象,org.springframework.dao包下提供一致性的异常处理层次,DataAccessException是这个层次的基础类,它继承自org.springframework.core包的NestedRuntimeException,而NestedRuntimeException继承自RuntimeException,对于一些异常,您可以选择处理它或者忽略它,由最上层的应用程序或是最后由JVM来处理。
如果要处理特定的异常,Spring也将异常作好了分类,例如数据库连结错误时会丢出的DataAccessResourceFailureException、SQL语法错误时的BadSqlGrammarException等,您可以针对想处理的异常加以捕捉。可以看看Spring参考手册中的DAO support章节,当中有一些对异常处理的说明,也有个DataAccessException的类继承图。
5.1.2 DataSource注入
对于不同的数据连接来源需求,Spring提供了javax.sql.DataSource注入,要更换数据来源只要在Bean定义文件中修改配置,而不用修改任何一行程序。
因为不同的系统,应用程序可能使用不同的数据来源,例如纯粹的使用JDBC、通过连接池、或是通过JNDI等,数据的来源更多是底层的行为,这些行为不应影响到上层的业务逻辑,为此,可以在需要取得连接来源的Bean对象上保留一个Datasource注入的操作接口,Spring提供org.springframework.jdbc.datasource.DriverManagerDataSource来取得它的实例,DriverManagerDataSource实现了javax.sql.DataSource,您可以在Bean定义文件中这么编写:
...
class="org.springframework.jdbc.
→ datasource.DriverManagerDataSource">
value="com.mysql.jdbc.Driver"/>
value="jdbc:mysql://localhost:3306/demo"/>
...
其中 "driverClassName"、"url"、"username"、"password" 4个属性分别是用来设置JDBC驱动程序类、数据库URL协定、用户名称和密码的。
在这里实际使用一个程序来作为DataSource注入的示范,并在示范之前有关于DAO介绍的实现,假设您定义了一个DAO的操作接口,如下所示。
DataSourceDemo IUserDAO.java
package onlyfun.caterpillar;
public interface IUserDAO {
public void insert(User user);
public User find(Integer id);
}
基于篇幅的限制,这里关于DAO的接口定义只介绍了insert() 与find() 两种方法。在这个接口中所使用到的User类则如下定义:
DataSourceDemo User.java
package onlyfun.caterpillar;
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
接着可以定义一个UserDAO类,它实现了IUserDAO接口,是实际进行数据库存取服务的对象,如下所示:
DataSourceDemo UserDAO.java
package onlyfun.caterpillar;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
public class UserDAO implements IUserDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void insert(User user) {
String name = user.getName();
int age = user.getAge().intValue();
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement (
"INSERT INTO user (name,age) VALUES(?,?)");
stmt.setString(1, name);
stmt.setInt(2, age);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
finally {
if(stmt != null) {
try {
stmt.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
}
}
public User find(Integer id) {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(
"SELECT * FROM user WHERE id=?");
stmt.setInt(1, id.intValue());
ResultSet result = stmt.executeQuery();
if(result.next()) {
Integer i = new Integer(result.getInt(1));
String name = result.getString(2);
Integer age = new Integer(result.getInt(3));
User user = new User();
user.setId(i);
user.setName(name);
user.setAge(age);
return user;
}
} catch (SQLException e) {
e.printStackTrace();
}
finally {
if(stmt != null) {
try {
stmt.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
}
UserDAO类上声明一个setDataSource() 方法,可以让您注入DataSource的实例,可以在Bean定义文件中进行依赖注入的定义,如下所示:
DataSourceDemo beans-config.xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
class="org.springframework.jdbc.
→ datasource.DriverManagerDataSource">
value="com.mysql.jdbc.Driver"/>
value="jdbc:mysql://localhost:3306/demo"/>
class="onlyfun.caterpillar.UserDAO">
可以编写一个简单的测试程序来操作UserDAO的实例,看看是否能进行数据的储存与查询:
DataSourceDemo SpringDAODemo.java
package onlyfun.caterpillar;
import org.springframework.context.ApplicationContext;
import org.springframework.context.
support.ClassPathXmlApplicationContext;
public class SpringDAODemo {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext(
"beans-config.xml");
User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
IUserDAO userDAO =
(IUserDAO) context.getBean("userDAO");
userDAO.insert(user);
user = userDAO.find(new Integer(1));
System.out.println("name: " + user.getName());
}
}
良葛格的话匣子<<<
在进行程序的测试之前,您要先开启数据库服务,并将beansconfig.xml中相关的驱动程序类、数据库URL、用户名称与密码等修改为您的设置,在这里所使用的是MySQL数据库,而建立的user表格是使用以下的SQL建立的:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT
);
程序的执行结果会先在数据库的user表格中存入一笔数据,接着根据id值查询出先前所存入的数据,最后结果会显示 "name: caterpillar" 的文字。
良葛格的话匣子<<<
您需要spring-core.jar、spring-beans.jar、spring-context.jar、spring-dao.jar与spring-jdbc.jar这几个文件,如果您使用的是spring.jar,当中已经包括了相关的API,另外也需要相依赖的commons-logging.jar,当然,为了使用JDBC,您必须要有JDBC驱动程序的jar文件。
5.1.3 DataSource置换
DriverManagerDataSource并没有提供连接池的功能,只是用来作简单的单机连接测试,并不适合使用于真正的项目之中。您可以使用DBCP以获得连接池的功能。如果使用Spring,则置换DataSource并不需要修改程序源代码,只要修改Bean定义文件就可以了,例如修改一下DataSourceDemo项目中的beans-config.xml如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
value="com.mysql.jdbc.Driver"/>
value="jdbc:mysql://localhost:3306/demo"/>
class="onlyfun.caterpillar.UserDAO">
现在所使用的是org.apache.commons.dbcp.BasicDataSource作为注入的DataSource实例,为了使用DBCP的功能,您需要在Classpath路径中设置commons-dbcp.jar、commons-pool.jar与commonscollections.jar,这些都可以在Spring所依赖版本中的lib目录下找到。
注意到在dataSource上设置了 "destroy-method" 属性,如此可以确保BeanFactory在关闭时也一并关闭BasicDataSource。
如果Servlet容器提供了JNDI(Java Naming and Directory Interface)的DataSource,也可以简单地换上这个DataSource:
...
class="org.springframework.jndi.JndiObjectFactoryBean">
...
为了使用org.springframework.indi.JndiObjectFactoryBean,您需要spring-context.jar,"jndiName" 实际上要根据所设置的JNDI查询名称,例如在Tomcat中可以这么设置server.xml:
...
type="javax.sql.DataSource"/>
factory
org.apache.commons.dbcp.BasicDataSourceFactory
url
jdbc:mysql://localhost/demo
driverClassName
com.mysql.jdbc.Driver
username
caterpillar
password
123456
maxWait
3000
maxIdle
10
maxActive
100
...