转自http://www.html.org.cn/books/springReference/ch09s05.html
大多数Spring用户选择声明式事务管理。这是对应用代码影响最小的选择,因此也最符合 非侵入式 轻量级容器的理念。
Spring的声明式事务管理是通过Spring AOP实现的,因为事务方面的代码与Spring绑定并以一种样板式风格使用, 不过尽管如此,你一般并不需要理解AOP概念就可以有效地使用Spirng的声明式事务管理。
从考虑EJB CMT和Spring声明式事务管理的相似以及不同之处出发是很有益的。它们的基本方法是相似的: 都可以指定事务管理到单独的方法;如果需要可以在事务上下文调用 setRollbackOnly()
方法。不同之处在于:
-
不像EJB CMT绑定在JTA上,Spring声明式事务管理可以在任何环境下使用。只需更改配置文件, 它就可以和JDBC、JDO、Hibernate或其他的事务机制一起工作。
-
Spring的声明式事务管理可以被应用到任何类(以及那个类的实例)上,不仅仅是像EJB那样的特殊类。
-
Spring提供了声明式的 回滚规则 :EJB没有对应的特性,我们将在下面讨论。回滚可以声明式的控制,不仅仅是编程式的。
-
Spring允许你通过AOP定制事务行为。例如,如果需要,你可以在事务回滚中插入定制的行为。 你也可以增加任意的通知,就象事务通知一样。使用EJB CMT,除了使用
setRollbackOnly()
,你没有办法能够影响容器的事务管理。 -
Spring不提供高端应用服务器提供的跨越远程调用的事务上下文传播。如果你需要这些特性,我们推荐你使用EJB。 然而,不要轻易使用这些特性。因为通常我们并不希望事务跨越远程调用。
回滚规则的概念比较重要:它使我们能够指定什么样的异常(和throwable)将导致自动回滚。 我们在配置文件中声明式地指定,无须在Java代码中。同时,我们仍旧可以通过调用 TransactionStatus
的 setRollbackOnly()
方法编程式地回滚当前事务。通常,我们定义一条规则, 声明 MyApplicationException
必须总是导致事务回滚。 这种方式带来了显著的好处,它使你的业务对象不必依赖于事务设施。典型的例子是你不必在代码中导入Spring API,事务等。
对EJB来说,默认的行为是EJB容器在遇到 系统异常 (通常指运行时异常)时自动回滚当前事务。 EJB CMT遇到 应用异常 (例如,除了 java.rmi.RemoteException
外别的checked exception)时并不会自动回滚。 默认式Spring处理声明式事务管理的规则遵守EJB习惯(只在遇到unchecked exceptions时自动回滚),但通常定制这条规则会更有用。
本节的目的是消除与使用声明式事务管理有关的神秘性。简单点儿总是好的,这份参考文档只是告诉你给你的类加上@Transactional
注解,在配置文件中添加('<tx:annotation-driven/>'
)行,然后期望你理解整个过程是怎么工作的。此节讲述Spring的声明式事务管理内部的工作机制,以帮助你在面对事务相关的问题时不至于误入迷途,回朔到上游平静的水域。
在理解Spring的声明式事务管理方面最重要的概念是:Spring的事务管理是通过AOP代理 实现的。 其中的事务通知由元数据 (目前基于XML或注解)驱动。 代理对象与事务元数据结合产生了一个AOP代理,它使用一个PlatformTransactionManager
实现品配合TransactionInterceptor
,在方法调用 前后实施事务。
注意
尽管使用Spring声明式事务管理不需要AOP(尤其是Spring AOP)的知识,但了解这些是很有帮助的。你可以在 第 6 章 使用Spring进行面向切面编程(AOP) 章找到关于Spring AOP的全部内容。
概念上来说,在事务代理上调用方法的工作过程看起来像这样:
请看下面的接口和它的实现。这个例子的意图是介绍概念,使用 Foo
和 Bar
这样的名字只是为了让你关注于事务的用法,而不是领域模型。
(对该例的目的来说,上例中实现类(DefaultFooService
)的每个方法在其方法体中抛出 UnsupportedOperationException
的做法是恰当的,我们可以看到,事务被创建出来, 响应 UnsupportedOperationException
的抛出,然后回滚。)
我们假定,FooService
的前两个方法(getFoo(String)
和getFoo(String, String)
)必须执行在只读事务上下文中,其他的方法(insertFoo(Foo)
和 updateFoo(Foo)
)必须执行在可读写事务上下文中。不要想着一次理解下面的配置,所有内容都会在后面的章节详细讨论。
我们来分析一下上面的配置。我们要把一个服务对象('fooService'
bean)做成事务性的。 我们想施加的事务语义封装在<tx:advice/>
定义中。<tx:advice/>
“把所有以 'get'
开头的方法看做执行在只读事务上下文中, 其余的方法执行在默认语义的事务上下文中 ”。 其中的 'transaction-manager'
属性被设置为一个指向 PlatformTransactionManager
bean的名字(这里指 'txManager'
), 该bean将会真正管理事务。
提示
事实上,如果 PlatformTransactionManager
bean的名字是 'transactionManager'
的话,你的事务通知(<tx:advice/>
)中的 'transaction-manager'
属性可以忽略。否则你则需要像上例那样明确指定。
配置中最后一段是 <aop:config/>
的定义, 它确保由 'txAdvice'
bean定义的事务通知在应用中合适的点被执行。 首先我们定义了 一个切面,它匹配 FooService
接口定义的所有操作, 我们把该切面叫做 'fooServiceOperation'
。然后我们用一个通知器(advisor)把这个切面与 'txAdvice'
绑定在一起, 表示当 'fooServiceOperation'
执行时,'txAdvice'
定义的通知逻辑将被执行。
<aop:pointcut/>
元素定义是AspectJ的切面表示法,可参考Spring 2.0 第 6 章 使用Spring进行面向切面编程(AOP) 章获得更详细的内容。
一个普遍性的需求是让整个服务层成为事务性的。满足该需求的最好方式是让切面表达式匹配服务层的所有操作方法。例如:
(这个例子中假定你所有的服务接口定义在 'x.y.service'
包中。你同样可以参考 第 6 章 使用Spring进行面向切面编程(AOP) 章获得更详细内容。)
现在,既然我们已经分析了整个配置,你可能会问了,“好吧,但是所有这些配置做了什么? ”。
上面的配置将为'fooService'
bean创建一个代理对象,这个代理对象被装配了事务通知,所以当它的相应方法被调用时,一个事务将被启动、挂起、被标记为只读,或者其它(根据该方法所配置的事务语义)。我们来看看下面的例子,测试一下上面的配置。
运行上面程序的输出结果看起来像这样(注意为了清楚起见,Log4J的消息和从 DefaultFooService
的 insertFoo(..)
方法抛出的 UnsupportedOperationException
异常堆栈信息被省略了)。
9.5.3. 回滚
在前面的章节里,概述了如何在你的应用中用声明的风格为类(特别是服务层的类)指定事务配置。 这一章将描述如何使用一个简单的声明式配置来控制事务的回滚。
我们推荐做法是在Spring框架的事务架构里指出当context的事务里的代码抛出 Exception
时事务进行回滚。Spring框架的事务基础架构代码将从调用的堆栈里捕获到任何未处理的 Exception
,并将标识事务将回滚。
然而,请注意Spring框架的事务基础架构代码将默认地 只 在抛出运行时和unchecked exceptions时才标识事务回滚。 也就是说,当抛出一个 RuntimeException
或其子类例的实例时。(Errors
也一样 - 默认地 - 标识事务回滚。)从事务方法中抛出的Checked exceptions将 不 被标识进行事务回滚。
可以配置哪些 Exception
类型将被标识进行事务回滚。 下面的XML配置片断里示范了如何配置一个用于回滚的checked、应用程序特定的 Exception
类型。
有时候你不 想在异常抛出的时候回滚事务,就可以使用“不回滚规则”。 在下面的例子中,我们告诉Spring 框架即使遇到没有经过处理的InstrumentNotFoundException
异常,也不要回滚事务。
当Spring框架捕获到一个异常后会检查配置回滚规则来决定是不是要回滚事务,这时候会遵循最匹配 的规则。 所以在下面这种配置中,除了InstrumentNotFoundException
这种类型的异常不会导致事务回滚以外,其他任何类型的异常都会。
第二种方法是通过 编程式 方式来指定回滚事务。 虽然写法非常的简单,但是这个方法是高侵入性的,并且使你的代码与Spring框架的事务架构高度耦合。 下面的代码片断里示范了Spring框架管理事务的编程式回滚:
强烈推荐你尽可能地使用声明式事务回滚方法。 编程式方法的回滚对你来说是可见,如果你需要它你就可以使用,但是使用它就直接违反了在你的应用中使用一个纯基于POJO的模型。
现在让我们考虑一下这样的场景,假设你有许多服务对象,你想为他们分别设置 完全不同 的事务语义。 在Spring中,你可以通过分别定义特定的 <aop:advisor/>
元素, 让每个advisor采用不同的 'pointcut'
和 'advice-ref'
属性,来达到目的。
让我们假定你所有的服务层类定义在以 'x.y.service'
为根的包内。 为了让service包(或子包)下所有名字以 'Service'
结尾的类的对象拥有默认的事务语义,你可以做如下的配置:
下面的配置示例演示了两个拥有完全不同的事务配置的bean。
9.5.5. <tx:advice/>
有关的设置
这一节里将描述通过 <tx:advice/>
标签来指定不同的事务性设置。默认的 <tx:advice/>
设置如下:
-
事务传播设置 是
REQUIRED
-
隔离级别是
DEFAULT
-
事务是 读/写
-
事务超时默认是依赖于事务系统的,或者事务超时没有被支持。
-
任何
RuntimeException
将触发事务回滚,但是任何 checkedException
将不触发事务回滚
这些默认的设置当然也是可以被改变的。 <tx:advice/>
和 <tx:attributes/>
标签里的 <tx:method/>
各种属性设置总结如下:
表 9.1. <tx:method/>
有关的设置
属性 | 是否需要? | 默认值 | 描述 |
---|---|---|---|
name | 是 | 与事务属性关联的方法名。通配符(*)可以用来指定一批关联到相同的事务属性的方法。 如: | |
propagation | 不 | REQUIRED | 事务传播行为 |
isolation | 不 | DEFAULT | 事务隔离级别 |
timeout | 不 | -1 | 事务超时的时间(以秒为单位) |
read-only | 不 | false | 事务是否只读? |
rollback-for | 不 | 将被触发进行回滚的 | |
no-rollback-for | 不 | 不 被触发进行回滚的 |
在写代码的时候,不可能对事务的名字有个很清晰的认识,这里的名字是指会在事务监视器(比如WebLogic的事务管理器)或者日志输出中显示的名字, 对于声明式的事务设置,事务名字总是包含完整包名的类名加上"."和方法名,比如 'com.foo.BusinessService.handlePayment'
.
注意
@Transactional
注解及其支持类所提供的功能最低要求使用Java 5(Tiger)。
除了基于XML文件的声明式事务配置外,你也可以采用基于注解式的事务配置方法。直接在Java源代码中声明事务语义的做法让事务声明和将受其影响的代码距离更近了,而且一般来说不会有不恰当的耦合的风险,因为,使用事务性的代码几乎总是被部署在事务环境中。
下面的例子很好地演示了 @Transactional
注解的易用性,随后解释其中的细节。先看看其中的类定义:
当上述的POJO定义在Spring IoC容器里时,上述bean实例仅仅通过一 行xml配置就可以使它具有事务性的。如下:
提示
实际上,如果你用 'transactionManager'
来定义 PlatformTransactionManager
bean的名字的话,你就可以忽略 <tx:annotation-driven/>
标签里的 'transaction-manager'
属性。 如果 PlatformTransactionManager
bean你要通过其它名称来注入的话,你必须用 'transaction-manager'
属性来指定它,如上所示。
@Transactional
注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。 然而,请注意只是使用 @Transactional
注解并不会启用事务行为, 它仅仅 是一种元数据 ,能够被可以识别 @Transactional
注解和上述的配置适当的具有事务行为的beans所使用。上面的例子中,其实正是 <tx:annotation-driven/>
元素的出现 开启 了事务行为。
Spring团队的建议是你只在具体的类上使用 @Transactional
注解, 而不要注解在接口上。你当然可以在接口(或接口方法)上使用 @Transactional
注解, 但是这只有在你使用基于接口的代理时它才会生效。因为注解是 不能继承 的, 这就意味着如果你正在使用基于类的代理时,事务的设置将不能被基于类的代理所识别,而且对象也不会被事务代理所包装 (这是很糟糕的 )。 因此,请接受Spring团队的建议,在具体的类(包括该类的方法)上使用 @Transactional
注解。
注意:在代理模式下(默认的情况),只有从代理传过来的‘外部’方法调用才会被拦截。 这就意味着‘自我调用’是不会触发事务的,比如说,一个在目标对象中调用目标对象其他方法的方法是不会触发一个事务的,即使这个方法被标记为 @Transactional
!
如果你期望‘自我调用’被事务覆盖到,可以考虑使用AspectJ 模式(如下所示)。在这种情况下,一开始就没有任何代理的存在; 为了把@Transactional
的方法变成运行时的行为,目标类会被‘编织’起来(比如修改它的字节码)。
表 9.2. <tx:annotation-driven/>
设置
属性 | 默认值 | 描述 |
---|---|---|
transaction-manager | transactionManager | 使用的事务管理器的名字。只有像在上面的例子那样,事务管理器不是 |
mode | proxy | 默认的模式“proxy”会用Spring的AOP框架来代理注解过的bean(就像在前面讨论过的那样, 下面代理的语义只对通过代理传递过来的方法调用起效)。 另一种可行的模式"aspectj"会使用Spring的AspectJ事务切面来编织类(通过修改目标对象的字节码应用到任何方法调用上)。 AspectJ织入需要在classpath中有spring-aspects.jar这个文件,并且启用装载时织入 (或者编译时织入)。 (关于如何设置装载时编织的详情请参见 第 6.8.4.5 节 “Spring配置” ) |
proxy-target-class | false | 只对代理模式有效。决定为那些使用了 |
order | Ordered.LOWEST_PRECEDENCE | 定义事务通知的顺序会作用到使用 |
注意
在<tx:annotation-driven/>
元素上的"proxy-target-class
" 属性 控制了有什么类型的事务性代理会为使用@Transactional
来注解的类创建代理。 如果"proxy-target-class
" 属性被设为"true
",那么基于类的代理就会被创建。 如果"proxy-target-class
" 属性被设为"false
" 或者没设,那么会创建基于接口的标准JDK代理。(关于不同代理类型的解释请参见 第 6.6 节 “代理机制” )
在多数情形下,方法的事务设置将被优先执行。在下列情况下,例如: DefaultFooService
类在类的级别上被注解为只读事务,但是,这个类中的 updateFoo(Foo)
方法的 @Transactional
注解的事务设置将优先于类级别注解的事务设置。
9.5.6.1. @Transactional
有关的设置
@Transactional
注解是用来指定接口、类或方法必须拥有事务语义的元数据。 如:“当一个方法开始调用时就开启一个新的只读事务,并停止掉任何现存的事务 ”。 默认的 @Transactional
设置如下:
-
事务传播设置是
PROPAGATION_REQUIRED
-
事务隔离级别是
ISOLATION_DEFAULT
-
事务是 读/写
-
事务超时默认是依赖于事务系统的,或者事务超时没有被支持。
-
任何
RuntimeException
将触发事务回滚,但是任何 checkedException
将不触发事务回滚
这些默认的设置当然也是可以被改变的。 @Transactional
注解的各种属性设置总结如下:
表 9.3. @Transactional
注解的属性
属性 | 类型 | 描述 |
---|---|---|
propagation | 枚举型:Propagation | 可选的传播性设置 |
isolation | 枚举型:Isolation | 可选的隔离性级别(默认值:ISOLATION_DEFAULT ) |
readOnly | 布尔型 | 读写型事务 vs. 只读型事务 |
timeout | int型(以秒为单位) | 事务超时 |
rollbackFor | 一组 Class 类的实例,必须是Throwable 的子类 | 一组异常类,遇到时 必须 进行回滚。默认情况下checked exceptions不进行回滚,仅unchecked exceptions(即RuntimeException 的子类)才进行事务回滚。 |
rollbackForClassname | 一组 Class 类的名字,必须是Throwable 的子类 | 一组异常类名,遇到时 必须 进行回滚 |
noRollbackFor | 一组 Class 类的实例,必须是Throwable 的子类 | 一组异常类,遇到时 必须不 回滚。 |
noRollbackForClassname | 一组 Class 类的名字,必须是Throwable 的子类 | 一组异常类,遇到时 必须不 回滚 |
在写代码的时候,不可能对事务的名字有个很清晰的认识,这里的名字是指会在事务监视器(比如WebLogic的事务管理器)或者日志输出中显示的名字, 对于声明式的事务设置,事务名字总是全限定名+"."+事务通知的类的方法名。比如BusinessService
类的handlePayment(..)
方法启动了一个事务,事务的名称是:
com.foo.BusinessService.handlePayment
请注意这部分的Spring参考文档不是 事务传播的介绍, 而是详细介绍了在Spring中与事务传播相关的一些语义。
在由Spring管理的事务中,请记住 物理 和 逻辑 事务存在的差异, 以及传播设置是如何影响到这些差异的。
当事务传播被设置PROPAGATION_REQUIRED
的时候, 会为每一个被应用到的方法创建一个逻辑 事务作用域。 每一个这样的逻辑事务作用域都可以自主地决定rollback-only状态,当这样的逻辑事务作用域被外部的一个逻辑事务作用域所包含的时候, 他们在逻辑上是独立的。当然了,对于正常的 PROPAGATION_REQUIRED
设置来说,他会被映射到相同的物理事务上。 所以一个标记有rollback-only的内部逻辑事务作用域的确会影响到外部的逻辑事务作用域(就像你所预料的那样)。
然而,当内部的事务作用域标记为rollback-only,同时外部的事务作用域并没有决定要回滚, 这样的回滚是意料不到的(静悄悄地由内部事务作用域触发的): 一个对应的UnexpectedRollbackException
异常会在这个时候被抛出。这是 可以预料到的行为 , 只有这样,这个事务的调用者才不会被误导,在事务没有提交的情况下误以为事务已经提交。所以如果内部的事务(外部的调用者并不知情)标记该事务为 rollback-only,而外部的调用者却依旧在不知情的情况下提交后,它需要收到一个 UnexpectedRollbackException
异常来清楚的了解事务并没有提交而是发生了回滚。
PROPAGATION_REQUIRES_NEW
,与之前相反,为每一个相关的事务作用域使用了一个完全 独立的事务。在这种情况下,物理事务也将是不同的,因此外部事务可以不受内部事务回滚状态的影响独立提交或者回滚。
考虑一下这样的情况,如果你希望 同时 执行事务性通知(advice)和 一些基本的剖析(profiling)通知。 那么,在<tx:annotation-driven/>
环境中该怎么做?
我们调用 updateFoo(Foo)
方法时希望这样:
-
配置的剖析切面(profiling aspect)开始启动,
-
然后进入事务通知(根据配置创建一个新事务或加入一个已经存在的事务),
-
然后执行原始对象的方法,
-
然后事务提交(我们假定这里一切正常),
-
最后剖析切面报告整个事务方法执行过程花了多少时间。
注意
这章不会专门讲述AOP的所有细节(除了应用于事务方面的之外)。 请参考 第 6 章 使用Spring进行面向切面编程(AOP) 章节以获得对各种AOP配置及其一般概念的详细叙述。
这里有一份简单的剖析切面(profiling aspect)的代码。 (请注意,通知的顺序是由 Ordered
接口来控制的。 要想了解更多细节,请参考 第 6.2.4.7 节 “通知顺序” 节。)
上面配置的结果将获得到一个拥有剖析和事务方面的 按那样的顺序 应用于它上面的 'fooService'
bean。 许多附加的方面的配置将一起达到这样的效果。
最后,下面的一些示例演示了使用纯XML声明的方法来达到上面一样的设置效果。
上面配置的结果是创建了一个 'fooService'
bean,剖析方面和事务方面被 依照顺序 施加其上。如果我们希望剖析通知在目标方法执行之前 后于 事务通知执行,而且在目标方法执行之后 先于 事务通知,我们可以简单地交换两个通知bean的order值。
如果配置中包含更多的方面,它们将以同样的方式受到影响。
通过AspectJ切面,你也可以在Spring容器之外使用Spring框架的 @Transactional
功能。要使用这项功能你必须先给相应的类和方法加上 @Transactional
注解,然后把 spring-aspects.jar
文件中定义的 org.springframework.transaction.aspectj.AnnotationTransactionAspect
切面连接进(织入)你的应用。同样,该切面必须配置一个事务管理器。你当然可以通过Spring框架容器来处理注入,但因为我们这里关注于在Spring容器之外运行应用,我们将向你展示如何通过手动书写代码来完成。
注意
在我们继续之前,你可能需要好好读一下前面的第 9.5.6 节 “使用 @Transactional
” 和 第 6 章 使用Spring进行面向切面编程(AOP) 两章。
注意
使用此切面(aspect),你必须在 实现 类(和/或类里的方法)、而 不是 类的任何所实现的接口上面进行注解。AspectJ遵循Java的接口上的注解 不被继承 的规则。
定义在类上的 @Transactional
注解指定了类中所有方法执行时的默认事务语义。
定义在类的方法上的 @Transactional
注解将覆盖掉类上注解的所指定的默认事务语义(如过存在的话)。 所有的方法都可以注解,不管它的可见度是什么样的。
要把 AnnotationTransactionAspect
织入你的应用,你或者基于AspectJ构建你的应用(参考 AspectJ Development Guide ),或者采取“载入时织入”(load-time weaving),参考 第 6.8.4 节 “在Spring应用中使用AspectJ加载时织入(LTW)” 获得关于使用AspectJ进行“载入时织入”的讨论。