文章40 | 阅读 19324 | 点赞0
事务是逻辑上的一组操作,组成这组操作的各个逻辑单元,要么一起成功,要么一起失败。
如果如果不考虑事务的隔离性,那么就会引发如下安全性问题。
我们可以通过设置隔离级别来解决上述读的问题。
常见数据库的默认级别:
Spring进行事务管理的时候,主要使用一个PlatformTransactionManager接口,它表示事务管理器,是Spring中真正管理事务的对象。Spring针对不同的持久化框架,提供了不同PlatformTransactionManager接口的实现类,如下:
TransactionDefinition也是一个接口,它用于定义事务的相关的信息,如隔离级别、超时信息、传播行为以及(设置)是否只读等。
TrancactionStatus也是一个接口,用于记录在事务管理过程中事务的状态。
Spring中的这组事务管理的API是如何进行事务管理的呢?Spring在进行事务管理的时候,平台事务管理器根据事务定义的信息来进行事务的管理,在事务管理过程中,事务就会产生各种状态,于是Spring会将这些状态的信息记录到事务状态的这个对象当中。
Spring中事务的传播行为很有可能会让人很困惑,因为它还是比较难理解的,有可能你粗略过了一遍Spring,到最后也压根不是很了解它。为了让初学者能够理解起来更简单,我尽量通俗易懂地进行讲解。
在真实的项目开发中,你必然会遇到特别复杂的业务逻辑,那就是出现业务层之间的方法相互调用的情况。这种情况下,必然就会出现一些问题,如下图所示。
从上图可知,之所以要引出Spring事务的传播行为,其实就是为了要解决我们上面遇到的问题。Spring事务的传播行为主要是用来解决什么问题的呢?这里,直接给出答案。Spring事务的传播行为主要是用来解决特别复杂的业务逻辑之间的方法相互调用的问题,也就是说Spring事务的传播行为主要用来解决业务层方法相互调用的问题。
Spring中提供了七种事务的传播行为,这七种事务的传播行又为可分为三类。
我们先从整体上认识这七种事务的传播行为,要有一个全局观念,然后接下来我会一个一个详细地去介绍它们,力求通俗易懂。
如果没有设置Spring事务的传播行为的话,那么这是它的默认值。它到底指的是什么意思呢?我用通俗易懂的话来说吧!
如果设置Spring事务的传播行为为它的话,那么可以分为以下两种情况来讨论。
如果设置Spring事务的传播行为为它的话,那么可以分为以下两种情况来讨论。
如果设置Spring事务的传播行为为它的话,那么可以分为以下两种情况来讨论。
如果设置Spring事务的传播行为为它的话,那么x方法中有事务,这时在y方法中调用了x方法,在y方法中就会将x方法的事务挂起(暂停),并且不使用事务管理。
如果设置Spring事务的传播行为为它的话,那么x方法中有事务,直接就会报异常。
如果我们将Spring事务的传播行为设置为它的话,那么会发生什么事情呢?如果x方法中有事务,那么在y方法中执行到x方法时,就会按照x方法中的事务来执行,并且执行完成后,会设置一个保存点(相当于你玩游戏,存个档)。接着,执行y方法中的操作,如果没有异常,就执行通过,如果有异常,这时你可以选择回滚到最初始位置,也可以选择回滚到保存点(存档的位置)。
下面,我会通过一个转账的案例来演示Spring中的事务管理,这个例子用在这儿,很合适。
首先创建一个动态web项目,例如spring_demo03_tx,然后导入Spring框架相关依赖jar包,要导入哪些jar包呢?
其中,spring-tx-4.2.4.RELEASE.jar就是Spring用于事务管理时使用到的jar包。
创建数据库和表的sql语句如下所示,为了方便测试,可以在数据库表中自己添加几条记录。
create database spring4_demo03_tx;
use spring4_demo03_tx;
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
INSERT INTO `account` VALUES ('1', '秦始皇', '10000');
INSERT INTO `account` VALUES ('2', '刘邦', '10000');
INSERT INTO `account` VALUES ('3', '项羽', '10000');
INSERT INTO `account` VALUES ('4', '扶苏', '10000');
在src目录下新建一个com.meimeixia.tx.domain包,并在该包下编写一个Account类。
package com.meimeixia.tx.domain;
public class Account {
private Integer id;
private String name;
private Double money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account [id=" + id + ", name=" + name + ", money=" + money + "]";
}
}
首先,在src目录下新建一个com.meimeixia.tx.demo01包,并在该包下编写一个名为AccountService的接口。
package com.meimeixia.tx.demo01;
/** * 转账的业务层的接口 * @author liayun * */
public interface AccountService {
//转账的方法
public void transfer(String from, String to, Double money);
}
然后,再在com.meimeixia.tx.demo01包下编写以上接口的一个实现类(AccountServiceImpl.java)。
package com.meimeixia.tx.demo01;
/** * 转账的业务层的实现类 * @author liayun * */
public class AccountServiceImpl implements AccountService {
//注入AccountDao
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
/* * from:转出账号 * to:转入账号 * money:转账金额 */
@Override
public void transfer(String from, String to, Double money) {
accountDao.outMoney(from, money);
accountDao.inMoney(to, money);
}
}
首先,在com.meimeixia.tx.demo01包下编写一个名为AccountDao的接口。
package com.meimeixia.tx.demo01;
/** * 转账的Dao的接口 * @author liayun * */
public interface AccountDao {
public void outMoney(String from, Double money);
public void inMoney(String to, Double money);
}
然后,在com.meimeixia.tx.demo01包下编写以上接口的实现类,先将该实现类写成下面这样。
package com.meimeixia.tx.demo01;
/** * 转账的Dao的实现类 * @author liayun * */
public class AccountDaoImpl implements AccountDao {
//扣钱
@Override
public void outMoney(String from, Double money) {
//待会再来写扣钱的具体实现代码...
}
//加钱
@Override
public void inMoney(String to, Double money) {
//待会再来写加钱的具体实现代码...
}
}
首先,咱得引入Spring的配置文件,一开始tx.xml文件的内容肯定是空的,只不过包含了各种schema约束,下面我给出的tx.xml文件包含的schema约束应该是最全面的。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>
然后,我们就可以在Spring配置文件中配置service层和dao层中的类了(主要配置的是实现类),即在Spring配置文件中添加如下配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 配置Service和Dao~~~~~~~~~~~~~~~~~~ -->
<bean id="accountService" class="com.meimeixia.tx.demo01.AccountServiceImpl">
<property name="accountDao" ref="accountDao" />
</bean>
<!-- 配置Dao~~~~~~~~~~~~~~~~~~ -->
<bean id="accountDao" class="com.meimeixia.tx.demo01.AccountDaoImpl">
</bean>
</beans>
最后,还要记得在src目录下引入Log4j的配置文件(log4j.properties)哟!也就是日志记录文件,该文件内容如下:
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.err
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=c\:mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
# error warn info debug trace
log4j.rootLogger= info, stdout
可以想到的是咱必然会在AccountDaoImpl实现类中使用到JdbcTemplate模板类,因为还要用它来实现底层向数据库表中进行增删改查的方法。而JdbcTemplate模板类对象的创建必须要依赖于连接池,所以,最好的办法就是将连接池和JdbcTemplate模板类都交给Spring来管理。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 配置Service和Dao~~~~~~~~~~~~~~~~~~ -->
<bean id="accountService" class="com.meimeixia.tx.demo01.AccountServiceImpl">
<property name="accountDao" ref="accountDao" />
</bean>
<!-- 配置Dao~~~~~~~~~~~~~~~~~~ -->
<bean id="accountDao" class="com.meimeixia.tx.demo01.AccountDaoImpl">
</bean>
<!-- 配置一个C3P0连接池和JDBC的模板~~~~~~~~~~~~~~~~~~ -->
<!-- 第二种方式:通过context标签来引入 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 配置Spring的JDBC模板,它还得需要使用一个连接池 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 属性注入 -->
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
在以上tx.xml文件中,可以看到我们使用的是C3P0连接池,连接池对象中所使用到的配置信息(包括数据库驱动类的全名称、要连接的数据库、用户名以及密码等)都来自于src目录下的jdbc.properties属性文件当中,该文件的内容如下所示。
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///spring4_demo03_tx
jdbc.username=root
jdbc.password=liayun
接着,在AccountDaoImpl实现类中使用JdbcTemplate模板类来编写扣钱和加钱方法的具体实现代码。我们先将该实现类写成下面这样,但这肯定不是最终的样子,只不过这是我们最容易想到的,好像理应就是这样写!后面我会教大家如何优化该实现类。
package com.meimeixia.tx.demo01;
import org.springframework.jdbc.core.JdbcTemplate;
/** * 转账的Dao的实现类 * @author liayun * */
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
//扣钱
@Override
public void outMoney(String from, Double money) {
jdbcTemplate.update("update account set money = money - ? where name = ?", money, from);
}
//加钱
@Override
public void inMoney(String to, Double money) {
jdbcTemplate.update("update account set money = money + ? where name = ?", money, to);
}
}
紧接着,在Spring的配置文件中id为accountDao的bean标签里面使用property标签注入以上jdbcTemplate属性。
上面写的AccountDaoImpl实现类的代码不是最优的,你想啊,每一次你编写XxxDao的时候,都需要在其中定义一个jdbcTemplate属性,并生成set方法,这不是有点脑残吗?所以说,Spring发现你每写一个XxxDao,都得自己亲自撸一遍,怕你觉得麻烦,就给你提供了一个JdbcDaoSupport类,你只需要让你的实现类去继承它就可以不用这样干了。如果你不信的话,可以查看该类的源代码,你就能发现它里面提供了一个setJdbcTemplate方法,所以说你只需要让你的实现类去继承它,那么相当于就有了这个setJdbcTemplate方法了。
如此一来,AccountDaoImpl实现类就可以改写成下面这样了。
package com.meimeixia.tx.demo01;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
/** * 转账的Dao的实现类 * @author liayun * */
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
// private JdbcTemplate jdbcTemplate;
//
// public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
// this.jdbcTemplate = jdbcTemplate;
// }
//扣钱
@Override
public void outMoney(String from, Double money) {
this.getJdbcTemplate().update("update account set money = money - ? where name = ?", money, from);
}
//加钱
@Override
public void inMoney(String to, Double money) {
this.getJdbcTemplate().update("update account set money = money + ? where name = ?", money, to);
}
}
其实AccountDaoImpl实现类继承了JdbcDaoSupport类之后,还可以让它的代码简化,查看JdbcDaoSupport类的源代码,在它的源代码里面,还提供了一个setDataSource(DataSource dataSource)方法,如果你给它注入了一个连接池的话,它便会使用这个连接池帮你创建一个JDBC的模板。
所以说,谁继承了这个类,谁就可以直接给这个类里面注入一个连接池了。这带来的好处便是在Spring配置文件中就再也不用定义JDBC的模板了。
至此,总算是搭建好咱的Spring事务管理的环境了,真是不容易!
在com.meimeixia.tx.demo01包下编写一个SpringDemo01单元测试类。
package com.meimeixia.tx.demo01;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/** * 测试转账的环境 * @author liayun * */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx.xml")
public class SpringDemo01 {
@Resource(name="accountService")
private AccountService accountService;
@Test
public void demo01() {
accountService.transfer("秦始皇", "刘邦", 1000d);
}
}
运行以上demo01方法即可实现秦始皇转账1000元给刘邦的操作。现在我来演示一个问题,在AccountServiceImpl类中调用了AccountDaoImpl类的两个方法构成了一个转账业务,但是如果秦始皇少了1000元之后,这时突然出现异常,比如银行断电,就会出现秦始皇的钱少了1000元,而刘邦的钱没有多出1000元,钱丢失了的情况。
package com.meimeixia.tx.demo01;
/** * 转账的业务层的实现类 * @author liayun * */
public class AccountServiceImpl implements AccountService {
//注入AccountDao
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
/* * from:转出账号 * to:转入账号 * money:转账金额 */
@Override
public void transfer(String from, String to, Double money) {
accountDao.outMoney(from, money);
int d = 1 / 0;//模拟银行断电的情况(在这儿出现了异常)
accountDao.inMoney(to, money);
}
}
这时应该怎么解决钱转丢了的这个问题呢?可以使用事务来解决,Spring中的事务管理主要分为两大类,它们分别是:
下面,我会分别对这两类事务管理进行介绍。
为了解决上面钱转丢了的问题,这里我会使用Spring的编程式事务管理这种方式来管理事务。步骤如下所示。
package com.meimeixia.tx.demo01;
import org.springframework.transaction.support.TransactionTemplate;
/** * 转账的业务层的实现类 * @author liayun * */
public class AccountServiceImpl implements AccountService {
//注入AccountDao
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
//注入事务管理的模板
private TransactionTemplate transactionTemplate;
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
/* * from:转出账号 * to:转入账号 * money:转账金额 */
@Override
public void transfer(String from, String to, Double money) {
accountDao.outMoney(from, money);
int d = 1 / 0;//模拟银行断电的情况(在这儿出现了异常)
accountDao.inMoney(to, money);
}
}
然后,在id为accountService的bean标签里面使用property标签注入以上transactionTemplate属性,即在业务层中注入事务管理的模板。
此时,再次运行SpringDemo01单元测试类中的demo01方法,可以发现即使出现了异常,比如银行断电,也不会出现钱转丢了的情况。
基于XML配置文件的方式来进行声明式事务的操作,不需要进行手动编写代码,通过一段配置即可完成事务管理。为了演示这种方式的声明式事务管理,我们得回到刚开始搭建好的转账环境下,即出现了异常时(比如银行断电),钱会转丢的情况下。如果是使用Spring基于XML配置文件方式的声明式事务管理来解决这个问题,那么步骤如下所示。
其中,<tx:method>
标签中有如下一些属性。
此时,运行SpringDemo01单元测试类中的demo01方法,可以发现即使出现了异常,比如银行断电,也不会出现钱转丢了的情况。
基于注解方式来进行声明式事务的操作会更加简单,在实际开发中我们也会用的比较多。为了演示这种方式的声明式事务管理,我们得回到刚开始搭建好的转账环境下,即出现了异常时(比如银行断电),钱会转丢的情况下。如果是使用Spring基于注解方式的声明式事务管理来解决这个问题,那么步骤如下所示。
以上配置添加完毕之后,Spring配置文件的内容就变成下面这个样子了。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 配置Service和Dao~~~~~~~~~~~~~~~~~~ -->
<bean id="accountService" class="com.meimeixia.tx.demo03.AccountServiceImpl">
<property name="accountDao" ref="accountDao" />
</bean>
<!-- 配置Dao~~~~~~~~~~~~~~~~~~ -->
<!-- <bean id="accountDao" class="com.meimeixia.tx.demo01.AccountDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate" /> </bean> -->
<!-- 注意:其实你继承了JdbcDaoSupport类之后,还可以让你的代码简化,查看该类的源代码, 在它的源代码里面,它还提供了一个setDataSource(DataSource dataSource)方法, 给它注入一个连接池,如果你给它注入了一个连接池的话,它会使用这个连接池帮你创建一个JDBC的模板。 所以说,谁继承了这个类,谁就可以直接给这个类里面注入一个连接池。 好处:这儿都不用写JDBC模板的定义了。 -->
<bean id="accountDao" class="com.meimeixia.tx.demo03.AccountDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!-- 配置一个C3P0连接池和JDBC的模板~~~~~~~~~~~~~~~~~~ -->
<!-- 第二种方式:通过context标签来引入 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 配置Spring的JDBC模板,它还得需要使用一个连接池 -->
<!-- <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> 属性注入 <property name="dataSource" ref="dataSource" /> </bean> -->
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!-- 配置平台的事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 平台事务管理器必须得能够拿到连接,它才能帮我们管理事务 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启注解(式)事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
此时,运行SpringDemo01单元测试类中的demo01方法,可以发现即使出现了异常,比如银行断电,也不会出现钱转丢了的情况。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://liayun.blog.csdn.net/article/details/70024364
内容来源于网络,如有侵权,请联系作者删除!