要了解Spring为什么要提供统一的异常访问层次体系,得先从DAO模式说起.
不管是一个逻辑简单的小软件系统,还是一个关系复杂的大型软件系统,都很可能涉及到对数据的访问和存储,而这些对数据的访问和存储往往随着场景的不同而各异。为了统一和简化相关的数据访问操作,J2EE核心模式提出了DAO(Data Access Object,数据访问对象)模式。使用DAO模式,可以完全分离数据的访问和存储,很好的屏蔽了数据访问的差异性。不论数据存储在普通的文本文件或者csv文件,还是关系数据库(RDBMS)或者LDAP(Lightweight Derectory Access Protocol 轻量级目录访问协议),使用DAO模式访问数据的客户端代码完全可以忽视这种差异,用统一的接口访问相关的数据。
看一具体的场景:
对于大部分软件系统来说,访问用户信息是大家经常接触到的。以访问用户信息为例,使用DAO模式的话,需要先声明一个数据访问的接口,如下所示:
package com.google.spring.jdbc;
public interface IUserDao
{
public User findUserByPK(Integer id);
public void updateUser(User user);
}
对于客户端代码,即通常的服务层代码来说,只需要声明依赖的DAO接口即可,即使数据访问方式方式发生了改变,只需要改变相关的DAO实现方式,客户端代码不需要做任何的调整。
package com.google.spring.jdbc;
public class UserService
{
private IUserDao userDao;
public IUserDao getUserDao()
{
return userDao;
}
public void setUserDao(IUserDao userDao)
{
this.userDao = userDao;
}
public void disableUser(Integer userId)
{
User user = this.userDao.findUserByPK(userId);
userDao.updateUser(user);
}
}
通常情况下,用户信息存储在关系数据库中,所以,相应的我们会提供一个基于JDBC的DAO接口实现类:
package com.google.spring.jdbc;
public class JDBCUserDao implements IUserDao
{
@Override
public User findUserByPK(Integer id)
{
// TODO Auto-generated method stub
return null;
}
@Override
public void updateUser(User user)
{
// TODO Auto-generated method stub
}
}
可能随着系统需求的变更,顾客信息需要转移到LDAP服务,或者转而使用其它的LDAP服务,又或者别人需要使用我们的Service,但是他们用的是另外的数据访问机制,这时就需要提供一个基于LDAP的数据访问对象,如下所示:
package com.google.spring.jdbc;
public class LdapUserDao implements IUserDao
{
@Override
public User findUserByPK(Integer id)
{
// TODO Auto-generated method stub
return null;
}
@Override
public void updateUser(User user)
{
// TODO Auto-generated method stub
}
}
即使具体的实现类发生了变化,客户端代码完全可以忽视这种变化,唯一需要变化的是factory中几行代码的改变,或者是IOC容器中几行简单的替换而已,所以DAO模式可以很好的屏蔽不同的数据访问的差异。
为了简化描述,上述省略了最基本的数据访问代码,当引入具体的数据访问代码的时候,问题就出现了。
package com.google.spring.jdbc;
import java.sql.Connection;
import javax.sql.DataSource;
public class JDBCUserDao implements IUserDao
{
private DataSource dataSource ;
public DataSource getDataSource()
{
return dataSource;
}
public void setDataSource(DataSource dataSource)
{
this.dataSource = dataSource;
}
@Override
public User findUserByPK(Integer id)
{
Connection conn = null;
try
{
conn = getDataSource().getConnection();
//....
User user = new User();
//........
return user;
}
catch (Exception e)
{
//是抛出异常,还是在当前位置处理。。。
}
finally
{
releaseConnection(conn);
}
return null;
}
@Override
public void updateUser(User user)
{
// TODO Auto-generated method stub
}
public void releaseConnection(Connection conn)
{
}
}
使用JDBC进行数据库访问,当其间出现问题的时候,JDBC API会抛出SQLException来表明问题的发生。而SQLException属于checked exception,所以,我们的DAO实现类要捕获这种异常并处理。
那如何处理DAO中捕获的SQLException呢,直接在DAO实现类处理掉?如果这样的话,客户端代码就无法得知在数据访问期间发生了什么变化?所以只好将SQLException抛给客户端,进而,DAO实现类的相应的签名
public User findUserByPK(Integer id) throws SQLException
相应的,DAO接口中的相应的方法签名也需要修改:
public User findUserByPK(Integer id) throws SQLException;
但是,这样并没有解决问题:
1、我们的数据访问接口对客户端应该是通用的,不管数据访问的机制发生了如何的变化,客户端代码都不应该受到牵连。但是,因为现在用的JDBC访问数据库,需要抛出特定的SQLException,这与数据访问对象模式的初衷是背离的。
2、当引入另外一种数据访问的模式的时候,比如,当加入LdapUserDao的时候,会抛出NamingException,如果要实现该接口,那么该方法签名又要发生改变,如下所示:
public User findUserByPK(Integer id) throws SQLException,NamingException;
这是很糟糕的解决方案,如果不同的数据访问的对象的实现越来越多,以及考虑到数据访问对象中的其它的数据访问的方法,这种糟糕的问题还得继续下去吗?
也就是说,因为数据访问的机制有所不同,我们的数据访问接口的定义现在变成了空中楼阁,我们无法最终确定这个接口!比如,有的数据库提供商采用SQLException的ErrorCode作为具体的错误信息标准,有的数据库提供商则通过SQLException的SqlState来返回相信的错误信息。即使将SQLException封装后抛给客户端对象,当客户端要了解具体的错误信息的时候,依然要根据数据库提供商的不同采取不同的信息提取方式,这种客户端处理起来将是非常的糟糕,我们应该向客户端对象屏蔽这种差异性。可以采用分类转译(Exception Translation)
a>首先,不应该将特定的数据访问异常的错误信息提取工作留给客户端对象,而是应该由DAO实现类,或者某个工具类进行统一的处理。假如我们让具体的DAO实现类来做这个工作,那么,对于JdbcUserDao来说,代码如下:
try
{
conn = getDataSource().getConnection();
//....
User user = new User();
Statement stmt = conn.createStatement();
stmt.execute("");
//........
return user;
}
catch (SQLException e)
{
//是抛出异常,还是在当前位置处理。。。
if(isMysqlVendor())
{
//按照mysql数据库的规则分析错误信息然后抛出
throw new RuntimeException(e);
}
if(isOracleVendor())
{
//按照oracle数据库的规则分析错误信息并抛出
throw new RuntimeException(e);
}
throw new RuntimeException(e);
}
b>信息提出出来了,可是,只通过RuntimeException一个异常类型,还不足以区分不同的错误类型,我们需要将数据访问期间发生的错误进行分类,然后为具体的错误分类分配一个对应的异常类型。比如,数据库连接不上、ldap服务器连接失败,他们被认为是资源获取失败;而主键冲突或者是其它的资源冲突,他们被认为是数据访问一致性冲突。针对这些情况,可以为RuntimeException为基准,为获取资源失败这种情况分配一个RuntimeException子类型,称其为ResourceFailerException,而数据一致性冲突对应另外一个子类型DataIntegrityViolationException,其它的分类异常可以加以类推,所以我们需要的只是一套unchecked exception类型的面向数据访问领域的异常层次类型。
不需要重新发明轮子
我们知道unchecked exception类型的面向数据访问领域的异常层次体系存在的必要性,不需我们设计,spring已经提供了异常访问体系。
spring框架中的异常层次体系所涉及的大部分的异常类型均定义在org.springframework.dao包中,处于这个异常体系中的异常类型均是以org.springframework.dao.DataAccessException为统领,然后根据职能划分为不同的子类型,总体上看,整个异常体系如下所示:
CleanupFailureDataAccessException:当成功完成数据访问要对资源进行清理的时候,将抛出该异常,比如使用jdbc进行数据库进行访问的时候,查询或者更新完成之后需要关闭相应的数据库连接,如果在关闭的过程中出现了SQLException,那么导致数据库连接没有被释放,导致资源清理失败。
DataAccessResourceFailureException:在无法访问相应的数据资源的情况下,将抛出DataAccessResourceFailureException。对应这种异常出现最常见的场景就是数据库服务器挂掉的情况,这时,连接数据库的应用程序通过捕获该异常需要了解到是数据库服务器出现了问题。对于JDBC来说,服务器挂掉会抛出该类型的子类型,即org.springframework.dao.CannotGetJdbcConnectionException。
DataSourceLookupFailureException:当尝试对jndi服务或者是其它位置上的DataSource进行查找的时候,可以抛出DataSourceLookupFailureException。
ConcurrencyFailureException:并发访问失败的时候,可以抛出ConcurrencyFailureException 比如无法取得相应的数据库的锁,或者乐观锁更新冲突。根据不同的并发数据访问失败的情况,ConcurrencyFailureException细分为所个子类:
OptimisticLockingFailureException对应数据更新的时候出现乐观锁冲突的情况。PessimisticLockingFailureException对应的是悲观锁冲突,PessimisticLockingFailureException还可以细分为CannotAcquireLockException和DeadlockLoserDataAccessException子类型。
InvalidDataAccessApiUsageException:该异常不是因为数据库资源出现了问题,而是我们以错误的方式,使用了特定的数据访问API,比如使用Spring的JdbcTemplate的queryForObject()语义上只返回一个结果对象,所以我们在查询多行的时候不能使用此方法。
InvalidDataAccessResourceUsageException:以错误的方式访问数据资源,会抛出该异常,比如要访问数据库资源,却传入错误的sql语句,分为不同的子类,基于JDBC的访问会抛出BadSqlGrammarException 基于hibernate的会抛出HibernateQueryException异常。
DataRetrievalFailureException:在要获取预期的数据却失败的时候。
PermissionDeniedDataAccessException:要访问相关的数据资源却没相应的权限的时候。
DataIntegrityViolationException:数据一致性冲突异常,比如主键冲突。
UncategorizedDataAccessException:无法细分的其它的异常,可以子类化定义具体的异常。