通過上一篇面試被問了幾百遍的 IoC 和 AOP,還在傻傻搞不清楚?我們了解了 IOC 和 AOP 這兩個思想,下面我們先不去考慮Spring是如何實(shí)現(xiàn)這兩個思想的,先通過一個銀行轉(zhuǎn)賬的案例,分析一下該案例在代碼層面存在什么問題?分析之后使用我們已有的知識來解決這些問題(痛點(diǎn))。 其實(shí)這個過程就是在一步步分析并手動實(shí)現(xiàn) IOC 和 AOP 。 案例介紹銀行轉(zhuǎn)賬:賬戶A向賬戶B轉(zhuǎn)賬(賬戶A減錢,賬戶B加錢)。為了簡單起見,在前端頁面中寫死了兩個賬戶。每次只需要輸入轉(zhuǎn)賬金額,進(jìn)行轉(zhuǎn)賬操作,驗(yàn)證功能即可。 案例表結(jié)構(gòu)name varcher 255 用戶名money int 255 賬戶金額cardNo varcher 255 銀行卡號 案例代碼調(diào)用關(guān)系核心代碼TransferServlet
TransferService TransferServiceImpl AccountDao JdbcAccountDaoImpl public class JdbcAccountDaoImpl implements AccountDao { @Override public Account queryAccountByCardNo(String cardNo) throws Exception { //從連接池獲取連接 Connection con = DruidUtils.getInstance().getConnection(); String sql = 'select * from account where cardNo=?'; PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setString(1,cardNo); ResultSet resultSet = preparedStatement.executeQuery(); Account account = new Account(); while(resultSet.next()) { account.setCardNo(resultSet.getString('cardNo')); account.setName(resultSet.getString('name')); account.setMoney(resultSet.getInt('money')); } resultSet.close(); preparedStatement.close(); con.close(); return account; } @Override public int updateAccountByCardNo(Account account) throws Exception { // 從連接池獲取連接 Connection con = DruidUtils.getInstance().getConnection(); String sql = 'update account set money=? where cardNo=?'; PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setInt(1,account.getMoney()); preparedStatement.setString(2,account.getCardNo()); int i = preparedStatement.executeUpdate(); preparedStatement.close(); con.close(); return i; }} 案例問題分析通過上面的流程分析以及簡要代碼,我們可以發(fā)現(xiàn)如下問題: 問題一: new 關(guān)鍵字將 service 層的實(shí)現(xiàn)類 TransferServiceImpl 和 Dao 層的具體實(shí)現(xiàn)類 JdbcAccountDaoImpl 耦合在了一起,當(dāng)需要切換Dao層實(shí)現(xiàn)類的時候必須要修改 service 的代碼、重新編譯,這樣不符合面向接口開發(fā)的最優(yōu)原則。 問題二: service 層沒有事務(wù)控制,如果轉(zhuǎn)賬過程中出現(xiàn)異常可能會導(dǎo)致數(shù)據(jù)錯亂,后果很嚴(yán)重,尤其是在金融銀行領(lǐng)域。 問題解決思路new關(guān)鍵字耦合問題解決方案實(shí)例化對象的方式處理new之外,還有什么技術(shù)? 答:反射(將類的權(quán)限定類名配置在xml文件中) 項(xiàng)目中往往有很多對象需要實(shí)例化,考慮使用工程模式通過反射來實(shí)例化對象。(工廠模式是解耦合非常好的一種方式) 代碼中能否只聲明所需實(shí)例的接口類型,不出現(xiàn)new關(guān)鍵字,也不出現(xiàn)工廠類的字眼? 答:可以,聲明一個變量并提供一個set方法,在反射的時候?qū)⑺枰膶ο笞⑷脒M(jìn)去。 new關(guān)鍵字耦合問題代碼改造首先定義 bean.xml 文件 定義BeanFactory BeanFactory
對象的實(shí)例化工作交給BeanFactory來進(jìn)行之后,我們再具體使用是就可以像如下這樣了: 事務(wù)控制問題分析在轉(zhuǎn)賬的業(yè)務(wù)代碼中手動模擬轉(zhuǎn)賬異常,來驗(yàn)證一下。在兩個賬戶的轉(zhuǎn)入和轉(zhuǎn)出之間模擬一個分母為0的異常。 accountDao.updateAccountByCardNo(to);int i = 1/0;accountDao.updateAccountByCardNo(from); 然后啟動程序,點(diǎn)擊轉(zhuǎn)賬(李大雷 向 韓梅梅轉(zhuǎn) 100 ¥)之后,會出現(xiàn)如下錯誤。 這時我們再查看數(shù)據(jù)庫 發(fā)現(xiàn) 韓梅梅 的賬戶增加了100¥,但是李大雷的賬戶并沒有減少(兩個賬戶原本都有10000¥)。 出現(xiàn)這個問題的原因就是因?yàn)镾ervice層沒有事務(wù)控制的功能,在轉(zhuǎn)賬過程中出現(xiàn)錯誤(轉(zhuǎn)入和轉(zhuǎn)出之間出現(xiàn)異常,轉(zhuǎn)入已經(jīng)完成,轉(zhuǎn)出沒有進(jìn)行)這事就會造成上面的問題。 數(shù)據(jù)庫的事務(wù)問題歸根結(jié)底是 Connection 的事務(wù)
在上面銀行轉(zhuǎn)賬的案例中,兩次update操作使用的是兩個數(shù)據(jù)庫連接,這樣的話,肯定就不屬于同一個事務(wù)控制了。 解決思路: 通過上面的分析,我們得出問題的原因是兩次update使用了兩個不同的connection連接。那么要想解決這個問題,我們就需要讓兩次update使用同一個connection連接 兩次update屬于同一個線程內(nèi)的執(zhí)行調(diào)用,我們可以給當(dāng)前線程綁定一個Connection,和當(dāng)前線程有關(guān)系的數(shù)據(jù)庫操作都去使用這個connection(從當(dāng)前線程中獲取,第一次使用連接,發(fā)現(xiàn)當(dāng)前線程沒有,就從連接池獲取一個連接綁定到當(dāng)前線程) 另一方面,目前事務(wù)控制是在Dao層進(jìn)行的(connection),我們需要將事務(wù)控制提到service層(service層才是具體執(zhí)行業(yè)務(wù)邏輯的地方,這里可能會調(diào)用多個dao層的方法,我們需要對service層的方法進(jìn)行整體的事務(wù)控制)。 有了上面兩個思路,下面我們進(jìn)行代碼修改。 事務(wù)控制代碼修改增加 ConnectionUtils 工具類 ConnectionUtils 增加 TransactionManager 事務(wù)管理類 TransactionManager 增加代理工廠 ProxyFactory ProxyFactory
修改beans.xml文件 beans 修改 JdbcAccountDaoImpl的實(shí)現(xiàn) JdbcAccountDaoImpl public class JdbcAccountDaoImpl implements AccountDao { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } @Override public Account queryAccountByCardNo(String cardNo) throws Exception { //從連接池獲取連接// Connection con = DruidUtils.getInstance().getConnection(); // 改造為:從當(dāng)前線程當(dāng)中獲取綁定的connection連接 Connection con = connectionUtils.getCurrentThreadConn(); String sql = 'select * from account where cardNo=?'; PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setString(1,cardNo); ResultSet resultSet = preparedStatement.executeQuery(); Account account = new Account(); while(resultSet.next()) { account.setCardNo(resultSet.getString('cardNo')); account.setName(resultSet.getString('name')); account.setMoney(resultSet.getInt('money')); } resultSet.close(); preparedStatement.close();// con.close(); return account; } @Override public int updateAccountByCardNo(Account account) throws Exception { // 從連接池獲取連接// Connection con = DruidUtils.getInstance().getConnection(); // 改造為:從當(dāng)前線程當(dāng)中獲取綁定的connection連接 Connection con = connectionUtils.getCurrentThreadConn(); String sql = 'update account set money=? where cardNo=?'; PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setInt(1,account.getMoney()); preparedStatement.setString(2,account.getCardNo()); int i = preparedStatement.executeUpdate(); preparedStatement.close();// con.close(); return i; }} 修改 TransferServlet TransferServlet
改造完之后,我們再次進(jìn)行測試,這時會發(fā)現(xiàn)當(dāng)轉(zhuǎn)賬過程中出現(xiàn)錯誤時,事務(wù)能夠成功的被控制住(轉(zhuǎn)出賬戶不會少錢,轉(zhuǎn)入賬戶不會多錢)。 為什么要使用代理的方式來實(shí)現(xiàn)事務(wù)控制?這里我們可以考慮一個問題,為什么要使用代理的方式來實(shí)現(xiàn)事務(wù)控制? 如果沒有使用代理的方式,我們要向?qū)崿F(xiàn)事務(wù)控制這需要將,事務(wù)控制的相關(guān)代碼寫在service層的TransferServiceImpl 具體實(shí)現(xiàn)中。 public class TransferServiceImpl implements TransferService { // 最佳狀態(tài) private AccountDao accountDao; // 構(gòu)造函數(shù)傳值/set方法傳值 public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } @Override public void transfer(String fromCardNo, String toCardNo, int money) throws Exception { try{ // 開啟事務(wù)(關(guān)閉事務(wù)的自動提交) TransactionManager.getInstance().beginTransaction();*/ Account from = accountDao.queryAccountByCardNo(fromCardNo); Account to = accountDao.queryAccountByCardNo(toCardNo); from.setMoney(from.getMoney()-money); to.setMoney(to.getMoney()+money); accountDao.updateAccountByCardNo(to); // 模擬異常 int c = 1/0; accountDao.updateAccountByCardNo(from); // 提交事務(wù) TransactionManager.getInstance().commit(); }catch (Exception e) { e.printStackTrace(); // 回滾事務(wù) TransactionManager.getInstance().rollback(); // 拋出異常便于上層servlet捕獲 throw e; } }} 這樣的話,事務(wù)控制和具體的業(yè)務(wù)代碼就耦合在了一起,如果有多個方法都需要實(shí)現(xiàn)事務(wù)控制的功能,我們需要在每個業(yè)務(wù)方法是都添加上這些代碼。這樣將會出現(xiàn)大量的重復(fù)代碼。所以這里使用了 AOP 的思想通過動態(tài)代理的方式實(shí)現(xiàn)了事務(wù)控制。 |
|