手把手帶你實現 Spring 中的 IoC 和 AOP

一、核心思想

IoCAOP不是spring提出來的,在spring之前就已經存在,只不過更偏向理論化,spring在技術層面把這兩個思想做了非常好的實現。在手寫spring中的IoCAOP之前,我們先來了解IoCAOP的思想。

1、IoC

1.1 什麼是 IoC?

IoC Inversion of Control(控制反轉、反轉控制),注意它是一個技術思想,不是技術實現。

描述的事情:Java開發領域對象的創建、管理的問題。

傳統開發模式:比如類 A 依賴類 B,往往會在類 A 中new一個 B 的對象。

IoC思想下開發模式:我們不再自己去new對象了,而是由IoC容器 (Spring框架) 去幫助我們實例化對象並且管理它,我們需要哪個對象,去問IoC容器要即可。

我們喪失了一個權力 (創建、管理對象的權力),得到一個福利 (不用考慮對象的創建、管理等一系列事情)。

爲什麼叫控制反轉?

控制:指的是對象創建 (實例化、管理) 的權力。
反轉:控制權交給外部環境了 (Spring框架、IoC容器)。


1.2 IoC 解決了什麼問題

IoC 解決對象之家的耦合問題


1.3 IoC 和 DI 的區別

DIDependency Injection(依賴注入)

如何理解:

IoC 和 DI 描述的是同一件事情,只不過角度不一樣罷了。

2、AOP

2.1 什麼是 AOP?

AOPAspect Oriented Programming 面向切面編程 / 面向方面編程

AOPOOP的延續,我們從OOP說起

OOP三大特徵:封裝、繼承、多態

OOP是一種垂直繼承體系:

OOP編程思想可以解決大多數的代碼重複問題,但是有一些情況是處理不了的,比如下面的在頂級父類Animal中的多個方法中相同位置出現了重複代碼,OOP就解決不了了。

橫切邏輯代碼:

橫切邏輯代碼存在什麼問題:

AOP出場,AOP獨闢蹊徑提出橫向抽取機制,將橫切邏輯代碼和業務邏輯代碼分析:

代碼拆分容易,那麼如何在不改變原有業務邏輯的情況下,悄無聲息的把橫切邏輯代碼應用到原有的業 務邏輯中,達到和原來一樣的效果,這個是比較難的。

2.2 AOP 解決的什麼問題

在不改變原有業務邏輯情況下,增強橫切邏輯代碼,根本上解耦合,避免橫切邏輯代碼重複。

2.3 爲什麼叫面向切面編程

「切」: 指的是橫切邏輯,原有業務邏輯代碼我們不能動,只能操作橫切邏輯代碼,所以面向橫切邏輯。

「面」: 橫切邏輯代碼往往要影響的是很多個方法,每一個方法都如同一個點,多個點構成面,有一個 面的概念在裏面。

二、手寫實現 IoC 和 AOP

上面我們理解了IoCAOP的思想。我們先不考慮 Spring 是如何實現這兩個思想的,我這裏準備了一 個『銀行轉賬』的案例,** 請分析該案例在代碼層次有什麼問題?** 分析之後使用我們已有知識解決這些問題 (痛點)。其實這個過程我們就是在一步步分析並手寫實現 IoC 和 AOP。

1、銀行轉賬案例界面

2、銀行轉賬案例表結構

3、銀行轉賬案例代碼調用關係

4、銀行轉賬案例關鍵代碼

TransferServlet

@WebServlet()
public class TransferServlet extends HttpServlet {

    // 1. 實例化service層對象
    private TransferService transferService = new TransferServiceImpl();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 設置請求體的字符編碼
        req.setCharacterEncoding("UTF-8");

        String fromCardNo = req.getParameter("fromCardNo");
        String toCardNo = req.getParameter("toCardNo");
        String moneyStr = req.getParameter("money");
        int money = Integer.parseInt(moneyStr);

        Result result = new Result();

        try {
            // 2. 調用service層方法
            transferService.transfer(fromCardNo,toCardNo,money);
            result.setStatus("200");
        } catch (Exception e) {
            e.printStackTrace();
            result.setStatus("201");
            result.setMessage(e.toString());
        }

        // 響應
        resp.setContentType("application/json;charset=utf-8");
        resp.getWriter().print(JsonUtil.object2Json(result));
    }
}

TransferService 接口及實現類

public interface TransferService {

    void transfer(String fromCardNo, String toCardNo, int money) throws Exception;

}
public class TransferServiceImpl implements TransferService {

    private AccountDao accountDao = new JdbcAccountDaoImpl();

    @Override
    public void transfer(String fromCardNo, String toCardNo, int money) throws Exception {
        Account from = accountDao.queryAccountByCardNo(fromCardNo);
        Account to = accountDao.queryAccountByCardNo(toCardNo);

        from.setMoney(from.getMoney() - money);
        to.setMoney(to.getMoney() + money);

        accountDao.updateAccountByCardNo(to);
        accountDao.updateAccountByCardNo(from);
    }

}

AccountDao 層接口及基於 Jdbc 的實現類

public interface AccountDao {

    Account queryAccountByCardNo(String cardNo) throws Exception;

    int updateAccountByCardNo(Account account) throws Exception;

}
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;
    }
}

5、銀行轉賬案例代碼問題分析

(1)、問題一: 在上述案例實現中,service 層實現類在使用 dao 層對象時,直接在 TransferServiceImpl 中通過 AccountDao accountDao = new JdbcAccountDaoImpl() 獲得了 dao層對 象,然而一個 new 關鍵字卻將 TransferServiceImpl 和 dao 層具體的一個實現類 JdbcAccountDaoImpl 耦合在了一起,如果說技術架構發生一些變動,dao 層的實現要使用其它技術, 比如 Mybatis,思考切換起來的成本? 每一個 new 的地方都需要修改源代碼,重新編譯,面向接口開發的意義將大打折扣?

(2)、問題二: service 層代碼沒有竟然還沒有進行事務控制?! 如果轉賬過程中出現異常,將可能導致數據庫數據錯亂,後果可能會很嚴重,尤其在金融業務。

6、 問題解決思路

針對問題一思考:

針對問題二思考:

7、案例代碼改造

(1)、針對問題一的代碼改造

public class TransferServiceImpl implements TransferService {

    private AccountDao accountDao;

    // 構造函數傳值/set方法傳值
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    ...
}

(2)、針對問題二的代碼改造

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AU_tbAso4HGyw5ASvWvGvw