详解阿里P7架构师是怎么在Spring中实现事务暂停
摘要
Spring框架是一个流行的基于轻量级控制反转容器的Java/J2EE应用框架,尤其在数据访问和事务管理方面的能力是众所周知的。Spring的声明性事务分离可以应用到任何POJO目标对象,并且包含所有EJB基于容器管理事务中的已声明事务。后台的事务管理器支持简单的基于JDBC的事务和全功能的基于JTA的J2EE事务。
这篇文章详细的讨论了Spring的事务管理特性。重点是如何在使用JTA作为后台事务策略的基础上让POJO利用Spring的声明性事务,这也显示了Spring的事务服务可以无缝地与J2EE服务器(如BEA WebLogic Server的事务协调器)的事务协调器进行交互,作为EJB CMT传统事务分离方式的一个替代者。
POJO的声明性事务
作为Spring声明性事务分离方式的样例,让我们来看一下Spring的样例应用PetClinic的中心服务外观中的配置:
清单1:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/petclinic</value> </property> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/> <bean id="clinicTarget" class="org.springframework.samples.petclinic.jdbc.JdbcClinic"> <property name="dataSource"><ref bean="dataSource"/></property> </bean> <bean id="clinic" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="target"><ref bean="clinicTarget"/></property> <property name="transactionAttributes"> <props> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop> </props> </property> </bean>
他遵循Spring的标准XMLBean定义格式。定义了:
- 一个DataSource引用,指向一个JNDI位置—在J2EE服务器管理下这将从JNDI环境中获取特定的DataSource。
- 一个应用服务实现—这是一个POJO,封装了业务和数据访问逻辑。在这里实现了应用中的Clinic服务接口。
- 一个应用服务的事务代理—这个代理为目标服务定义了事务属性,匹配特定的方法名模式并为之创建相应的事务。在实际的事务管理中,代理指向一个PlatformTransactionManager实现。
注意:除了显式的代理定义,Spring还支持自动代理机制和通过Commons Attributes或J2SE 5.0注解实现源程序级的元数据使用。这些可选方法的讨论超过了本文的范围。可以参考Spring的文档来了解相关细节。
业务接口和业务实现是特定于应用的并且不需要关心Spring或者Spring的事务管理。普通Java对象可以作为服务的目标对象,而且任何普通Java接口可以作为服务的接口。下面是一个Clinic接口的示例:
清单2:
public interface Clinic { Pet loadPet(int id); void storePet(Pet pet); ... }
这个接口的实现如下显示,假设他使用JDBC来执行必要的数据访问。他通过bean属性的设置方法来获取JDBC的DataSource;这与上面的配置中的dataSource属性定义相对应。
清单3:
public class JdbcClinic implements Clinic { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Pet loadPet(int id) { try { Connection con = this.dataSource.getConnection(); ... } catch (SQLException ex) { ... } } public void storePet(Pet pet) { try { Connection con = this.dataSource.getConnection(); ... } catch (SQLException ex) { ... } } ... }
如你所见,代码相当直接。我们使用一个简单的Java对象,而事务管理由事务代理来处理,这个我们会在下面讨论。
注意在PetClinic示例应用中实际的基于JDBC的Clinic实现利用了Spring的JDBC支持类来避免直接使用JDBC的API。虽然Spring的事务管理也可以与普通的基于JDBC实现一起工作,就向上面的示例。
定义事务代理
除了JdbcClinic实例以外,配置中也定义了一个事务代理。如果愿意这个代理所暴露的实际接口也可以显式定义。默认情况下,所有由目标对象实现的接口都暴露出来,在这个例子中就是应用的Clinic服务接口。
从客户端的观点来看,"clinic" bean只是这个应用的Clinic接口的实现。客户端不需要知道这会被一个事务代理所处理。这就是接口的能力:一个直接的目标对象的引用可以容易的被一个实现相同接口的代理所代替—在这儿就是一个隐式创建事务的代理。
代理的具体事务行为会由为根据特定的方法或方法命名模式而定义的事务属性来驱动,就像下面的例子所示:
清单3:
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop>
Key属性决定代理将为方法提供什么样的事务行为。这个属性的最重要部分就是事务传播行为。下面是一些可选的属性值:
- PROPAGATION_REQUIRED --支持当前的事务,如果不存在就创建一个新的。这是最常用的选择。
- PROPAGATION_SUPPORTS --支持当前的事务,如果不存在就不使用事务。
- PROPAGATION_MANDATORY --支持当前的事务,如果不存在就抛出异常。
- PROPAGATION_REQUIRES_NEW --创建一个新的事务,并暂停当前的事务(如果存在)。
- PROPAGATION_NOT_SUPPORTED --不使用事务,并暂停当前的事务(如果存在)。
- PROPAGATION_NEVER --不使用事务,如果当前存在事务就抛出异常。
- PROPAGATION_NESTED --如果当前存在事务就作为嵌入事务执行,否则与PROPAGATION_REQUIRED类似。
前6个事务策略与EJB的CMT类似,而且使用相同的常量名,因此对EJB开发人员来说是很亲切的。第7个策略PROPAGATION_NESTED是Spring提供的一个变体:他需要事务管理器(如DataSourceTransactionManager)提供类似JDBC3.0那样的保存点API来嵌套事务行为或者通过
JTA支持嵌套事务。
事务属性中的readOnly标识指示相应的事务应该作为一个只读事务来优化。这是一个优化提示:一些事务策略在这种情况下可以得到很好的性能优化,如使用ORM工具如Hibernate或TopLink时避免脏数据检查(“flush”尝试)。
在事务属性中还有一个“timeout”选项来定义事务的超时秒数。在JTA中,这个属性会简单地传递给J2EE服务器的事务协调器并被正确地解释。
使用事务代理
在运行时,客户端会取得一个“clinic”引用并转换为Clinic接口,然后调用如loadPet或storePet方法。这就隐式地使用了Spring的事务代理,通过“事务解释器”在目标对象中注册;这样一个新的事务就创建了,然后具体的工作就会代理给JdbcClinic的目标方法。
图1示例了一个使用“建议链”并到达最后目标的AOP代理的潜在概念。在这个示例中,唯一的建议是一个事务解释器用来包装目标方法的事务行为。这是一种用来在声明性事务功能下使用的基于代理的AOP。
Figure 1. An AOP proxy with an advisor chain and a target at the end
例如,一个PetClinic应用的WEB层组件可以执行ServletContext定位来获取Spring WebApplicationContext的引用并且获取受管理的“clinic”BEAN:
清单4:
WebApplicationContext ctx = WebApplicationContexUtils.getWebApplicationContext(servletContext); Clinic clinic = (Clinic) ctx.getBean("clinic); Pet pet = new Pet(); pet.setName("my new cat"); clinic.storePet(pet);
在调用storePet()之前,Spring的事务代理隐式地创建一个事务。当storePet()调用返回时,事务将提交或回滚。缺省情况下任何RuntimeException或Error将导致回滚。实际的提交或回滚可以是可以定义的:Spring的事务属性支持“回滚规则”的概念。
例如,我们可以可以引入一个强制的PetClinicException并且告诉事务代理在抛出异常时回滚:
清单5:
<prop key="load*">PROPAGATION_REQUIRED,readOnly,-PetClinicException</prop> <prop key="store*">PROPAGATION_REQUIRED,-PetClinicException</prop>
这儿也有一个类似的“提交规则”语法,指示特定的异常将触发一次提交。
注意上面示例的显式定位引用的方法只是一种访问受Spring管理BEAN的方法的变化,可以用在任何WEB资源如servlet或filter。在构建基于Spring自身的MVC框架时,BEAN可以直接被注射到WEB控制器中。当然也支持在如Struts, WebWork, JSF, and Tapestry框架中访问Spring管理BEAN。详情可以参考Spring的文档。
PlatformTransactionManager策略
Spring事务支持的核心接口是org.springframework.transaction.PlatformTransactionManager。所有Spring的事务分离功能都会委托给PlatformTransactionManager(传给相应的TransactionDefinition实例)来做实际的事务执行。虽然PlatformTransactionManager接口可以直接调用,但通常应用只需要配置一个具体的事务管理器并且通过声明性事务来分离事务。
Spring提供几种不同的PlatformTransactionManager实现,分为如下两个类别:
- 本地事务策略—支持单一资源的事务(通常是单个数据库),其包括
org.springframework.jdbc.datasource.DataSourceTransactionManager
和org.springframework.orm.hibernate.HibernateTransactionManager
。 - 全局事务管理—支持可能跨越多个资源的全局事务。其相应的类为org.springframework.transaction.jta.JtaTransactionManager,将事务委托给遵循JTA规范的事务协调器(通常为J2EE服务器,但不是强制的)。
PlatformTransactionManager抽象的主要价值在于应用不再被绑定在特定的事务管理环境。相反,事务策略可以很容易地切换—通过选择不同的PlatformTransactionManager实现类。这就使得应用代码与声明事务分离保持一致,而不需要考虑应用组件所使用的环境了。
例如,应用的初始版本可能布署在Tomcat上,与单个Oracle数据库交互。这可以方便地利用Spring的事务分离特性,只要选择基于JDBC的DataSourceTransactionManager作为使用的事务策略。Spring会分离事务,而JDBC驱动会执行相应的原始JDBC事务。
相同应用的另一个版本可能会布署在WebLogic服务器上,使用两个Oracle数据库。应用代码和事务分离不需要改变。唯一不同的是选择作为JtaTransactionManager事务策略,让Spring来分离事务而WebLogic服务器的事务协调器来执行事务。
JTA UserTransaction与JTA TransactionManager比较
让我们来看一下Spring对JTA支持的细节。虽然并非经常需要考虑这个细节但了解相关的细节还有必要的。对简单的用例如前面章节的示例,标准的JtaTransactionManager定义已经足够了,缺省的Spring JtaTransactionManager设置会从标准JNDI位置(J2EE规范所定义的java:comp/UserTransaction)获取JTA的javax.transaction.UserTransaction对象。这对大部分标准J2EE环境来说已经足够了。
然而,缺省的JtaTransactionManager不能执行事务暂停(也就是说不支持PROPAGATION_REQUIRES_NEW和PROPAGATION_NOT_SUPPORTED)。原因就在于标准的JTA UserTransaction接口不支持事务的暂停和恢复,而只支持开始和完成新的事务。
为了实现事务的暂停,需要一个javax.transaction.TransactionManager实例,他提供了JTA定义的标准的暂停和恢复方法。不幸的是,J2EE没有为JTA TransactionManager定义标准的JNDI位置!因此,我们需要使用厂商自己的定位机制。
清单6:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManagerName"> <value>vendorSpecificJndiLocation</value> </property> </bean>
J2EE本质上没有考虑将JTA TransactionManager接口作为公共API的一部分。JTA规范自身定义了将TransactionManager接口作为容器集成的想法。虽然这是可以理解的,但是JTA TransactionManager的标准JNDI位置还是可以增加一定的价值,特别是对轻量级容器如Spring,这样任何J2EE服务器就可以用统一的方式来定位JTA TransactionManager了。
不仅Spring的JtaTransactionManager可以从访问中获益,O/R映射工具如Hibernate, Apache OJB, and Kodo JDO也能得到好处,因为他们需要在JTA环境中执行缓存同步的能力(释放缓存意味着JTA事务的完成)。这种注册事务同步的能力只有JTA TransactionManager接口才能提供,而UserTransaction是处理不了的。因此,这些工具都需要实现自己的TransactionManager定位器。
为JTA TransactionManager定义标准的JNDI位置是许多底层软件供应商最期望J2EE实现的功能。如果J2EE5.0的规范制定团队能够认识到这个特性的重要性就太好了。幸运地是,高级J2EE服务器如WebLogic Server已经考虑将JTA TransactionManager作为公共的API包含在扩展功能中。
在WebLogic JTA中实现Spring的事务分离
在WebLogic Server中,JTA TransactionManager官方的JNDI位置定义为javax.transaction.TransactionManager。这个值可以在Spring的JtaTransactionManager中作为“transactionManagerName”使用。原则上这样就可以在WebLogic's JTA系统中实现事务暂停了,也就是说支持PROPAGATION_REQUIRES_NEW
和PROPAGATION_NOT_SUPPORTED
行为。
除了标准的JtaTransactionManager和其支持的通用配置选项外,Spring还提供了一个专用的WebLogicJtaTransactionManager适配器来直接利用WebLogic的JTA扩展。
在享受自动探测WebLogic的JTA TransactionManager的便利之外,他提供超越标准JTA的三个重要特性:
- 事务命名—暴露出Spring的事务名给WebLogic Server,使得Spring事务在WebLogic的事务监听器可见。缺省的,Spring会使用声明性事务的完整方法名。
- 每事务隔离级别—将Spring事务属性中定义的隔离级别应用到WebLogic JTA事务中。这使得每个事务都可以定义数据库的隔离级别,而这是标准JTA所不支持的。
- 强制事务恢复—即使在暂停的事务被标识为回滚时也可以恢复。这需要使用WebLogic的扩展TransactionManager接口来调用forceResume()方法。
Figure 2. WebLogic Server's transaction monitor (click the image for a full-size screen shot)
Spring的WebLogicJtaTransactionManager有效地为基于Spring的应用提供了WebLogic Server事务管理的全部功能。这使得Spring事务分离成为一种能与EJB CMT竟争的产品,而且提供了相同级别的事务支持。
Spring and EJB CMT
如上所示,Spring的POJO声明性事务分离可以作为一种除传统EJB CMT这外的选择。但是Spring与EJB并不是完成互斥的,Spring的应用上下文也可以作为EJB façade的后台来管理数据访问(DAO)和其他细纹理的业务对象。
在EJB情景中,事务是由EJB CMT来驱动的。对Spring来说,数据访问支持特性会自动检测到这样的环境并且采用相应的事务。例如,Spring对Hibernate的支持能够提供隐式的资源管理,即使是EJB驱动的事务,甚至可以在不需要修改任何DAO代码的情况下提供相同的语义。
Spring有效的解耦了DAO实现与实际的运行环境。DAO可以参与Spring的事务就像参与EJB CMT事务一样。这不仅简化在其他环境中的重用,而且更方便在J2EE容器外进行测试。
结论
Spring框架为J2EE和非J2EE环境提供了全量的事务分离的特性,特别表现在POJO的声明性事务上。他用一种灵活而非侵入式的方式为非EJB环境中的事务分离提供了便利。与EJB不同,这样的事务性POJO应用对象可以很容易的被测试和在J2EE容器外补重用。
Spring提供了各种事务策略,如JtaTransactionManager是用来代理J2EE服务器的事务协调器,而JDBC DataSourceTransactionManager是用来为简单的JDBC DataSource(就是单一目标数据库)执行事务。Spring可以很容易为不同的环境通过后台配置的简单修改来调整事务策略。
超越标准的JTA支持,Spring为WebLogic Server的JTA扩展提供了完善的集成,可以支持高级特性如事务监视和每事务隔离级别。通过对WebLogic Server的特殊支持,基于Spring的应用可以完全利用WebLogic Server的事务管理功能。
Spring事务分离是继EJB CMT之外的另一种可选方式,特别是对那些基于POJO的轻量级架构。在那只是因为选择LSSB(本地无状态会话BEAN)来应用声明性事务的情况下,基于Spring的POJO服务模型是一种可行的选择,他提供了非常高层的灵活性、可测试性和重用性。
【本人秃顶程序员】:专注于Java开发技术的研究与知识分享!
————END————
- 点赞
- ...
- 转发
- ...
- 关注
- ...
最后,欢迎做Java的工程师朋友们加入Java高级架构进阶Qqun:963944895
群内有技术大咖指点难题,还提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
比你优秀的对手在学习,你的仇人在磨刀,你的闺蜜在减肥,隔壁老王在练腰, 我们必须不断学习,否则我们将被学习者超越!
趁年轻,使劲拼,给未来的自己一个交代!