Spring --15.Spring中基於xml的宣告事務控制
開發環境:
jdk1.8
Idea 2017 :Maven工程、引入父工程
Tomcat:apache-tomcat-8
Spring:5.0.7
一、事務控制
1、概述
事務的概念:
事務是邏輯上一組操作、組成這組操作各個邏輯單元、要麼一起成功、要麼一起失敗。
事務的特性:
原子性:事務不可分割
一致性:事務執行前後資料完整性保持一致
隔離性:一個事務的執行不應該受到其他事條的干擾
永續性:一旦事務結束、資料就持久化到資料庫
2、事務併發讀問題
髒讀:一個事務讀取到另一個事務未提交的資料 不可重複讀:一個事務讀取到另一個事務已提交的update的資料、導致一個事務中多次查詢結果不一致 虛讀(幻讀):一個事務讀取到另一個事務已提交的insert的資料、導致一個事務中多次查詢結果不一致
解決讀問題:設定事務隔離級別:
read uncommitted:未提交讀、什麼也解決不了
read committed :已提交讀、解決髒讀、解決不了不可重複讀和虛讀
repeatable read :重複讀、解決髒讀和不可重複讀
Serializable:序列化、解決所有讀問題
3、Spring事務管理的API
3.1、PlatformTransactionManager:平臺事務管理器
platformTransactionManager介面提供事務操作的方法包含3個具體的方法:
--獲取事務狀態資訊:TransactionStatus getTransaction
--提交事務:void commit(TransactionStatus status)
--回滾事務:void rollback(TransactionStatus status)
platformTransactionManager實現類:
DataSourceTransactionManager --使用Spring JDBC或iBatis進行持久化資料時使用
3.2、TransactionDefinition:事務定義資訊(介面)
事務定義:用於定義事務的相關的資訊、隔離級別、超時資訊、傳播行為、是否可讀
有如下方法:
(1)--String getName() -- 獲取事務物件名稱
(2)--int getlsolationLevel() --獲取事務的隔離級別
(3)--int getPropagationBehavior()-- 獲取事務傳播行為
(4)--int getTimeout() --獲取事務超時時間
(5)--boolean isReadOnly() -- 獲取事務是否只讀
==================================================
--int getlsolationLevel() --獲取事務的隔離級別
--int getPropagationBehavior()-- 獲取事務傳播行為
Spring中提供了七種事務的傳播行為:
保證多個操作在同一個事務中
PROPAGATION_REQUIRED
:預設值,如果A中有事務,使用A中的事務,如果A沒有,建立一個新的事務,將操作包含進來
PROPAGATION_SUPPORTS
:支援事務,如果A中有事務,使用A中的事務。如果A沒有事務,不使用事務。
PROPAGATION_MANDATORY
:如果A中有事務,使用A中的事務。如果A沒有事務,丟擲異常。
保證多個操作不在同一個事務中
PROPAGATION_REQUIRES_NEW
:如果A中有事務,將A的事務掛起(暫停),建立新事務,只包含自身操作。如果A中沒有事務,建立一個新事務,包含自身操作。
PROPAGATION_NOT_SUPPORTED
:如果A中有事務,將A的事務掛起。不使用事務管理。
PROPAGATION_NEVER
:如果A中有事務,報異常。
巢狀式事務
PROPAGATION_NESTED
:巢狀事務,如果A中有事務,按照A的事務執行,執行完成後,設定一個儲存點,執行B中的操作,如果沒有異常,執行通過,如果有異常,可以選擇回滾到最初始位置,也可以回滾到儲存點。
--int getTimeout 超時時間、預設值是-1,沒有超時限制。如果有,以秒為單位進行設定。
--boolean isReadOnly() -- 建議查詢設定為只讀
3.3、TransactionStatus:事務的狀態(介面)
事務狀態:用於記錄在事務管理過程中、事務的狀態的物件
包含6個具體的方法:
--void flush() --重新整理事務
--boolean hasSavePoint() -- 獲取是否是存在儲存點
--boolean isCompleted() -- 獲取事務是否完成
--boolean isNewTransaction() --獲取事務是否為新的事務
--boolean isRollbackOnly() --獲取事務是否回滾
--void setRollbackOnly() --設定事務回滾
3.4、事務管理的API關係
Spring進行事務管理的時候、首先平臺事務管理器根據事務定義資訊進行事務的管理、在事務管理過程中、產生各種狀態、
將這些狀態的資訊記錄到事務狀態的物件中
二、Spring基於xml的宣告事條控制
1、建立工程、引入依賴
建立maven子模組、
匯入依賴:Ioc依賴、aop依賴、jdbc模板+資料庫連線池的依賴、Spring整合Junit單元測試依賴
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<!-- jdbc模板 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
2、編寫相關類
domain/Account.java
package com.day04_tx.domain;
import java.io.Serializable;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 8:50 2018/11/15
*/
public class Account implements Serializable {
private Long id;
private String name;
private Double money;
public Account(Long id, String name, Double money) {
this.id = id;
this.name = name;
this.money = money;
}
public Account() {
}
public Long getId() {
return id;
}
public void setId(Long 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 + '\'' +
'}';
}
}
重寫RowMapper介面的實現類AccountRowMapper類、把資料庫表中的一行資料形成帳戶物件
AccouontRowMapper.java
package com.day04_tx.rowmapper;
import com.day04_tx.domain.Account;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
*把資料庫表中的一行資料形成帳戶物件
* @ Author :ShaoWei Sun.
* @ Date :Created in 8:51 2018/11/15
*/
public class AccountRowMapper implements RowMapper<Account> {
@Override
public Account mapRow(ResultSet resultSet, int row) throws SQLException {
Account account = new Account();
account.setId(resultSet.getLong("id"));
account.setName(resultSet.getString("name"));
account.setMoney(resultSet.getDouble("money"));
return account;
}
}
AccountDao.java介面
package com.day04_tx.dao;
import com.day04_tx.domain.Account;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:36 2018/11/15
*/
public interface AccountDao {
/**
* 根據id查詢使用者物件
* @param id
* @return
*/
public Account findById(Long id);
/**
* 更新使用者物件
* @param account
*/
public void update(Account account);
}
編寫AccountDaoImpl實現類、繼承JdbcDaoSupport(xml中就不用注入jdbc了)
package com.day04_tx.dao.Impl;
import com.day04_tx.dao.AccountDao;
import com.day04_tx.domain.Account;
import com.day04_tx.rowmapper.AccountRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
/**
* 繼承JdbcDaoSupport 使用這個類中的getJdbcTemplate方法
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:38 2018/11/15
*/
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
/**
* 根據id查詢使用者物件
* @param id
* @return
*/
@Override
public Account findById(Long id) {
Account account = this.getJdbcTemplate().queryForObject("select * from account where id = ?", new AccountRowMapper(), id);
return account;
}
/**
* 更新使用者物件
* @param account
*/
@Override
public void update(Account account) {
this.getJdbcTemplate().update("update account set name = ?, money = ? where id =? ",account.getName(),account.getMoney(),account.getId());
}
}
AccountService.java介面
package com.day04_tx.service;
import com.day04_tx.domain.Account;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:50 2018/11/15
*/
public interface AccountService {
/**
* 業務層:轉賬方法
* @param fromId 轉出賬戶
* @param toId 轉入賬戶
* @param money 轉帳金額
*/
public abstract void transfer(Long fromId, Long toId, Double money);
/**
* 根據id查詢賬戶物件
* @param id 使用者id
* @return 返回Account
*/
Account findById(Long id);
}
編寫AccountServiceImpl實現類 :
package com.day04_tx.service.Imlp;
import com.day04_tx.dao.AccountDao;
import com.day04_tx.domain.Account;
import com.day04_tx.service.AccountService;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:53 2018/11/15
*/
public class AccountServiceImpl implements AccountService {
//依賴注入accountDao到Spring中
private AccountDao accountDao;
//通過set方法把AccountDao注入到Spring容器中
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(Long fromId, Long toId, Double money) {
//查詢轉出賬戶
Account fromAccount = accountDao.findById(fromId);
//查詢轉入帳戶
Account toAccount = accountDao.findById(toId);
//轉出帳戶撿錢
fromAccount.setMoney(fromAccount.getMoney()-money);
//轉入帳戶加錢
toAccount.setMoney(toAccount.getMoney()+money);
//更新轉出賬戶
accountDao.update(fromAccount);
// int i = 1/0;
//更新轉入賬戶
accountDao.update(toAccount);
}
/**
* 查詢功能、現在做了修改操作、但不允許的。
* @param id 使用者id
* @return
*/
@Override
public Account findById(Long id) {
Account account = accountDao.findById(id);
account.setMoney(100000d);
accountDao.update(account);
return account;
}
}
3、把類交給Spring容器來管理
建立applicationContext.xml,並引入jdbc.properties,log4j.properties:
log4j.properties
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
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=D:/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' ###
log4j.rootLogger=debug, stdout, file
jdbc.properties
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/mybatis
jdbc.username = root
jdbc.password = sswqzx
applicationContext.xml
<?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.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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--匯入jdbc-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="accountService" class="com.day04_tx.service.Imlp.AccountServiceImpl">
<property name="accountDao" ref="accountDao" > </property>
</bean>
<bean id="accountDao" class="com.day04_tx.dao.Impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
</beans>
4、編寫測試類
src/test/java/com.day04_tx.test/TestTx.java
package com.day04_tx.test;
import com.day04_tx.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 10:15 2018/11/15
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Tx_test {
@Autowired
private AccountService accountService;
//用了註解就不要用set方法了。不會生效
// public void setAccountService(AccountService accountService) {
//// this.accountService = accountService;
//// }
//事務測試方法
@Test
public void test1(){
accountService.transfer(1L,2L,1000d);
}
//查詢測試方法
@Test
public void test2(){
accountService.findById(1L);
}
}
執行Test1
注意:
如果在AccountServiceImpl中加入自定義的異常、張三錢轉了。但王五沒收到的情況、解決這種情況就要用到事務
5、在Spring中配置事務
上面的案例中、需要據transfer方法套在一個事務裡、transfer中所有的操作要麼成功、要麼全部取消
applicationContext.xml
<?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.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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--匯入jdbc-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="accountService" class="com.day04_tx.service.Imlp.AccountServiceImpl">
<property name="accountDao" ref="accountDao" > </property>
</bean>
<bean id="accountDao" class="com.day04_tx.dao.Impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入資料來源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事務的屬性-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--find開頭的方法加只讀事務、*表示萬用字元、匹配任意方法-->
<tx:method name="find*" read-only="true"/>
<!--其餘方法是加可讀寫的事務-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--配置事務的切面-->
<aop:config>
<!--配置切入點表示式、告訴框架哪些方法要控制事務-->
<aop:pointcut id="pt" expression="execution(* com.day04_tx.service.Imlp.*.*(..))"/>
<!--將定義好的事務屬性應用到上述切入點-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>
</beans>
測試事務是否成功、
再次執行TestTx中的test1方法,控制檯依然會報“被零除異常”,但是表中的資料沒有發生變化:張三的錢沒有轉,王五的錢也沒有變化。
測試只讀事務
在AcountService介面中增加一個findById方法:根據id查詢賬戶物件
package com.day04_tx.service;
import com.day04_tx.domain.Account;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:50 2018/11/15
*/
public interface AccountService {
/**
* 業務層:轉賬方法
* @param fromId 轉出賬戶
* @param toId 轉入賬戶
* @param money 轉帳金額
*/
public abstract void transfer(Long fromId, Long toId, Double money);
/**
* 根據id查詢賬戶物件
* @param id 使用者id
* @return 返回Account
*/
Account findById(Long id);
}
在AccountServiceImpl中實現findById方法 :
package com.day04_tx.service.Imlp;
import com.day04_tx.dao.AccountDao;
import com.day04_tx.domain.Account;
import com.day04_tx.service.AccountService;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:53 2018/11/15
*/
public class AccountServiceImpl implements AccountService {
//依賴注入accountDao到Spring中
private AccountDao accountDao;
//通過set方法把AccountDao注入到Spring容器中
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(Long fromId, Long toId, Double money) {
//查詢轉出賬戶
Account fromAccount = accountDao.findById(fromId);
//查詢轉入帳戶
Account toAccount = accountDao.findById(toId);
//轉出帳戶撿錢
fromAccount.setMoney(fromAccount.getMoney()-money);
//轉入帳戶加錢
toAccount.setMoney(toAccount.getMoney()+money);
//更新轉出賬戶
accountDao.update(fromAccount);
int i = 1/0;
//更新轉入賬戶
accountDao.update(toAccount);
}
/**
* 查詢功能、現在做了修改操作、但不允許的。
* @param id 使用者id
* @return
*/
@Override
public Account findById(Long id) {
Account account = accountDao.findById(id);
account.setMoney(100000d);
accountDao.update(account);
return account;
}
}
在單元測試類TestTx中建立第二個單元測試方法test2,測試findById方法 :
package com.day04_tx.test;
import com.day04_tx.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 10:15 2018/11/15
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Tx_test {
@Autowired
private AccountService accountService;
//用了註解就不要用set方法了。不會生效
// public void setAccountService(AccountService accountService) {
//// this.accountService = accountService;
//// }
//事務測試方法
@Test
public void test1(){
accountService.transfer(1L,2L,1000d);
}
//查詢測試方法
@Test
public void test2(){
accountService.findById(1L);
}
}
執行test2方法,發現控制檯報錯,異常資訊如下: