0x00 参考 本笔记直接参考或引自如下链接文章:
https://www.w3cschool.cn/wkspring/
http://c.biancheng.net/spring/
0x01 Spring事务管理 事务就是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。
在企业级应用程序开发中,事务管理是必不可少的技术,用来确保数据的完整性和一致性。
事务的概念可以描述为具有以下四个关键属性说成是ACID :
原子性: 事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。
一致性: 这表示数据库的引用完整性的一致性,表中唯一的主键等。
隔离性: 可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。
持久性: 一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。
Spring的事务管理是基于AOP实现的,而AOP是以方法为单位的。Spring的事务属性分别为传播行为、隔离级别、只读和超时属性,这些属性提供了事务应用的方法和描述策略。
在Java EE开发经常采用的分层模式中,Spring的事务处理位于业务逻辑层,它提供了针对事务的解决方案。
0x02 三个核心类 在Spring解压包的libs目录中,包含一个名称为spring-tx-4.1.6.RELEASE.jar的文件,该文件是 Spring 提供的用于事务管理的 JAR 包,其中包括事务管理的三个核心接口:PlatformTransactionManager、TransactionDefinition和TransactionStatus。
三个核心类之间的关系如图:
PlatformTransactionManager接口是Spring提供的平台事务管理器,用于管理事务。该接口中提供了三个事务操作方法,具体如下:
TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。
void commit(TransactionStatus status):用于提交事务。
void rollback(TransactionStatus status):用于回滚事务。
在项目中,Spring将xml中配置的事务详细信息封装到对象TransactionDefinition中,然后通过事务管理器的getTransaction()方法获得事务的状态(TransactionStatus),并对事务进行下一步的操作。
TransactionDefinition TransactionDefinition接口是事务定义(描述)的对象,它提供了事务相关信息获取的方法,其中包括五个操作,具体如下:
String getName():获取事务对象名称。
int getIsolationLevel():获取事务的隔离级别。
int getPropagationBehavior():获取事务的传播行为。
int getTimeout():获取事务的超时时间。
boolean isReadOnly():获取事务是否只读。
隔离级别:
序号
隔离 & 描述
1
TransactionDefinition.ISOLATION_DEFAULT 这是默认的隔离级别。
2
TransactionDefinition.ISOLATION_READ_COMMITTED 表明能够阻止误读;可以发生不可重复读和虚读。
3
TransactionDefinition.ISOLATION_READ_UNCOMMITTED 表明可以发生误读、不可重复读和虚读。
4
TransactionDefinition.ISOLATION_REPEATABLE_READ 表明能够阻止误读和不可重复读;可以发生虚读。
5
TransactionDefinition.ISOLATION_SERIALIZABLE 表明能够阻止误读、不可重复读和虚读。
传播类型:
序号
传播 & 描述
1
TransactionDefinition.PROPAGATION_MANDATORY 支持当前事务;如果不存在当前事务,则抛出一个异常。
2
TransactionDefinition.PROPAGATION_NESTED 如果存在当前事务,则在一个嵌套的事务中执行。
3
TransactionDefinition.PROPAGATION_NEVER 不支持当前事务;如果存在当前事务,则抛出一个异常。
4
TransactionDefinition.PROPAGATION_NOT_SUPPORTED 不支持当前事务;而总是执行非事务性。
5
TransactionDefinition.PROPAGATION_REQUIRED 支持当前事务;如果不存在事务,则创建一个新的事务。
6
TransactionDefinition.PROPAGATION_REQUIRES_NEW 创建一个新事务,如果存在一个事务,则把当前事务挂起。
7
TransactionDefinition.PROPAGATION_SUPPORTS 支持当前事务;如果不存在,则执行非事务性。
8
TransactionDefinition.TIMEOUT_DEFAULT 使用默认超时的底层事务系统,或者如果不支持超时则没有。
TransactionStatus TransactionStatus接口是事务的状态,它描述了某一时间点上事务的状态信息。其中包含六个操作,具体如下:
名称
说明
void flush()
刷新事务
boolean hasSavepoint()
获取是否存在保存点
boolean isCompleted()
获取事务是否完成
boolean isNewTransaction()
获取是否是新事务
boolean isRollbackOnly()
获取是否回滚
void setRollbackOnly()
设置事务回滚
0x03 编程式事务管理 编程式事务管理方法允许你在对你的源代码编程的帮助下管理事务。这给了你极大地灵活性,但是它很难维护。
Demo 注意,这里需要的jar包和前面《Spring基础篇之JDBC框架》 中的Demo是一样的。
StudentMarks.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.mi1k7ea;public class StudentMarks { private Integer age; private String name; private Integer id; private Integer marks; private Integer year; private Integer sid; public void setAge (Integer age) { this .age = age; } public Integer getAge () { return age; } public void setName (String name) { this .name = name; } public String getName () { return name; } public void setId (Integer id) { this .id = id; } public Integer getId () { return id; } public void setMarks (Integer marks) { this .marks = marks; } public Integer getMarks () { return marks; } public void setYear (Integer year) { this .year = year; } public Integer getYear () { return year; } public void setSid (Integer sid) { this .sid = sid; } public Integer getSid () { return sid; } }
StudentDAO.java,数据访问对象接口类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.mi1k7ea;import javax.sql.DataSource;import java.util.List;public interface StudentDAO { public void setDataSource (DataSource ds) ; public void create (String name, Integer age, Integer marks, Integer year) ; public List<StudentMarks> listStudents () ; }
StudentMarksMapper.java,:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.mi1k7ea;import org.springframework.jdbc.core.RowMapper;import java.sql.ResultSet;import java.sql.SQLException;public class StudentMarksMapper implements RowMapper <StudentMarks > { public StudentMarks mapRow (ResultSet rs, int rowNum) throws SQLException { StudentMarks studentMarks = new StudentMarks(); studentMarks.setId(rs.getInt("id" )); studentMarks.setName(rs.getString("name" )); studentMarks.setAge(rs.getInt("age" )); studentMarks.setSid(rs.getInt("sid" )); studentMarks.setMarks(rs.getInt("marks" )); studentMarks.setYear(rs.getInt("year" )); return studentMarks; } }
StudentJDBCTemplate.java,DAO接口类StudentDAO的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.mi1k7ea;import org.springframework.dao.DataAccessException;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.TransactionDefinition;import org.springframework.transaction.TransactionStatus;import org.springframework.transaction.support.DefaultTransactionDefinition;import javax.sql.DataSource;import java.util.List;public class StudentJDBCTemplate implements StudentDAO { private DataSource dataSource; private JdbcTemplate jdbcTemplateObject; private PlatformTransactionManager transactionManager; public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; this .jdbcTemplateObject = new JdbcTemplate(dataSource); } public void setTransactionManager ( PlatformTransactionManager transactionManager) { this .transactionManager = transactionManager; } public void create (String name, Integer age, Integer marks, Integer year) { TransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { String SQL1 = "insert into Student (name, age) values (?, ?)" ; jdbcTemplateObject.update( SQL1, name, age); String SQL2 = "select max(id) from Student" ; int sid = jdbcTemplateObject.queryForInt( SQL2 ); String SQL3 = "insert into Marks(sid, marks, year) " + "values (?, ?, ?)" ; jdbcTemplateObject.update( SQL3, sid, marks, year); System.out.println("Created Name = " + name + ", Age = " + age); transactionManager.commit(status); } catch (DataAccessException e) { System.out.println("Error in creating record, rolling back" ); transactionManager.rollback(status); throw e; } return ; } public List<StudentMarks> listStudents () { String SQL = "select * from Student, Marks where Student.id=Marks.sid" ; List <StudentMarks> studentMarks = jdbcTemplateObject.query(SQL, new StudentMarksMapper()); return studentMarks; } }
MainApp.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.mi1k7ea;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import java.util.List;public class MainApp { public static void main (String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml" ); StudentJDBCTemplate studentJDBCTemplate = (StudentJDBCTemplate)context.getBean("studentJDBCTemplate" ); System.out.println("------Records creation--------" ); studentJDBCTemplate.create("Zara" , 11 , 99 , 2010 ); studentJDBCTemplate.create("Nuha" , 20 , 97 , 2010 ); studentJDBCTemplate.create("Ayan" , 25 , 100 , 2011 ); System.out.println("------Listing all the records--------" ); List<StudentMarks> studentMarks = studentJDBCTemplate.listStudents(); for (StudentMarks record : studentMarks) { System.out.print("ID : " + record.getId() ); System.out.print(", Name : " + record.getName() ); System.out.print(", Marks : " + record.getMarks()); System.out.print(", Year : " + record.getYear()); System.out.println(", Age : " + record.getAge()); } } }
Beans.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd " > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/test?serverTimezone=UTC" /> <property name ="username" value ="root" /> <property name ="password" value ="" /> </bean > <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="studentJDBCTemplate" class ="com.mi1k7ea.StudentJDBCTemplate" > <property name ="dataSource" ref ="dataSource" /> <property name ="transactionManager" ref ="transactionManager" /> </bean > </beans >
在执行之前,需要在数据库中执行以下SQL语句创建test数据库以及新建相关的表:
1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE DATABASE test ;USE test ;CREATE TABLE Student( ID INT NOT NULL AUTO_INCREMENT, NAME VARCHAR (20 ) NOT NULL , AGE INT NOT NULL , PRIMARY KEY (ID ) ); CREATE TABLE Marks( SID INT NOT NULL , MARKS INT NOT NULL , YEAR INT NOT NULL );
创建成功后,运行MainApp成功使用JDBC框架实现数据库的连接使用:
0x04 声明式事务管理 Spring声明式事务管理在底层采用了AOP技术,其最大的优点在于无须通过编程的方式管理事务,只需要在配置文件中进行相关的规则声明,就可以将事务规则应用到业务逻辑中。
下面是与声明式事务相关的步骤:
我们使用标签,它创建一个事务处理的建议,同时,我们定义一个匹配所有方法的切入点,我们希望这些方法是事务型的并且会引用事务型的建议。
如果在事务型配置中包含了一个方法的名称,那么创建的建议在调用方法之前就会在事务中开始进行。
目标方法会在try/catch块中执行。
如果方法正常结束,AOP建议会成功的提交事务,否则它执行回滚操作。
Spring实现声明式事务管理主要有两种方式:
基于XML方式的声明式事务管理。
通过Annotation注解方式的事务管理。
基于XML实现事务管理 基于XML形式实现的事务管理,就是通过XML配置文件来实现的事务管理。下面直接看示例。
注意,这里需要的jar包,在前面编程式事务管理的基础上,还需添加一个c3p0、mchange-commons的jar包。
先在MySQL中创建一个名为spring的数据库,然后在该数据库中创建一个account表,并向表中插入两条数据,其SQL执行语句如下:
1 2 3 4 5 6 7 8 9 CREATE DATABASE spring;USE spring;CREATE TABLE account ( id INT (11 ) PRIMARY KEY AUTO_INCREMENT, username VARCHAR (20 ) NOT NULL , money INT DEFAULT NULL ); INSERT INTO account VALUES (1 ,'zhangsan' ,1000 );INSERT INTO account VALUES (2 ,'lisi' ,1000 );
接着在项目的src下创建一个名为c3p0-db.properties的配置文件,这里使用C3P0数据源,需要在该文件中添加如下配置:
1 2 3 4 jdbc.driverClass = com.mysql.jdbc.Driver jdbc.jdbcUrl = jdbc:mysql://localhost:3306/spring?serverTimezone=UTC jdbc.user = root jdbc.password =
AccountDao.java,DAO接口类,在com.mi1k7ea.dao包中,并在接口中创建汇款和收款的方法:
1 2 3 4 5 6 7 8 package com.mi1k7ea.dao;public interface AccountDao { void out (String outUser, int money) ; void in (String inUser, int money) ; }
AccountDaoImpl.java,DAO层接口实现类,在com.mi1k7ea.dao.impl包中,使用JdbcTemplate.update()方法实现了更新操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.mi1k7ea.dao.impl;import com.mi1k7ea.dao.AccountDao;import org.springframework.jdbc.core.JdbcTemplate;public class AccountDaoImpl implements AccountDao { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate (JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } public void out (String outUser, int money) { this .jdbcTemplate.update("update account set money =money-? where username =?" , money, outUser); } public void in (String inUser, int money) { this .jdbcTemplate.update("update account set money =money+? where username =?" , money, inUser); } }
AccountService.java,Service层DAO接口类,在com.mi1k7ea.service包中:
1 2 3 4 5 6 package com.mi1k7ea.service;public interface AccountService { void transfer (String outUser, String inUser, int money) ; }
AccountServiceImpl.java,Service层DAO接口实现类,在com.mi1k7ea.service.impl包中,实现了AccountService接口,,并对转账方法进行了实现,根据参数的不同调用DAO层相应的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.mi1k7ea.service.impl;import com.mi1k7ea.dao.AccountDao;import com.mi1k7ea.service.AccountService;public class AccountServiceImpl implements AccountService { private AccountDao accountDao; public void setAccountDao (AccountDao accountDao) { this .accountDao = accountDao; } public void transfer (String outUser, String inUser, int money) { this .accountDao.out(outUser, money); this .accountDao.in(inUser, money); } }
创建Spirng配置文件applicationContext.xml,位于src目录下,其中使用<tx:advice>
标记配置事务通知内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?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:tx ="http://www.springframework.org/schema/tx" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd" > <context:property-placeholder location ="classpath:c3p0-db.properties" /> <bean id ="dataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" > <property name ="driverClass" value ="${jdbc.driverClass}" /> <property name ="jdbcUrl" value ="${jdbc.jdbcUrl}" /> <property name ="user" value ="${jdbc.user}" /> <property name ="password" value ="${jdbc.password}" /> </bean > <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="accountDao" class ="com.mi1k7ea.dao.impl.AccountDaoImpl" > <property name ="jdbcTemplate" ref ="jdbcTemplate" /> </bean > <bean id ="accountService" class ="com.mi1k7ea.service.impl.AccountServiceImpl" > <property name ="accountDao" ref ="accountDao" /> </bean > <bean id ="txManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean > <tx:advice id ="txAdvice" transaction-manager ="txManager" > <tx:attributes > <tx:method name ="find*" propagation ="SUPPORTS" rollback-for ="Exception" /> <tx:method name ="*" propagation ="REQUIRED" isolation ="DEFAULT" read-only ="false" /> </tx:attributes > </tx:advice > <aop:config > <aop:pointcut expression ="execution(* com.mi1k7ea.service.*.*(..))" id ="txPointCut" /> <aop:advisor pointcut-ref ="txPointCut" advice-ref ="txAdvice" /> </aop:config > </beans >
MainApp.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.mi1k7ea;import com.mi1k7ea.service.AccountService;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class MainApp { public static void main (String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml" ); AccountService accountService = (AccountService) applicationContext.getBean("accountService" ); accountService.transfer("zhangsan" , "lisi" , 100 ); } }
运行之后,无报错的话,在MySQL中就可以看到zhangsan用户的钱减少了100而lisi的钱多了100:
基于Annotation实现事务管理 使用Annotation的方式非常简单,只需要在项目中做两件事:
在Spring容器中注册驱动,代码如下:
1 <tx:annotation-driven transaction-manager ="txManager" />
在需要使用事务的业务类或者方法中添加注解@Transactional,并配置@Transactional的参数;
下面还是修改上一小节的示例作为演示。
修改Spring配置文件applicationContext.xml,与原来的配置文件相比,这里只修改了事务管理器部分,新添加并注册了事务管理器的驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?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:tx ="http://www.springframework.org/schema/tx" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd" > <context:property-placeholder location ="classpath:c3p0-db.properties" /> <bean id ="dataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" > <property name ="driverClass" value ="${jdbc.driverClass}" /> <property name ="jdbcUrl" value ="${jdbc.jdbcUrl}" /> <property name ="user" value ="${jdbc.user}" /> <property name ="password" value ="${jdbc.password}" /> </bean > <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="accountDao" class ="com.mi1k7ea.dao.impl.AccountDaoImpl" > <property name ="jdbcTemplate" ref ="jdbcTemplate" /> </bean > <bean id ="accountService" class ="com.mi1k7ea.service.impl.AccountServiceImpl" > <property name ="accountDao" ref ="accountDao" /> </bean > <bean id ="txManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean > <tx:annotation-driven transaction-manager ="txManager" /> </beans >
需要注意得失,在学习AOP注解方式开发时,需要在配置文件中开启注解处理器,指定扫描哪些包下的注解,这里没有开启注解处理器是因为在XML中手动配置了AccountServiceImpl,而@Transactional注解就配置在该类中,所以会直接生效。
修改AccountServiceImpl.java,在文件中添加@Transactional注解及参数,注意在使用@Transactional注解时参数之间用”,”进行分割:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.mi1k7ea.service.impl;import com.mi1k7ea.dao.AccountDao;import com.mi1k7ea.service.AccountService;import org.springframework.transaction.annotation.Isolation;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;@Transactional (propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false )public class AccountServiceImpl implements AccountService { private AccountDao accountDao; public void setAccountDao (AccountDao accountDao) { this .accountDao = accountDao; } public void transfer (String outUser, String inUser, int money) { this .accountDao.out(outUser, money); this .accountDao.in(inUser, money); } }
运行MainApp之后,效果和前面的是一样的。