概述
近几年持久化技术 领 域异常喧嚣,各种框架雨后春笋般地冒出,Sun也连接不断地颁布几个持久化规范。Spring对多个持久化技术提供了集成的支持,包括 Hibernate、iBatis、JDO、JPA、TopLink,此外,还通过Spring JDBC框架对JDBC API进行简化。Spring面向DAO制定了一个通用的异常体系,屏蔽具体持久化技术的异常,使业务层和具体的持久化技术达到解耦。此外,Spring 提供了模板类简化各种持久化技术的使用。通用的异常体系及模板类是Spring整合各种五花八门持久化技术的不二法门,Spring不但借此实现了对多种 持久化技术的整合,还可以不费吹灰之力整合潜在的各种持久化框架,体现了“开-闭原则”的经典应用 。 ; t; u w6 B2 U/ d
Spring的DAO理念
DAO(Data Access Object)是用于访问数据 的对象,虽然我们在大多数情况下,将数据保存在数据库中,但这并不是唯一的选择,你也可以将数据存储到文件中或LDAP中。DAO不但屏蔽了数据存储的最终介质的不同,也屏蔽了具体的实现技术的不同。 2 A0 o) G: b7 a' u
早期,JDBC是访问数据库的主流选择,近几年,数据持久技术获得了长足的发展,Hibernate、iBatis、JPA、JDO成为持久层中争放异彩 的实现技术。只要为数据访问定义好DAO接口,并使用具体的技术实现DAO接口的功能,你就可以在不同的实现技术间平滑的切换。
6 S. p& /3 X {+ E
图 1 业务层通过DAO接口访问数据
图 1是一个典型的DAO应用实例,在UserDao中定义访问User数据对象的接口方法,业务层通过UserDao操作数据,并使用具体持久技术实现UserDao接口方法,这样业务层和具体持久化技术就实现了解耦。 8 w2 i9 F' `( K1 q, X# a
提供DAO层的抽象可以带来一些好处,首先,我们可以很容易地构造模拟对象,方便单元测试的开展,其次在使用切面时,我们有更多的选择:既可以使用JDK动态代理也可以使用CGLib动态代理。
Spring本质上希望以统一的方式整合底层的持久化技术:以统一的方式进行调用及事务管理,避免让具体的实现侵入到业务层的代码 中。由于每个持久化实现技术都有各自的异常体系,所以Spring提供了统一的异常体系,使不同异常体系的阻抗得以弥消,方便定义出和具体实现技术无关的DAO接口,以及整合到相同的事务管理体系中。 , d6 n" t" y- C0 w( y. J
: c U5 V: k5 U- h% F- b; H
统一的异常体系 , I7 U9 ]- C! W+ N3 F2 m
统一的异常体系是整合不同的持久化实现技术的关键,Spring提供了一套和实现技术无关的、面向于DAO层次语义的异常体系,并通过转换器将不同的持久化技术异常转换成Spring的异常。
Spring的DAO异常体系 5 O) k- B" |: f
在很多正统API或框架中,检查型异常被过多的使用,以至在使用API时,代码里充斥着大量的try/catch样板式的代码。在很多情况下,除了在 try/catch中记录异常信息以外,我们并没有做多少实质性的工作。引发异常的问题往往是不可恢复的,如数据连接失败,SQL语句存在语法错误,强制 捕捉的检查型异常除了限制开发人员的自由度以外,并没有提供什么有意义的作用。因此,Spring的异常体系都是建立在运行期异常的基础上,开发者可以根 据需要捕捉感兴趣的异常。 / `2 W& u; ?. a9 e
JDK很多API之所以难用,一个很大的原因就是检查型异常的泛滥,如JavaMail、EJB以及JDBC等等,使用这些API,一堆堆异常处理的代码喧宾夺主式地侵入业务代码中,破坏了代码的整洁和优雅。
. o9 G; F" v! e: X
Spring在org.springframework.dao包中提供了一套完备优雅的DAO异常体系,这些异常都继承于 DataAccessException,而DataAccessException本身又继承于 NestedRuntimeException,NestedRuntimeException异常以嵌套的方式封装了源异常。因为虽然不同持久化技术的 特定异常被转换到Spring的DAO异常体系中,原始的异常信息并不会丢失,只要你愿意,就可以方便地通过getCause()方法获取原始的异常信 息。
3 /0 B1 Z+ T$ V
Spring的DAO异常体系并不和具体的实现技术相关,它从DAO概念的抽象层面定义了异常的目录树。在所有的持久化框架中,我们并没有发现拥有如此丰 富语义异常体系的框架,Spring这种设计无疑是独具匠心的,它使得开发人员关注某一特定语义的异常变得容易。在JDBC中的SQLException 中,你必须通过异常的getErrorCode()或getSQLState()获取错误代码,直接根据这些代码判断是错误的类型,这种过于底层的API 不但带来了代码编写上的难度,而且也使代码的移植变得困难,因为getErrorCode()是数据库相关的。
u5 K% i7 b1 B4 y( x
Spring以分类手法建立了异常分类目录,对于大部分应用来说,这个异常分类目录对异常类型的划分具有适当的颗粒度。一方面,使开发者从底层细如针麻的 技术细节中脱身出来,另一方面,可以从这个语义丰富的异常体系中选择感兴趣的异常加以处理。图 2列出了那些位于Spring DAO异常体系第一层次的异常类,每个异常类下还可能拥有众多的子异常类:
图 2 Spring DAO异常体系 ' P, `7 g, T: q1 l2 `5 c, u
Spring DAO异常体系类非常丰富,这里,我们仅列出DataAccessException异常类下的子类。我们可以很容易地通过异常类的名字了解到异常所代表的语义。我们通过下表对这些异常进行简单的描述:表 1 Spring DAO异常体系类
异常 8 U5 q3 F" K1 `. j, n + K" ]5 K& [. z9 C6 p7 } |
说明 ; B& t: L+ H) i& T# k; i# ? 6 w0 t& l" ]3 f, b* _; v* I( h, V( H3 ? S |
CleanupFailureDataAccessException " S& d( j2 f$ n2 j7 z& ^6 T" Q 7 T5 e, O; n+ l F. {3 k1 C1 ?# t |
DAO 操作成功执行,但在释放数据资源时发生异常,如关闭 Connection 时发生异常等。 # V1 g! {7 D2 n) T% f0 z; U |
ConcurrencyFailureException 5 I: |* U8 z0 P3 I# _9 G1 A0 ? |
表示在进行并发数据操作时发生异常,如乐观锁无法获取、悲观锁无法获取、死锁引发的失败等待异常。 ' K( u, {$ U: J4 L |
DataAccessResourceFailureException 5 M) |6 y8 O0 R$ B2 k- z6 Z $ X; R( ^! E0 P" X |
访问数据资源时失败,如无法获取数据连接,无法获取 Hibernate 的会话等。 ' W% F/ Y) I, e1 q3 B( z, b 2 O; [6 C% a) ^; b/ } |
DataRetrievalFailureException # }6 K$ h, o# r N2 x) X 8 ~: D' j2 k0 o9 e& ^4 Y . ]/ x C4 d C |
获取数据失败,如找不到对应主键的数据,使用了错误的列索引等。 7 k& i/ {7 V$ J( I 9 b# G0 P2 b9 d/ V |
DataSourceLookupFailureException " h" Z% ]* Z2 W; }) G : E+ H5 W6 v6 V* d) Z# w0 V 5 g+ Y4 H! D9 n* q: N) P5 U! @ |
无法从 JNDI 中查找到数据源。这个异常是 Spring 2.0 新增的。 + p) E3 g: H2 {8 t) } |
DataIntegrityViolationException 5 y% N; M7 T* l- [+ ^2 / , P2 /1 _+ s8 C: v9 f |
当数据操作违反了数据一致性限制时抛出的异常,如插入重复的主键,引用不存在的外键等。 ; U& d3 O- n: ~& N # r# `$ J2 V( h , V+ `* i) j; g5 ^& m6 K! G; j |
InvalidDataAccessApiUsageException 0 j/ v9 x: t" w! W$ g, C [( E0 ]5 T 8 Z$ K, H) B* E! G y - E7 S2 v4 I# g& ` |
不正确地调用某一持久化技术时抛出的异常,如在 Spring JDBC 中查询对象在调用前必须进行编译操作,如果忘记这项操作将会产生该异常。这种异常不是由底层数据资源产生,而是由不正确地使用持久化技术产生的。 |
InvalidDataAccessResourceUsage6 r, `! p7 I A0 U0 c Exception # D6 c1 q- U' ^7 U' r# ~0 z" h3 _' h6 {) M |
在访问数据源时使用了不正确的方法所抛出的异常,如 SQL 语句错误将抛出该异常。 ( l6 a% j' {1 g; Z9 Y7 u, r . i& O4 W) w9 C' t7 n m* }* a: f2 T ) o _% S: {8 g |
PermissionDeniedDataAccessException |
数据访问时由于权限不足引发的异常。如用仅拥有只读权限用户试图进行数据更改操作将抛出该异常。该异常是 Spring 2.0 新增的。 ! k* L1 c& ]! _% Z# K0 j" j 0 E+ ]3 Y9 ?3 ?: M) P4 p% N& W' ? |
UncategorizedDataAccessException ; Q/ q; h4 _' ^/ T+ m |
其它未分类的异常都归到该异常中。 " s+ d( u9 u# i1 j |
为了进一步细化错误的问题域,Spring对一级异常类进行子类的细分,如InvalidDataAccessResourceUsageException就拥有10多个子类,下面是其中3个子类: ) h `9 I# N5 ~0 r* H
对于InvalidDataAccessResourceUsageException异常,不同的持久化实现技术均有对应的子异常类。 如 BadSqlGrammarException对应JDBC实现技术SQL语句语法错误的异常,而HibernateQueryExcpetion 和TopLinkQueryException分别对应Hibernate和TopLink实现技术的查询语法异常。
Spring的这个异常体系具有高度的可扩展性,当Spring需要对一个新的持久化技术提供支持时,只要定义为其定义一个对应的子异常就可以了,这种更改完全满足设计模式中的开-闭原则。 1 ]3 L3 m+ }# W
虽然Spring定义了如此丰富的异常类,作为开发人员,我们仅需要对感兴趣的异常进行处理就可以了。假设某一个项目要求在发生乐观锁异常时,尝试再次获 取乐观锁非不是直接返回错误。那么,我们只需要在代码中显式捕捉ConcurrencyFailureException异常,然后在catch代码块中 编写满足需求的逻辑即可。其它众多的异常则可以简单地交由框架自动处理(如发生运行期异常时自动回滚事务)。
JDBC的异常转换器
传统的JDBC API在发生几乎所有的数据操作问题都抛出相同的SQLException,它将异常的细节性信息封装在异常属性中,所以如果希望了解异常的具体原因,你必须分析异常对象的信息。
SQLException拥有两个代表异常具体原因的属性:错误码和SQL状态码,前者是数据库相关的,可通过getErrorCode()返回,其值的 类型是int;而后者是一个标准的错误代码,可通过getSQLState()返回,是一个String类型的值,由5字符组成。
Spring根据错误码和SQL状态码信息将SQLExeption翻译成Spring DAO的异常体系。在org.springframework.jdbc.support包中定义了SQLExceptionTranslator接口, 该接口的两个实现类SQLErrorCodeSQLExceptionTranslator和 SQLStateSQLExceptionTranslator分别负责处理SQLException中错误代码和SQL状态码的翻译工作。将 SQLException翻译成Spring DAO异常体系的工作是比较艰辛的,但Spring框架替我们完成这项艰巨的工作并保证转换的正确性,我们有充分的理由依赖这个转换的正确性。
' Q" C$ }8 s# @; i
其它持久技术的异常转换器 6 L$ s0 h5 E; N% |% g$ {9 E( }
由于各种框架级的持久化技术都拥有一个语义明确的异常体系,所以将这些异常转换为Spring DAO的体系相对轻松一些。下面,我们将对不同持久化技术的异常转换器进行学习。 # W5 p. `! u+ ?. E$ h& W2 A a
; F! p2 j' B3 A6 {3 u5 s2 h
由于Hibernate 3.0版本和低版本不兼容,Spring分别为这两个版本分别提供了支持(这也可以看出Hibernate在Spring中所享受的特殊礼遇)。在 org.springframework.orm包中,分别为Spring所支持的ORM持久化技术定义了一个子包,在这些子包中提供相应ORM技术的整 合类。Spring为各个ORM持久化技术所提供的异常转换器在表 2中说明: 4 Z/ T W1 K5 K+ V0 Z8 J R3 }
表 2 各ORM持久化技术异常转换器
ORM 持久化技术 3 ^ n3 p# /$ V! ~ |
异常转换器 + I+ m% f9 {6 e* M# W8 |& I4 D3 h |
Hibernate |
org.springframework.orm.hibernate.SessionFactoryUtils % `0 c0 K3 x. x3 S: W% O! B |
Hibernate 3.0 ' @% v. A/ L+ v* p |
org.springframework.orm.hibernate3.SessionFactoryUtils 1 j9 O" ]3 j6 a, o b |
JPA ) /7 {* y& ]$ g! u" e( l |
org.springframework.orm.jpa.EntityManagerFactoryUtils $ g% s/ j0 C$ Q4 `# k |
JDO # V( a8 J" v$ j, ~( ]# i" P& M# F K# n5 Q 0 w) q. I8 Q3 d. x |
org.springframework.orm.jdo.PersistenceManagerFactoryUtils , i% C9 I! P9 K6 m, _5 {7 q |
TopLink - M* Y c* b1 c* a |
org.springframework.orm.toplink.SessionFactoryUtils 8 a7 n; @: t+ f2 m- r1 [- F / M) /1 n3 x! z( Q$ i+ y1 d8 B3 r |
0 n; H! Z; H6 |! S
ORM持久化技术 异常转换器 0 T W1 X# c6 n
Hibernate org.springframework.orm.hibernate.SessionFactoryUtils 6 v. x# W$ R! n
Hibernate 3.0 org.springframework.orm.hibernate3.SessionFactoryUtils
JPA org.springframework.orm.jpa.EntityManagerFactoryUtils
JDO org.springframework.orm.jdo.PersistenceManagerFactoryUtils
TopLink org.springframework.orm.toplink.SessionFactoryUtils
这些工具类除了具有异常转换的功能外,在进行事务管理时,还提供了从事务上下文中返回相同会话的功能。
Spring也支持iBatis ORM持久化技术,由于iBatis抛出的异常是和JDBC相同的SQLException异常,所以直接采用和JDBC相同的异常转换器。
统一数据访问模板 * {4 X. C7 g, H9 u$ @
到一个餐馆用餐,大抵都会经历这个的一个流程,进入餐馆->迎宾小姐问候并引到适合的位置->抄起菜单点菜>用餐->买单 ->离开餐馆。之所以我们喜欢时不时下下馆子,就是因为我们只要点菜->用餐->买单就可以了,幕后的烹饪制作、刷锅洗盘等工作我们完 全不用关心,一切已经由餐馆服务人员按照服务流程按部就班,有条不紊地执行了。衡量一个餐馆服务质量好坏的一个重要标准是我们无须关心他们所负责流程:不 用催问菜为什么还没有上好(不但快而且服务态度佳),不用关心盘子为什么不干净(不但干净而且已经进行了消毒)。
从某种角度看,与其说餐馆为我们提供了服务,还不如说我们参与到餐馆的流程中:不管什么顾客点的菜都由相同的橱师烹制,不管什么顾客都按单付钱。在幕后, 餐馆拥有一个服务的模板,模板中定义的流程可以用于应付所有的顾客,只要为顾客提供几个专有需求(点的菜也可不一样,座位可以自由选择),其它一切都按模 板化的方式处理。
在直接使用具体的持久化技术时,我们大多需要处理整个流程,并没有享受餐馆用餐式的便捷。Spring为支持的持久化技术分别提供了模板访问的方式,降低了使用各种持久化技术的难度,可以大幅提高开发效率。
使用模板和回调机制
$ Q; u h: U6 R2 H4 h
下面是一段使用JDBC进行数据访问操作的简单代码,我们已经尽可能简化整个过程的处理了,但以下的步骤几乎都是不可或缺的。 % L* Y3 A u) W' z) L
代码清单 1 JDBC数据访问
public void saveCustomer(Customer customer) throws Exception ...{; [( N! `, H" c- b N; l9 f6 b4 A8 U
- }6 K& n: G. |+ y
Connection con=null;
PreparedStatement stmt=null;
try ...{
con=getConnection();① 获取资源
con.setAutoCommit(false); ② 启动事务 B1 [ N1 I+ /9 }& {! W$ @
6 {0 R, Q% t/ t: s! w
③ 具体数据访问操作和处理& h* P1 ~- a! m4 o$ z
stmt=con.prepareStatement("insert into CUSTOMERS(ID,NAME) values(?,?)");
stmt.setLong(1,customerId);
stmt.setString(2,customer.getName());
stmt.execute();
…3 q: p6 `( k A Q5 L( x
stmt.execute();$ p! s U. W+ T" K9 s7 A/ @
6 c+ a3 g3 U* j" {. _& o
con.commit();④提交事务) I5 b4 n( d; ^/ t
}catch(Exception e)...{ ③
⑤ 回滚事务, g4 b2 |% ]0 N; A8 x2 k
try...{4 D3 v% l i7 /% Q" J5 L4 H
con.rollback();
}catch(SQLException sqlex)...{) T, m W9 [# j8 C5 q& f! G
sqlex.printStackTrace(System.out);! y7 B3 T6 /6 T
}2 f D" |0 k) t9 B& h, f
throw e;
}finally...{3 g2 w- C# M$ `/ R' U i7 ]4 B$ a$ w
⑥ 释放资源
try...{5 T3 E" O+ l# ], ~7 |$ C6 h4 L
stmt.close();0 R$ w$ A1 v* G2 J% }; F* E
con.close();
}catch(Exception e)...{3 e, U% S1 t0 o& Z' l
e.printStackTrace(); I4 Y2 D2 e; x0 Y5 f+ J
}
}
}
如以上数据访问代码所示,JDBC数据访问操作按以下的流程进行: % r) w ~; j5 w, ^" c# j* Q
1. 准备资源;
2. 启动事务; 3 J. }8 u( x7 i3 H# y
3. 在事务中执行具体数据访问操作; 3 Z% l B g5 f& |. _
4. 返回数据;
5. 提交/回滚事务;
6. 关闭资源,处理异常。
按照传统的方式,编写任何带事务的数据访问的程序时,你都需要重复编写上面的代码,而其中只有粗体部分所示的代码是业务相关的,而其它的代码都是在做一些例行公事,因而导致了大量八股文式的代码充斥着整个程序。 : ^7 s! n) G7 }5 r% v' v
Spring将这个相同的数据访问流程固化到模板类中,并将数据访问中固定和变化的部分分开,同时保证模板类是线程安全,以便多个数据访问线程共享同一模 板实例。固定的部分在模板类中已经准备好,而变化的部分通过回调接口开放出来,用于定义具 体数据访问和结果返回的操作。图 4描述了模板类是如何拆分固定和变化部分的逻辑: * v' ?# N7 `, m7 u1 j4 y
8 a; k0 P0 A S2 e
图 4 Spring DAO模板和回调
这样,我们只要编写好回调接口,并调用模板类进行数据访问,就可以得到预想的结果:数据访问成功执行,前置和后置的样板化工作也得到顺序的执行,在提高开发效率的同时保证了资源使用的正确性,彻底消除因忽视资源释放而引起的资源泄漏的问题。
Spring为不同持久化技术所提供的模板类 $ D( y9 M/ a) x# J9 J, M$ k
Spring为各种支持的持久化技术都提供了简化操作的模板和回调,在回调中编写具体的数据操作逻辑,使用模板执行数据操 作,在Spring中,这是典型的数据操作模式。下面,我们来了解一下Spring为不同的持久化技术所提供的模板类。
表 3 不同持久化技术对应的模板类
ORM持久化技术 模板类
JDBC org.springframework.jdbc.core. JdbcTemplate % s+ W% J$ S' j( O# d& c" x
Hibernate org.springframework.orm.hibernate.HibernateTemplate
Hibernate 3.0 org.springframework.orm.hibernate3.HibernateTemplate
iBatis org.springframework.orm.ibatis.SqlMapClientTemplate / |: G& F6 z3 `/ O/ P- j# M
JPA org.springframework.orm.jpa.JpaTemplate
JDO org.springframework.orm.jdo.JdoTemplate ) l% r1 B- R5 t& _; x$ b
TopLink org.springframework.orm.jpa. JpaTemplate ! [ P$ m4 l, /) u
如果你使用JDK 5.0或以上版本,则可以使用jdbc.core.simple.SimpleJdbcTemplate的模板类,该模板类使用了泛型和不定入数的技术,使模板的功能更加强大,更易于使用。
如果,我们直接使用模板类,一般都需要在DAO中定义一个模板对象并提供数据资源,Spring为每一个持久化技术都提供了支持类,支持类中已经为我们完成这样的功能。这样,我们只需要扩展这些支持类就可以直接编写实际的数据访问逻辑,没有须臾的阻隔。 . l }2 k* j( X9 M- u
不同持久化技术的支持类如表 4所示:
ORM 持久化技术 ) D5 @6 M; i1 N4 v0 e |
支持类 |
JDBC 9 O; Y" u0 h) O) R X |
org.springframework.jdbc.core. JdbcDaoSupport |
Hibernate - z8 ~0 ~ O7 Q8 s |
org.springframework.orm.hibernate.HibernateDaoSupport |
Hibernate 3.0 9 E1 D+ p8 M% y5 p4 g |
org.springframework.orm.hibernate3.HibernateDaoSupport " {4 p1 S0 ?! k" P; f) ]4 Z |
iBatis + N3 d. ?. z w) _- ^ |
org.springframework.orm.ibatis.SqlMapClientDaoSupport |
JPA . M! }6 A" I) v% ]5 h( V |
org.springframework.orm.jpa.JpaDaoSupport . A; `. ]3 ~. i |
JDO |
org.springframework.orm.jdo.JdoDaoSupport |
TopLink |
org.springframework.orm.jpa. JpaDaoSupport , w" L3 t: X% Q4 D p, Q |
表 4 持久化技术的支持类
ORM持久化技术 支持类 1 g/ b, x2 I8 ]2 P
JDBC org.springframework.jdbc.core. JdbcDaoSupport
Hibernate org.springframework.orm.hibernate.HibernateDaoSupport ! |& p( @7 R0 Z: k' h1 B& m- }
Hibernate 3.0 org.springframework.orm.hibernate3.HibernateDaoSupport 1 e5 J+ `% D4 O$ L- q" c
iBatis org.springframework.orm.ibatis.SqlMapClientDaoSupport
JPA org.springframework.orm.jpa.JpaDaoSupport " I& r0 G* n/ k
JDO org.springframework.orm.jdo.JdoDaoSupport 3 [. N' s1 @" E% H7 C2 |* ^
TopLink org.springframework.orm.jpa. JpaDaoSupport 2 p. q- V9 }' L5 L& s* c8 O
这些支持类都继承于dao.support.DaoSupport类,DaoSupport实现了InitializingBean接口,在afterPropertiesSet()接口方法中检查模板对象和数据源是否被正确设置,否则将抛出异常。
所有的支持类都是abstract的,其目的是希望被继承使用,而非直接使用。 * v/ ^; y# _' H' V! D
小结
Spring 支持目前大多数常用的数据持久化技术,Spring定义了一套面向DAO层的异常体系,并为各种支持的持久化技术提供了异常转换器。这样,我们在设计DAO接口时,就可以抛开具体的实现技术,定义统一的接口。
不管采用何种持久化技术,访问数据的流 程是相对固定的。Spring将数据访问流程划分为固定和变化两部分,并以模板的方式定义好流程,用回调接口将变化的部分开放出来,留给开发者自行定义。 这样,我们仅需要提供业务相关的逻辑就可以完成整体的数据访问了。Spring为了进一步简化持久化模板类的调整,为各种持久化技术提供了使用模板的支持 类,支持类不但包含数据访问模板,还包含数据源或会话。通过扩展支持类定义自己的数据访问类是最简单的数据访问方式。 ' N& F# `& t7 V ?
; Q( _+ }% Q7 X5 a