5.Spring的事务
通常情况下,J2EE有2种事务管理方式:全局事务和本地事务,2种事务都比较明显的缺陷。
全局事务:
全局事务允许跨多个事务资源的事务管理(通常是关系数据库和消息队列),应用服务器通过JTA(一个很复杂的api)管理全局事务,此外,一个JTA的事务通常通过JNDI进行资源查找,即如果你想使用JTA就必须连带使用JNDI。JTA通常只能在应用服务器环境下使用,显然使用全局事务会限制应用代码的重用性。
更好的方式是通过EJB CMT提供全局事务管理,CMT是一种声明式的事务管理。EJB CMT移除了事务相关的JNDI查找,虽然使用EJB本身就需要使用JNDI,但这确实节省了大量的事务管理代码(并不是全部),重大缺陷是CMT绑定在JTA以及应用服务器环境上,并且你必须选择EJB来处理业务逻辑。EJB太庞大,并不是一个很吸引人的选择(仅仅是为了增加全局事务就使用EJB,确实....)。
本地事务:
本地事务则和底层所使用的持久化技术有关(使用JDBC处理持久化,事务管理需要JDBC中的connection对象,使用hibernate处理持久化,事务管理需要hibernate中的session对象),比起全局事务,本地事务更易使用,但是也有明显的缺陷:1.不能跨事务资源,例如:使用JDBC事务来进行事务管理的代码在JTA全局事务环境下就不能运行。2.使用本地事务,由于应用服务器不需要参与事务的管理,因此不能保证跨多个事务性资源的事务正确性(不需要特别注意这种情况,大多数应用使用单个事务资源)。3.显然事务管理和代码是耦合的,具有侵入性(这也是前面缺陷1的产生原因)。
Spring事务管理:
Spring解决了全局事务和本地事务的缺陷,允许应用开发者在任何环境下使用一致的编程模型。Spring同时支持编程式事务管理和声明式事务管理。当使用Spring编程式事务管理时,开发者直接使用Spring框架的事务抽象(事务抽象这个翻译有点拗口,原文为:transaction abstraction,不同具体事务策略的统一抽象接口,面向接口编程),使应用程序可以运行在任何具体的底层事务基础之上。当使用Spring声明式事务管理时,只需编写少量和事务相关的代码即可(只需编写一些Spring配置文件),这些代码和Spring的事务api或任何其他的事务api都没有耦合。显然Spring的声明式事务管理更加简单易用。
为了使用事务管理是否应该选择应用服务器?
答案是否定的。
Spring对事务管理的支持改变了企业级Java应用必须要使用应用服务器的传统观念。需要特别指出的是,仅仅是为了使用EJB的声明式事务管理就选择使用应用服务器是完全没有必要的(为了使用声明式事务管理就使用EJB同样可怕),尽管应用服务器提供了强劲的JTA功能,Spring的声明式事务管理比起EJB CMT更加给力,并且提供更加高效的编程方式。通常选择使用应用服务器是为了跨多个事务资源的事务管理,这种情况很少见并且许多高端应用会选择使用高性能数据库而不是多个事务资源。也可以选择一些独立的事务管理技术,但是大多数情况下这些技术都需要一些应用服务器功能(比如JMS或是JCA)的支持。Spring更符合一次编写,随处运行的原则,代价仅仅是修改一些Spring配置文件,而不是大量的重复性编码工作。
Spring支持的事务策略:
Spring事务策略是通过PlatformTransactionManager接口体现的,该接口是Spring事务策略的核心。
即使使用容器管理的JTA,代码依然不需要JNDI查找,无需与特定的JTA资源耦合在一起,通过配置文件,JTA资源传给PlatformTransactionManager的实现类,因此,程序的代码可以在JTA事务管理和非JTA事务管理之间轻松的切换。
Spring是否支持事务跨多个数据库资源?
答:Spring完全支持这种跨多个事务性资源的全局事务,前提是底层的应用服务器(如WebLogic、WebSphere、tomcat等)支持JTA全局事务,可以这样说:Spring本身没有任何事务支持,它只是负责包装底层的事务————当我们在程序中面向PlatformTransactionManager接口编程时,Spring在底层负责将这些操作转换成具体的事务操作代码,所以应用底层支持什么样的事务策略,那么Spring就支持什么样的事务策略。Spring事务管理的优势是将应用从具体的事务API中分离出来,而不是真正提供事务管理的底层实现。
Spring的具体的事务管理由PlatformTransactionManager的不同实现类来完成,在Spring容器中配置PlatformTransactionManager时必须针对不同的环境提供不同的实现类,下面提供了不同的持久化访问环境,及其对应的PlatformTransactionManager实现类的配置。
1,BC数据源的局部事务策略的配置文件如下:
<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定连接数据库的驱动 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定连接数据库的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定连接数据库的用户名 -->
<property name="user" value="root"/>
<!-- 指定连接数据库的密码 -->
<property name="password" value="32147"/>
<!-- 指定连接数据库连接池的最大连接数 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定连接数据库连接池的最小连接数 -->
<property name="minPoolSize" value="1"/>
<!-- 指定连接数据库连接池的初始化连接数 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定连接数据库连接池的连接的最大空闲时间 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 配置JDBC数据源的局部事务管理器,使用DataSourceTransactionManager 类 -->
<!-- 该类实现PlatformTransactionManager接口,是针对采用数据源连接的特定实现-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 配置DataSourceTransactionManager时需要依注入DataSource的引用 -->
<property name="dataSource" ref="dataSource"/>
</bean>
2.器管理JTA全局事务的配置文件如下:
<!-- 配置JNDI数据源Bean-->
<bean id="dateSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<!--容器管理数据源的JNDI-->
<property name="jndiName" value="jdbc/jpetstore"/>
</bean>
<!--使用JtaTransactionManager类,该类实现PlatformTransactionManager接口-->
<!--针对采用全局事务管理的特定实现-->
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
3.采用Hibernate持久层访问策略时,局部事务的策略的配置文件如下:
!-- 定义数据源Bean,使用C3P0数据源实现 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定连接数据库的驱动 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定连接数据库的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定连接数据库的用户名 -->
<property name="user" value="root"/>
<!-- 指定连接数据库的密码 -->
<property name="password" value="32147"/>
<!-- 指定连接数据库连接池的最大连接数 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定连接数据库连接池的最小连接数 -->
<property name="minPoolSize" value="1"/>
<!-- 指定连接数据库连接池的初始化连接数 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定连接数据库连接池的连接的最大空闲时间 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 定义Hibernate的SessionFactory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 依赖注入数据源,注入正是上面定义的dataSource -->
<property name="dataSource" ref="dataSource"/>
<!-- mappingResouces属性用来列出全部映射文件 -->
<property name="mappingResources">
<list>
<!-- 以下用来列出Hibernate映射文件 -->
<value>Person.hbm.xml</value>
</list>
</property>
<!-- 定义Hibernate的SessionFactory的属性 -->
<property name="hibernateProperties">
<props>
<!-- 指定数据库方言 -->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQLInnoDBDialect</prop>
<!-- 是否根据需要每次自动创建数据库 -->
<prop key="hibernate.hbm2ddl.auto">update</prop>
<!-- 显示Hibernate持久化操作所生成的SQL -->
<prop key="hibernate.show_sql">true</prop>
<!-- 将SQL脚本进行格式化后再输出 -->
<prop key="hibernate.format_sql">true</prop>
</props>
</property>
</bean>
<!--配置Hibernate局部事务管理器,使用HibernateTransactionManager类-->
<!--该类是PlatformTransactionManager接口针对采用Hibernate的特定实现类-->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
4.底层采用Hibernate持久化技术,但事务依然采用JTA全局事务:
<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定连接数据库的驱动 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定连接数据库的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定连接数据库的用户名 -->
<property name="user" value="root"/>
<!-- 指定连接数据库的密码 -->
<property name="password" value="32147"/>
<!-- 指定连接数据库连接池的最大连接数 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定连接数据库连接池的最小连接数 -->
<property name="minPoolSize" value="1"/>
<!-- 指定连接数据库连接池的初始化连接数 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定连接数据库连接池的连接的最大空闲时间 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 定义Hibernate的SessionFactory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 依赖注入数据源,注入正是上面定义的dataSource -->
<property name="dataSource" ref="dataSource"/>
<!-- mappingResouces属性用来列出全部映射文件 -->
<property name="mappingResources">
<list>
<!-- 以下用来列出Hibernate映射文件 -->
<value>Person.hbm.xml</value>
</list>
</property>
<!-- 定义Hibernate的SessionFactory的属性 -->
<property name="hibernateProperties">
<props>
<!-- 指定数据库方言 -->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQLInnoDBDialect</prop>
<!-- 是否根据需要每次自动创建数据库 -->
<prop key="hibernate.hbm2ddl.auto">update</prop>
<!-- 显示Hibernate持久化操作所生成的SQL -->
<prop key="hibernate.show_sql">true</prop>
<!-- 将SQL脚本进行格式化后再输出 -->
<prop key="hibernate.format_sql">true</prop>
</props>
</property>
</bean>
<!--使用JtaTransactionManager类,该类实现PlatformTransactionManager接口-->
<!--针对采用全局事务管理的特定实现-->
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
从上面的配置文件可以看出,当使用Spring事务管理事务策略时,应用程序无需与具体的事务策略耦合,Spring提供了两种事务管理方式:
编程式管理方式:即使使用Spring编程式事务时,程序也可直接获取容器中的TransactionManagerBean,该Bean总是PlatformTransactionManager的实例,可以通过该接口提供的3个方法来开始事务,提交事务和回滚事务。
声明式管理方式:无需在Java程序中书写任何的事务操作代码,而是通过在XML文件中为业务组件配置事务代理(AOP代理的一种),AOP事务代理所织入的增强处理也是由Spring提供:在目标方法前,织入开始事务;在目标方法后执行,织入结束事务。
注意:Spring编程事务还可以通过TransactionTemplate类来完成,该类提供了一个execute方法,可以更简洁的方式来进行事务操作。
当使用声明式事务时,开发者无需书写任何事务管理代码,不依赖Spring或或任何事务API,Spring的声明式事务编程无需任何额外的容器支持,Spring容器本身管理声明式事务,使用声明式事务策略,可以让开发者更好的专注于业务逻辑的实现。
使用TransactionProxyFactoryBean创建事务代理
在Spring1.X,声明事务使用TransactionProxyFactoryBean来配置事务代理Bean,正如他的名字所暗示的,它是一个工厂Bean,该工程Bean专为目标Bean生成事务代理Bean,既然TransactionProxyFactoryBean产生的是事务代理Bean,可见Spring的声明式事务策略是基于Spring AOP的。
例如:
<beans>
<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定连接数据库的驱动 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定连接数据库的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定连接数据库的用户名 -->
<property name="user" value="root"/>
<!-- 指定连接数据库的密码 -->
<property name="password" value="32147"/>
<!-- 指定连接数据库连接池的最大连接数 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定连接数据库连接池的最小连接数 -->
<property name="minPoolSize" value="1"/>
<!-- 指定连接数据库连接池的初始化连接数 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定连接数据库连接池的连接的最大空闲时间 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 配置JDBC数据源的局部事务管理器,使用DataSourceTransactionManager 类 -->
<!-- 该类实现PlatformTransactionManager接口,是针对采用数据源连接的特定实现-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 配置DataSourceTransactionManager时需要依注入DataSource的引用 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置一个业务逻辑Bean -->
<bean id="test" class="lee.TestImpl">
<property name="ds" ref="dataSource"/>
</bean>
<!-- 为业务逻辑Bean配置事务代理 -->
<bean id="testTrans" class=
"org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 为事务代理工厂Bean注入事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<property name="target" ref="test"/>
<!-- 指定事务属性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
public class TestImpl implements Test
{
private DataSource ds;
public void setDs(DataSource ds)
{
this.ds = ds;
}
public void insert(String u)
{
JdbcTemplate jt = new JdbcTemplate(ds);
jt.execute("insert into mytable values('" + u + "')");
//两次插入相同的数据,将违反主键约束
jt.execute("insert into mytable values('" + u + "')");
//如果增加事务控制,我们发现第一条记录也插不进去。
//如果没有事务控制,则第一条记录可以被插入
}
}
配置事务代理的时。需要传入一个事务管理器,一个目标Bena,并指定该事务的事务属性,事务实行由transactionAttribute指定,上面只有一条事务传播规则,该规则指定对于所有方法都使用PROPAGATION_REQUIRED的传播属性,Spring支持的事务传播规则:
PROPAGATION_MANDATORY:要求调用该方法的线程已处于事务环境中,否则抛出异常;
PROPAGATION_NESTED:如果执行该方法的线程已处于事务的环境下,依然启动新的事务,方法在嵌套的事务里执行,如果执行该方法的线程并未处于事务的环境下,也启动新的事务,此时与PROPAGATION_REQUIRED;
PROPAGATION_NOT_SUPPORTED:如果调用该方法的线程处于事务中,则暂停事务,再执行方法。
PROPAGATION_NEVER:不允许执行该方法的线程处于事务的环境下,如果执行该方法的线程处于事务的环境下,则会抛出异常;
PROPAGATION_REQUIRED:要求在事务环境中执行该方法,如果当前执行线程已处于事务中,则直接调用;否则,则启动新的事务后执行该方法。
PROPAGATION_REQUIREDS_NEW:该方法要求在新的事务中执行,如果当前执行线程已处于事务环境下,则暂停当前事务,启动新事务后执行方法;否则,启动新的事务后执行方法。
PROPAGATION_SUPPORTS:如果当前线程处于事务中,则使用当前事务,否则不使用事务。
主程序代码:
public class MainTest
{
public static void main(String[] args)
{
//创建Spring容器
ApplicationContext ctx = new
ClassPathXmlApplicationContext("bean.xml");
//获取事务代理Bean
Test t = (Test)ctx.getBean("testTrans");----------①
//执行插入操作
t.insert("bbb");
}
}
上面①处代码获取testTrans Bean,该Bean已不再是TestImpl的实例了,它只是Spring容器创建的事务代理,该事务代理以TestImpl实例为目标对象,且该目标对象也实现了Test接口,故代理对象也可当作Test实例使用。
实际上,Spring不仅支持对接口的代理,整合GBLIB后,Spring甚至可以对具体类生成代理,只要设置proxyTargetClass属性为true就可以了,如果目标Bean没有实现任何接口,proxyTargetClass默认为true,此时Spring会对具体的类生成代理。当然通常建议面向接口编程,而不面向具体的实现类编程。
Spring2.X的事务配置策略
上面介绍的TransactionProxyFactoryBean配置策略简单易懂,但是配置起来极为繁琐:每个目标Bean都需要额外配置一个TransactionProxyFactoryBean代理,这种方法将导致配置文件的急剧增加。
Spring2.X的XML Schema方式提供了更简洁事务配置策略,Spring提供了tx命名空间来配置事务管理,tx命名空间下提供了<tx:advice../>元素来配置事务切面,一旦使用了该元素配置了切面,就可以直接使用<aop:advisor.../>元素启动自动代理。
如:
<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定连接数据库的驱动 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定连接数据库的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定连接数据库的用户名 -->
<property name="user" value="root"/>
<!-- 指定连接数据库的密码 -->
<property name="password" value="32147"/>
<!-- 指定连接数据库连接池的最大连接数 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定连接数据库连接池的最小连接数 -->
<property name="minPoolSize" value="1"/>
<!-- 指定连接数据库连接池的初始化连接数 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定连接数据库连接池的连接的最大空闲时间 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 配置JDBC数据源的局部事务管理器,使用DataSourceTransactionManager 类 -->
<!-- 该类实现PlatformTransactionManager接口,是针对采用数据源连接的特定实现-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 配置DataSourceTransactionManager时需要依注入DataSource的引用 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置一个业务逻辑Bean -->
<bean id="test" class="lee.TestImpl">
<property name="ds" ref="dataSource"/>
</bean>
<!-- 配置事务切面Bean,指定事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 用于配置详细的事务语义 -->
<tx:attributes>
<!-- 所有以'get'开头的方法是read-only的 -->
<tx:method name="get*" read-only="true"/>
<!-- 其他方法使用默认的事务设置 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 配置一个切入点,匹配lee包下所有以Impl结尾的类
执行的所有方法 -->
<aop:pointcut id="leeService"
expression="execution(* lee.*Impl.*(..))"/>
<!-- 指定在txAdvice切入点应用txAdvice事务切面 -->
<aop:advisor advice-ref="txAdvice"
pointcut-ref="leeService"/>
</aop:config>
</beans>
提示:
Advisor的作用:将Advice和切入点绑在一起,保证Advice所包含的增强处理在对应的切入点被织入。
主程序:
public class MainTest
{
public static void main(String[] args)
{
//创建Spring容器
ApplicationContext ctx = new
ClassPathXmlApplicationContext("bean.xml");
//获取事务代理Bean
Test t = (Test)ctx.getBean("test");
//执行插入操作
t.insert("bbb");
}
}
上面的配置文件直接获取容器中的test Bean,因为Spring AOP会为该Bean自动织入事务增强处理的方式,所以test Bean的所有方法都具有事务性。
配置<tx:advice../>元素时除了需要一个transaction-Manager之外,重要的就是需要配置一个<attributes.../>子元素,该元素又可以包含多个<method../>子元素。
可以看出配置<tx:advice../>元素重点就是配置<method.../>子元素,每个<method../>子元素为一批方法指定所需的事务语义。
配置<method../>元素时可以指定如下属性:
name:必选属性,与该事务语义关联的方法名,该属性支持通配符。
propagation:指定事务传播行为,该属性可以为Propagation枚举类的任意一个枚举值,该属性 的默认值是Propagation.REQUIRED;
isolation:指定事务隔离级别,该属性值可为Isolation枚举类的任意枚举值。
timeout:指定事务超时时间(以秒为单位),指定-1意味不超时,默认值是-1;
read-only:指定事务是否只读,该属性默认为false;
rollback-for:指定触发事务回滚的异常类(全名),该属性可以指定多个异常类,用英文逗号隔开;
no-rollback-for:指定不触发事务回滚的类,该属性可以指定多个异常类,并且用英文的逗号隔开。
因此我们可以为不同的业务逻辑方法指定不同的事务策略,如:
<tx:advice id="aaa">
<tx:attributes>
<tx:method name="get*" read--only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<tx:advice id="bbb">
<tx:attributes>
<tx:method name="*" propagation="NEVER"/>
</tx:attributes>
</tx:advice>