Spring框架5:事務和動態代理


本系列筆記均是對b站教程https://www.bilibili.com/video/av47952931 的學習筆記,非本人原創

enter description here

事務

我們在service中加一個轉賬的功能

public void transfer(String sourceName, String targetName, Float money) {
            Account source = accountDao.findAccountByName(sourceName);
            Account target = accountDao.findAccountByName(targetName);
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            accountDao.updateAccount(source);
            accountDao.updateAccount(target);
    }
貌似沒什么問題吧,一套下來就是轉賬的流程。但是實際上這樣寫是會出問題的,就是不符合事務的一致性,可能會出現加錢失敗了,減錢的事務還在繼續。例如將上面的代碼稍作改動:
	source.setMoney(source.getMoney() - money);
	int a=1/0;
    target.setMoney(target.getMoney() + money);

毫無疑問上面是會報錯的,但是這時加錢的操作就不會進行了,但是減錢的操作已經做完了,這就導致了數據的異常。
導致這一現象的原因我們可以從accountDao的代碼片段中看出來:

    public void updateAccount(Account account) {
        try {
            //name和money都是可變的,不變的是id,也就是主鍵,所以查找的時候要查id
            runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

每一次操作都是調用一次runner.update,而注意我們在bean.xml對runner的配置

    <!-- queryRunner不能是單例對象,防止多線程出現問題-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!-- 注入數據源 -->
        <constructor-arg name="ds" ref="dataSoure"></constructor-arg>
    </bean>

runner是一個多例對象,每一個調用都是一個單獨的runner,對數據庫使用的是不同的連接,所以他們是不同的事務。解決方法就是讓所有操作共用一個連接,而不是像我們在xml中設置的那樣使用多例對象。這里的方法就是使用ThreadLocal對象把Connection和當前線程綁定從而使得一個線程只有一個能控制事務的對象。
關於ThreadLocal,可以看這篇文章:https://www.cnblogs.com/jiading/articles/12337399.html
來看我們的解決方案
首先,我們新建了一個連接的工具類,用於從數據源中獲取連接,並且和線程相綁定
ConnectionUtils.java

package com.jiading.utils;

import javax.sql.DataSource;
import java.sql.Connection;

/*
連接的工具類,用於從數據源中獲取連接並實現和線程的綁定
 */
public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();//新建一個ThreadLocal對象,它存放的是Connection類型的對象
/*
設置datasource的set函數,以便於之后spring進行注入
*/
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /*
        獲取當前線程上的連接
         */
    private DataSource dataSource;
	
//通過對connection的get方法
    public Connection getThreadConnection() {
        //1. 先從ThreadLocal上獲取
        Connection conn = tl.get();
        try {
            //2. 判斷當前線程上是否有連接,沒有就創建一個
            if (conn == null) {
                //3.從數據源中獲取一個連接,並和線程綁定
                conn = dataSource.getConnection();
                //4. 存入連接
                tl.set(conn);//現在ThreadLocal就和這個Connection對象綁定了
            }
            //5. 返回當前線程上的連接
            return conn;
        }catch(Exception e){
            throw new RuntimeException();
        }
    }
    /*
    把連接和線程解綁
     */
    public void removeConnection(){
        tl.remove();
    }
}

我們還需要另外一個工具類來幫忙實現和事務相關的操作。
TransactionManager.java:(注:transaction就是事務的意思)

package com.jiading.utils;

import java.sql.Connection;
import java.sql.SQLException;

/*
和事務管理相關的工具類,它包含了開啟事務、提交事務、回滾事務和釋放連接的方法
 */
public class TransactionManager {
/*
需要用到ConnectionUtils,這里設置一個set函數以便於spring注入
*/
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    private ConnectionUtils connectionUtils;
    /*
    開啟一個連接,並且設置它的自動commit關閉
     */
    public void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
	/*
	手動提交
	*/
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
	/*
	回滾
	*/
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public void release(){
        try {
            connectionUtils.getThreadConnection().close();//將連接放回連接池
            connectionUtils.removeConnection();//解綁
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

這里的工具類只負責事務相關的操作,它默認在一個線程中通過connectionUtils.getThreadConnection()獲取的connection對象都是同一個,所以它才能如此放心地在每一個方法中都調用一遍connectionUtils.getThreadConnection()來獲取對象。而對於ConnectionUtils,它和單例對象不一樣,單例對象不能滿足多線程使用的要求;但是也和多例對象不一樣,多例對象不能區分是不同線程調用函數一個線程內多次調用。
對於Dao層,相應地方法的實現也變了:

package com.jiading.dao.impl;

import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import java.sql.SQLException;
import java.util.List;

public class AccountDaoImpl implements IAccountDao {
    public void setRunner(QueryRunner runner) {

        this.runner = runner;
    }

    private QueryRunner runner;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    private ConnectionUtils connectionUtils;//需要使用這個連接工具類

    public List<Account> findAllAccount() {
        try {
            return runner.query(connectionUtils.getThreadConnection(),"select * from account", new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public Account findAccountById(Integer accountId) {
        try {
            return runner.query(connectionUtils.getThreadConnection(),"select * from account where id=?", new BeanHandler<Account>(Account.class),accountId);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public void saveAccount(Account acc) {
        try {
            runner.update(connectionUtils.getThreadConnection(),"insert into account(name,money) values(?,?)",acc.getName(),acc.getMoney());
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public void updateAccount(Account account) {
        try {
            //name和money都是可變的,不變的是id,也就是主鍵,所以查找的時候要查id
            runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public void deleteAccount(Integer accountId) {
        try {
            runner.update(connectionUtils.getThreadConnection(),"delete from account where id=?",accountId);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public Account findAccountByName(String AccountName) {
        /*
        有唯一結果就返回
        沒有結果就返回Null
        如果結果集合超過一個就拋出異常
         */
        try {
            List<Account>accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where name=?", new BeanListHandler<Account>(Account.class), AccountName);
            if(accounts==null || accounts.size()==0){
                return null;
            }
            if(accounts.size()>1){
                throw new RuntimeException("結果集不唯一");
            }
            return accounts.get(0);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

這里有個地方要注意,因為我們是采用工具類獲取連接對象的並傳入runner了,所以不能再向runner中注入connection了,所以runner.query()的參數多了一個,相應的bean.xml中也要修改:

    <!-- queryRunner不能是單例對象,防止多線程出現問題-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    </bean>

這里要注意,Dao中並沒有用到我們寫的那個事務的工具類,因為Dao是持久層,只負責sql語句的執行,而我們所說的事務是和我們的業務相關的,它應該是在service中:

package com.jiading.service.impl;

import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import com.jiading.utils.TransactionManager;

import java.util.List;

/*
事務的控制應該是在業務層的,而不是持久層
 */
public class AccountServiceImpl implements IAccountService {
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    private IAccountDao accountDao;

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    private TransactionManager txManager;

    public List<Account> findAllAccount() {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            List<Account> allAccount = accountDao.findAllAccount();
            //3.提交事務
            txManager.commit();
            //4.返回結果
            return allAccount;
        } catch (Exception e) {
            //5. 回滾事務
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6. 釋放連接
            txManager.release();
        }

    }

    public Account findAccountById(Integer accountId) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            Account accountById = accountDao.findAccountById(accountId);
            //3.提交事務
            txManager.commit();
            //4.返回結果
            return accountById;
        } catch (Exception e) {
            //5. 回滾事務
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6. 釋放連接
            txManager.release();
        }
    }

    public void saveAccount(Account acc) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            accountDao.saveAccount(acc);
            //3.提交事務
            txManager.commit();
            //4.返回結果
        } catch (Exception e) {
            //5. 回滾事務
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6. 釋放連接
            txManager.release();
        }
    }

    public void updateAccount(Account account) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            accountDao.updateAccount(account);
            //3.提交事務
            txManager.commit();
            //4.返回結果
        } catch (Exception e) {
            //5. 回滾事務
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6. 釋放連接
            txManager.release();
        }
    }

    public void deleteAccount(Integer accountId) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            accountDao.deleteAccount(accountId);
            //3.提交事務
            txManager.commit();
            //4.返回結果
        } catch (Exception e) {
            //5. 回滾事務
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6. 釋放連接
            txManager.release();
        }
    }

    public void transfer(String sourceName, String targetName, Float money) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            Account source = accountDao.findAccountByName(sourceName);
            Account target = accountDao.findAccountByName(targetName);
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
        /*
        這樣寫是會出問題的,就是不符合事務的一致性,可能會出現加錢失敗了,減錢的事務還在繼續
        注意:一個連接對應的是一個事務。解決方法就是讓所有操作共用一個連接,而不是像我們在xml中設置的那樣使用多例對象
        解決方法:
            使用ThreadLocal對象把Connection和當前線程綁定從而使得一個線程只有一個能控制事務的對象
         */
            accountDao.updateAccount(source);
            accountDao.updateAccount(target);
            //3.提交事務
            txManager.commit();
            //4.返回結果;
        } catch (Exception e) {
            //5. 回滾事務
            txManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //6. 釋放連接
            txManager.release();
        }
    }
}

我們還需要將自己寫的工具類加入xml中以便於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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置數據源 -->
    <bean id="dataSoure" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 連接數據庫的必備信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/jd_learning"></property>
    <property name="user" value="root"></property>
    <property name="password" value="<密碼>"></property>
    </bean>

    <!-- queryRunner不能是單例對象,防止多線程出現問題-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    </bean>

    <bean id="accountDao" class="com.jiading.dao.impl.AccountDaoImpl">
        <property name="runner" ref="runner"></property>
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl">
        <!-- 注入dao對象-->
        <property name="accountDao" ref="accountDao"></property>
        <property name="txManager" ref="txManager"></property>
    </bean>
    <!-- 配置connection工具類 ConnectionUtils-->
    <bean id="connectionUtils" class="com.jiading.utils.ConnectionUtils">
        <property name="dataSource" ref="dataSoure"></property>
    </bean>
    <!-- 配置事務管理器-->
    <bean id="txManager" class="com.jiading.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

這樣我們就在允許多線程的前提下實現了數據庫的事務,但是顯然,這么寫是有一些問題的:

  • 項目變得非常地復雜
  • 重復代碼太多,特別是service中,每一個操作都要完成全套的事務相關操作
    對於這些問題,我們可以使用動態代理進行解決

動態代理

Java動態代理的實現請看這篇文章:https://www.cnblogs.com/jiading/p/12343777.html
動態代理實現
BeanFactory.java:

package com.jiading.factory;

import com.jiading.service.IAccountService;
import com.jiading.utils.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/*
用於創建service的代理對象的工廠
 */
public class BeanFactory {
    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    private IAccountService accountService;
    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    private TransactionManager txManager;
    /*
    獲取service的代理對象
     */
    public IAccountService getAccountService(){
        IAccountService accountServiceProxy=(IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /*
                添加事務的支持
                 */
                Object rtValue=null;
                try {
                    //1.開啟事務
                    txManager.beginTransaction();
                    //2.執行操作
                    rtValue = method.invoke(accountService, args);
                    //3.提交事務
                    txManager.commit();
                    //4.返回結果
                    return rtValue;
                } catch (Exception e) {
                    //5. 回滾事務
                    txManager.rollback();
                    throw new RuntimeException(e);
                } finally {
                    //6. 釋放連接
                    txManager.release();
                }
            }
        });
        return accountServiceProxy;
    }
}

這應該比較好理解,就是將service的一個方法放在一個事務中,這樣service中的transfer的各個步驟就是在一個事務中了
相應地,service中的事務控制就可以刪除了。這就使得service對象的編寫只需要考慮業務需求即可
AccountServiceImpl.java

package com.jiading.service.impl;

import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import com.jiading.utils.TransactionManager;

import java.util.List;

/*
事務的控制應該是在業務層的,而不是持久層
 */
public class AccountServiceImpl implements IAccountService {
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    private IAccountDao accountDao;

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    private TransactionManager txManager;

    @Override
    public List<Account> findAllAccount() {
        List<Account> allAccount = accountDao.findAllAccount();
        return allAccount;
    }

    @Override
    public Account findAccountById(Integer accountId) {
        Account accountById = accountDao.findAccountById(accountId);
        return accountById;

    }

    @Override
    public void saveAccount(Account acc) {
            accountDao.saveAccount(acc);
    }

    @Override
    public void updateAccount(Account account) {
            accountDao.updateAccount(account);
    }

    @Override
    public void deleteAccount(Integer accountId) {
            accountDao.deleteAccount(accountId);
    }

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        Account source = accountDao.findAccountByName(sourceName);
        Account target = accountDao.findAccountByName(targetName);
        source.setMoney(source.getMoney() - money);
        target.setMoney(target.getMoney() + money);
        accountDao.updateAccount(source);
        accountDao.updateAccount(target);
    }
}

還需要到bean.xml中配置一下
因為不需要在service中使用事務控制了,所以就不用在z其中注入事務控制器了:

    <bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl">
        <!-- 注入dao對象-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

需要配置一下代理類和beanFactory:

<!--配置代理的service-->
    <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
    <!-- 配置beanFactory-->
    <bean id="beanFactory" class="com.jiading.factory.BeanFactory">
        <property name="accountService" ref="accountService"></property>
        <property name="txManager" ref="txManager"></property>
    </bean>

這樣做的好處之前已經說過了,但是有沒有什么缺點呢?有的,就是實現和配置起來很復雜。使用接下來我們就要介紹spring中的AOP,我們之后可以使用spring框架已經實現的功能,僅需要做配置就好


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM