public class EmployeeBusinessServiceImpl implements IEmployeeBusinessService { public List getEmployeesWithinSalaryRange(Map salaryMap){ IEmployeeDAO empDAO = DAOFactory.getInstance() .getEmployeeDAO(); List empList = empDAO.findBySalaryRange(salaryMap); return empList; } } | 交互过程十分简洁,完全不依赖于任何持久性接口(包括JDBC)。
问题
DAO设计模式也有缺点:
代码重复:从EmployeeDAOImpl清单可以清楚地看到,对于基于JDBC的传统数据库访问,代码重复(如上面的粗体字所示)是一个主要的问题。一遍又一遍地写着同样的代码,明显违背了基本的面向对象设计的代码重用原则。它将对项目成本、时间安排和工作产生明显的副面影响。
耦合:DAO代码与JDBC接口和核心collection耦合得非常紧密。从每个DAO类的导入声明的数量可以明显地看出这种耦合。
资源耗损:依据EmployeeDAOImpl类的设计,所有DAO方法必须释放对所获得的连接、声明、结果集等数据库资源的控制。这是危险的主张,因为一个编程新手可能很容易漏掉那些约束。结果造成资源耗尽,导致系统停机。
错误处理:JDBC驱动程序通过抛出SQLException来报告所有的错误情况。SQLException是检查到的异常,所以开发人员被迫去处理它,即使不可能从这类导致代码混乱的大多数异常中恢复过来。而且,从SQLException对象获得的错误代码和消息特定于数据库厂商,所以不可能写出可移植的DAO错误发送代码。
脆弱的代码:在基于JDBC的DAO中,两个常用的任务是设置声明对象的绑定变量和使用结果集检索数据。如果SQL where子句中的列数目或者位置更改了,就不得不对代码执行更改、测试、重新部署这个严格的循环过程。
让我们看看如何能够减少这些问题并保留DAO的大多数优点。
进入Spring DAO
先识别代码中发生变化的部分,然后将这一部分代码分离出来或者封装起来,就能解决以上所列出的问题。Spring的设计者们已经完全做到了这一点,他们发布了一个超级简洁、健壮的、高度可伸缩的JDBC框架。固定部分(像检索连接、准备声明对象、执行查询和释放数据库资源)已经被一次性地写好,所以该框架的一部分内容有助于消除在传统的基于JDBC的DAO中出现的缺点。
图2显示的是Spring JDBC框架的主要组成部分。业务服务对象通过适当的接口继续使用DAO实现类。JdbcDaoSupport是JDBC数据访问对象的超类。它与特定的数据源相关联。Spring Inversion of Control (IOC)容器或BeanFactory负责获得相应数据源的配置详细信息,并将其与JdbcDaoSupport相关联。这个类最重要的功能就是使子类可以使用JdbcTemplate对象。
图2. Spring JDBC框架的主要组件
JdbcTemplate是Spring JDBC框架中最重要的类。引用文献中的话:“它简化了JDBC的使用,有助于避免常见的错误。它执行核心JDBC工作流,保留应用代码以提供SQL和提取结果。”这个类通过执行下面的样板任务来帮助分离JDBC DAO代码的静态部分:
从数据源检索连接。
准备合适的声明对象。
执行SQL CRUD操作。
遍历结果集,然后将结果填入标准的collection对象。
处理SQLException异常并将其转换成更加特定于错误的异常层次结构。
利用Spring DAO重新编写
既然已基本理解了Spring JDBC框架,现在要重新编写已有的代码。下面将逐步讲述如何解决前几节中提到的问题。
第一步:修改DAO实现类- 现在从JdbcDaoSupport扩展出EmployeeDAOImpl以获得JdbcTemplate.
import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.jdbc.core.JdbcTemplate; public class EmployeeDAOImpl extends JdbcDaoSupport implements IEmployeeDAO{ public List findBySalaryRange(Map salaryMap){ Double dblParams [] = {Double.valueOf((String) salaryMap.get("MIN_SALARY")) ,Double.valueOf((String) salaryMap.get("MAX_SALARY")) }; //The getJdbcTemplate method of JdbcDaoSupport returns an //instance of JdbcTemplate initialized with a datasource by the //Spring Bean Factory JdbcTemplate daoTmplt = this.getJdbcTemplate(); return daoTmplt.queryForList(FIND_BY_SAL_RNG,dblParams); } } | 在上面的清单中,传入参数映射中的值存储在双字节数组中,顺序与SQL字符串中的位置参数相同。queryForList()方法以包含Map(用列名作为键,一项对应一列)的List(一项对应一行)的方式返回查询结果。稍后我会说明如何返回传输对象列表。
从简化的代码可以明显看出,JdbcTemplate鼓励重用,这大大削减了DAO实现中的代码。JDBC和collection包之间的紧密耦合已经消除。由于JdbcTemplate方法可确保在使用数据库资源后将其按正确的次序释放,所以JDBC的资源耗损不再是一个问题。
另外,使用Spring DAO时,不必处理异常。JdbcTemplate类会处理SQLException,并根据SQL错误代码或错误状态将其转换成特定于Spring异常的层次结构。例如,试图向主键列插入重复值时,将引发DataIntegrityViolationException.然而,如果无法从这一错误中恢复,就无需处理该异常。因为Spring DAO的根异常类DataAccessException是运行时异常类,所以可以这样做。值得注意的是Spring DAO异常独立于数据访问实现。如果实现是由O/R映射解决方案提供,就会抛出同样的异常。
第二步:修改业务服务- 现在业务服务实现了一个新方法setDao(),Spring容器使用该方法传递DAO实现类的引用。该过程称为“设置方法注入(setter injection)”,通过第三步中的配置文件告知Spring容器该过程。注意,不再需要使用DAOFactory,因为Spring BeanFactory提供了这项功能:
public class EmployeeBusinessServiceImpl implements IEmployeeBusinessService {
IEmployeeDAO empDAO;
public List getEmployeesWithinSalaryRange(Map salaryMap){
List empList = empDAO.findBySalaryRange(salaryMap); return empList; } public void setDao(IEmployeeDAO empDAO){ this.empDAO = empDAO; } }
| 请注意P2I的灵活性;即使极大地改动DAO实现,业务服务实现也只需少量更改。这是由于业务服务现在由Spring容器进行管理。
第三步:配置Bean Factory- Spring bean factory需要一个配置文件进行初始化并启动Spring框架。这个配置文件包含所有业务服务和带Spring bean容器的DAO实现类。除此之外,它还包含用于初始化数据源和JdbcDaoSupport的信息:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <!-- Configure Datasource --> <bean id="FIREBIRD_DATASOURCE" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiEnvironment"> <props> <prop key="java.naming.factory.initial"> weblogic.jndi.WLInitialContextFactory </prop> <prop key="java.naming.provider.url"> t3://localhost:7001 </prop> </props> </property> <property name="jndiName"> <value> jdbc/DBPool </value> </property> </bean> <!-- Configure DAO --> <bean id="EMP_DAO" class="com.bea.dev2dev.dao.EmployeeDAOImpl"> <property name="dataSource"> <ref bean="FIREBIRD_DATASOURCE"></ref> </property> </bean> <!-- Configure Business Service --> <bean id="EMP_BUSINESS" class="com.bea.dev2dev.sampleapp.business.EmployeeBusinessServiceImpl"> <property name="dao"> <ref bean="EMP_DAO"></ref> </property> </bean> </beans> | 这个Spring bean容器通过调用JdbcDaoSupport提供的setDataSource()方法,设置包含DAO实现的数据源对象。
第四步:测试- 最后是编写JUnit测试类。依照Spring的方式,需要在容器外部进行测试。然而,从第三步中的配置文件可以清楚地看到,我们一直在使用WebLogic Server连接池。
package com.bea.dev2dev.business; import java.util.*; import junit.framework.*; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class EmployeeBusinessServiceImplTest extends TestCase { private IEmployeeBusinessService empBusiness; private Map salaryMap; List expResult; protected void setUp() throws Exception { initSpringFramework(); initSalaryMap(); initExpectedResult(); } private void initExpectedResult() { expResult = new ArrayList(); Map tempMap = new HashMap(); tempMap.put("EMP_NO",new Integer(1)); tempMap.put("EMP_NAME","John"); tempMap.put("SALARY",new Double(46.11)); expResult.add(tempMap); } private void initSalaryMap() { salaryMap = new HashMap(); salaryMap.put("MIN_SALARY","1"); salaryMap.put("MAX_SALARY","50"); } private void initSpringFramework() { ApplicationContext ac = new FileSystemXmlApplicationContext ("C:/SpringConfig/Spring-Config.xml"); empBusiness = (IEmployeeBusinessService)ac.getBean("EMP_BUSINESS"); } protected void tearDown() throws Exception { } /** * Test of getEmployeesWithinSalaryRange method, * of class * com.bea.dev2dev.business.EmployeeBusinessServiceImpl. */ public void testGetEmployeesWithinSalaryRange() { List result = empBusiness.getEmployeesWithinSalaryRange (salaryMap); assertEquals(expResult, result); } } |
使用绑定变量
到目前为止,我们搜索了工资介于最低值和最高值之间的雇员。假设在某种情形下,业务用户想要颠倒这一范围。DAO代码很脆弱,将不得不通过更改来满足要求的变化。这个问题在于使用了静态的位置绑定变量(用“?”表示)。Spring DAO通过支持命名的绑定变量来挽救这个情况。修改的IEmployeeDAO清单引入了命名的绑定变量(用“:<some name>”表示)。注意查询中的变化,如下所示:
import java.util.Map; public interface IEmployeeDAO { //SQL String that will be executed public String FIND_BY_SAL_RNG = "SELECT EMP_NO, EMP_NAME, " + "SALARY FROM EMP WHERE SALARY >= :max AND SALARY <= :min"; //Returns the list of employees falling into the given salary range //The input parameter is the immutable map object obtained from //the HttpServletRequest. This is an early refactoring based on //- "Introduce Parameter Object" public List findBySalaryRange(Map salaryMap); } | 多数JDBC驱动程序仅支持位置绑定变量。所以,Spring DAO在运行时将这个查询转换成位置绑定、基于变量的查询,并且设置正确的绑定变量。现在,为了完成这些任务,需要使用NamedParameterJdbcDaoSupport类和NamedParameterJdbcTemplate类,以代替JdbcDaoSupport和JdbcTemplate.下面就是修改后的DAO实现类:
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; public class EmployeeDAOImpl extends NamedParameterJdbcDaoSupport implements IEmployeeDAO{ public List findBySalaryRange(Map salaryMap){ NamedParameterJdbcTemplate tmplt = this.getNamedParameterJdbcTemplate(); return tmplt.queryForList(IEmployeeDAO.FIND_BY_SAL_RNG ,salaryMap); } } | NamedParameterJdbcDaoSupport的getNamedParameterJdbcTemplate()方法返回一个NamedParameterJdbcTemplate实例,该实例由数据源句柄进行了预初始化。Spring Beanfactory执行初始化任务,从配置文件获得所有的详细信息。在执行时,一旦将命名的参数替换成位置占位符,NamedParameterJdbcTemplate就将操作委托给JdbcTemplate.可见,使用命名的参数使得DAO方法不受底层SQL声明任何更改的影响。
最后,如果数据库不支持自动类型转换,需要如下所示,对JUnit测试类中的initSalaryMap()方法稍做修改。
private void initSalaryMap() { salaryMap = new HashMap(); salaryMap.put("MIN_SALARY",new Double(1)); salaryMap.put("MAX_SALARY",new Double(50)); }
|
Spring DAO回调函数
至此,已经说明为了解决传统DAO设计中存在的问题,如何封装和概括JdbcTemplate类中JDBC代码的静态部分。现在了解一下有关变量的问题,如设置绑定变量、结果集遍历等。虽然Spring DAO已经拥有这些问题的一般化解决方案,但在某些基于SQL的情况下,可能仍需要设置绑定变量。
在尝试向Spring DAO转换的过程中,介绍了由于业务服务及其客户机之间的约定遭到破坏而导致的隐蔽运行时错误。这个错误的来源可以追溯到原始的DAO.dbcTemplate.queryForList()方法不再返回EmployeeTO实例列表。而是返回一个map表(每个map是结果集的一行)。
如您目前所知,JdbcTemplate基于模板方法设计模式,该模式利用JDBC API定义SQL执行工作流。必须改变这个工作流以修复被破坏的约定。第一个选择是在子类中更改或扩展工作流。您可以遍历JdbcTemplate.queryForList()返回的列表,用EmployeeTO实例替换map对象。然而,这会导致我们一直竭力避免的静态代码与动态代码的混合。第二个选择是将代码插入JdbcTemplate提供的各种工作流修改钩子(hook)。明智的做法是在一个不同的类中封装传输对象填充代码,然后通过钩子链接它。填充逻辑的任何修改将不会改变DAO.
编写一个类,使其实现在Spring框架特定的接口中定义的方法,就可以实现第二个选择。这些方法称为回调函数,通过JdbcTemplate向框架注册。当发生相应的事件(例如,遍历结果集并填充独立于框架的传输对象)时,框架将调用这些方法。
第一步:传输对象
下面是您可能感兴趣的传输对象。注意,以下所示的传输对象是固定的:
package com.bea.dev2dev.to; public final class EmployeeTO implements Serializable{ private int empNo; private String empName; private double salary; /** Creates a new instance of EmployeeTO */ public EmployeeTO(int empNo,String empName,double salary) { this.empNo = empNo; this.empName = empName; this.salary = salary; } public String getEmpName() { return this.empName; } public int getEmpNo() { return this.empNo; } public double getSalary() { return this.salary; } public boolean equals(EmployeeTO empTO){ return empTO.empNo == this.empNo; } } | 第二步:实现回调接口
实现RowMapper接口,填充来自结果集的传输对象。下面是一个例子:
package com.bea.dev2dev.dao.mapper; import com.bea.dev2dev.to.EmployeeTO; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.jdbc.core.RowMapper; public class EmployeeTOMapper implements RowMapper{ public Object mapRow(ResultSet rs, int rowNum) throws SQLException{ int empNo = rs.getInt(1); String empName = rs.getString(2); double salary = rs.getDouble(3); EmployeeTO empTo = new EmployeeTO(empNo,empName,salary); return empTo; } } | 注意实现类不应该对提供的ResultSet对象调用next()方法。这由框架负责,该类只要从结果集的当前行提取值就行。回调实现抛出的任何SQLException也由Spring框架处理。
第三步:插入回调接口
执行SQL查询时,JdbcTemplate利用默认的RowMapper实现产生map列表。现在需要注册自定义回调实现来修改JdbcTemplate的这一行为。注意现在用的是NamedParameterJdbcTemplate的query()方法,而不是queryForList()方法:
public class EmployeeDAOImpl extends NamedParameterJdbcDaoSupport implements IEmployeeDAO{ public List findBySalaryRange(Map salaryMap){ NamedParameterJdbcTemplate daoTmplt = getNamedParameterJdbcTemplate(); return daoTmplt.query(IEmployeeDAO.FIND_BY_SAL_RNG, salaryMap, new EmployeeTOMapper()); } } | Spring DAO框架对执行查询后返回的结果进行遍历。它在遍历的每一步调用EmployeeTOMapper类实现的mapRow()方法,使用EmployeeTO传输对象填充最终结果的每一行。
第四步:修改后的JUnit类
现在要根据返回的传输对象测试这些结果。为此要对测试方法进行修改。
public class EmployeeBusinessServiceImplTest extends TestCase { private IEmployeeBusinessService empBusiness; private Map salaryMap; List expResult; // all methods not shown in the listing remain the // same as in the previous example private void initExpectedResult() { expResult = new ArrayList(); EmployeeTO to = new EmployeeTO(2,"John",46.11); expResult.add(to); } /** * Test of getEmployeesWithinSalaryRange method, of * class com.bea.dev2dev.business. * EmployeeBusinessServiceImpl */ public void testGetEmployeesWithinSalaryRange() { List result = empBusiness. getEmployeesWithinSalaryRange(salaryMap); assertEquals(expResult, result); } public void assertEquals(List expResult, List result){ EmployeeTO expTO = (EmployeeTO) expResult.get(0); EmployeeTO actualTO = (EmployeeTO) result.get(0); if(!expTO.equals(actualTO)){ throw new RuntimeException("** Test Failed **"); } } } | 优势
Spring JDBC框架的优点很清楚。我们获益很多,并将DAO方法简化到只有几行代码。代码不再脆弱,这要感谢该框架对命名的参数绑定变量的“开箱即用”支持,以及在映射程序中将传输对象填充逻辑分离。
Spring JDBC的优点应该促使您向这一框架移植现有的代码。希望本文在这一方面能有所帮助。它会帮助您获得一些重构工具和知识。例如,如果您没有采用P2I Extract Interface,那么可以使用重构,从现有的DAO实现类创建接口。除此之外,查看本文的参考资料可以得到更多指导。
源代码下载
收藏到ViVi 收藏此页到365Key
上一篇:彻底理解spring的定制任务(scheduling)
下一篇:关于spring中的aop的解释