1. 程式人生 > >Spring --15.Spring中基於xml的宣告式事務控制

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(TransactionDefinition   definition)

--提交事務: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中提供了七種事務的傳播行為:

      保證多個操作在同一個事務中

:預設值,如果A中有事務,使用A中的事務,如果A沒有,建立一個新的事務,將操作包含進來

:支援事務,如果A中有事務,使用A中的事務。如果A沒有事務,不使用事務。

     保證多個操作不在同一個事務中

:如果A中有事務,將A的事務掛起(暫停),建立新事務,只包含自身操作。如果A中沒有事務,建立一個新事務,包含自身操作。

    巢狀式事務

:巢狀事務,如果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方法,發現控制檯報錯,異常資訊如下: