《Mybatis 手擼專欄》第2章:建立簡單的對映器代理工廠

小傅哥發表於2022-04-05

作者:小傅哥
部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

著急和快,是最大的障礙!

慢下來,慢下來,只有慢下來,你才能看到更全的資訊,才能學到更紮實的技術。而那些滿足你快的短篇內容雖然有時候更抓眼球,但也容易把人在技術學習上帶偏,總想著越快越好。

在小傅哥編寫技術文章的過程中,也會遇到這樣的情況,不少讀者更喜歡看;一個系列內容的開頭、一段成長故事的分享、一天成為架構的祕籍。當然我也能理解這種喜歡,畢竟大多數人都喜歡走捷徑,就像冬天買了運動健身裝備,夏天過去了還沒有拆封。

好了,接下來我們們幹正事!

二、目標

在你能閱讀這篇文章之時,我相信你已經是一個 Mybatis ORM 框架工具使用的熟練工了,那你是否清楚這個 ORM 框架是怎麼遮蔽我們對資料庫操作的細節的?

比如我們使用 JDBC 的時候,需要手動建立資料庫連結、編碼 SQL 語句、執行資料庫操作、自己封裝返回結果等。但在使用 ORM 框架後,只需要通過簡單配置即可對定義的 DAO 介面進行資料庫的操作了。

那麼本章節我們就來解決 ORM 框架第一個關聯物件介面和對映類的問題,把 DAO 介面使用代理類,包裝對映操作。

三、設計

通常如果能找到大家所在事情的共性內容,具有統一的流程處理,那麼它就是可以被凝聚和提煉的,做成通用的元件或者服務,被所有人進行使用,減少重複的人力投入。

而參考我們最開始使用 JDBC 的方式,從連線、查詢、封裝、返回,其實都一個固定的流程,那麼這個過程就可以被提煉以及封裝和補全大家所需要的功能。

當我們來設計一個 ORM 框架的過程中,首先要考慮怎麼把使用者定義的資料庫操作介面、xml配置的SQL語句、資料庫三者聯絡起來。其實最適合的操作就是使用代理的方式進行處理,因為代理可以封裝一個複雜的流程為介面物件的實現類,設計如圖 2-1:

圖 2-1 代理類設計

  • 首先提供一個對映器的代理實現類 MapperProxy,通過代理類包裝對資料庫的操作,目前我們本章節會先提供一個簡單的包裝,模擬對資料庫的呼叫。
  • 之後對 MapperProxy 代理類,提供工廠例項化操作 MapperProxyFactory#newInstance,為每個 IDAO 介面生成代理類。這塊其實用到的就是一個簡單工廠模式

接下來我們就按照這個設計實現一個簡單的對映器代理操作,編碼過程比較簡單。如果對代理知識不熟悉可以先補充下。

四、實現

1. 工程結構

mybatis-step-01
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis.binding
    │           ├── MapperProxy.java
    │           └── MapperProxyFactory.java
    └── test
        └── java
            └── cn.bugstack.mybatis.test.dao
                ├── dao
                │   └── IUserDao.java
                └── ApiTest.java

工程原始碼:https://t.zsxq.com/bmqNFQ7

Mybatis 對映器代理類關係,如圖 2-2

如圖 2-2 代理類關係圖

  • 目前這個 Mybatis 框架的代理操作實現的還只是最核心的功能,相當於是光屁股的娃娃,還沒有新增衣服。不過這樣漸進式的實現可以讓大家先了解到最核心的內容,後續我們在陸續的完善。

    • MapperProxy 負責實現 InvocationHandler 介面的 invoke 方法,最終所有的實際呼叫都會呼叫到這個方法包裝的邏輯。
    • MapperProxyFactory 是對 MapperProxy 的包裝,對外提供例項化物件的操作。當我們後面開始給每個運算元據庫的介面對映器註冊代理的時候,就需要使用到這個工廠類了。

2. 對映器代理類

原始碼詳見cn.bugstack.mybatis.binding.MapperProxy

public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;

    private Map<String, String> sqlSession;
    private final Class<T> mapperInterface;

    public MapperProxy(Map<String, String> sqlSession, Class<T> mapperInterface) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            return "你的被代理了!" + sqlSession.get(mapperInterface.getName() + "." + method.getName());
        }
    }

}
  • 通過實現 InvocationHandler#invoke 代理類介面,封裝操作邏輯的方式,對外介面提供資料庫操作物件。
  • 目前我們這裡只是簡單的封裝了一個 sqlSession 的 Map 物件,你可以想象成所有的資料庫語句操作,都是通過介面名稱+方法名稱作為key,操作作為邏輯的方式進行使用的。那麼在反射呼叫中則獲取對應的操作直接執行並返回結果即可。當然這還只是最核心的簡化流程,後續不斷補充內容後,會看到對資料庫的操作
  • 另外這裡要注意如果是 Object 提供的 toString、hashCode 等方法是不需要代理執行的,所以新增 Object.class.equals(method.getDeclaringClass()) 判斷。

3. 代理類工廠

原始碼詳見cn.bugstack.mybatis.binding.MapperProxyFactory

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public T newInstance(Map<String, String> sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }

}
  • 工廠操作相當於把代理的建立給封裝起來了,如果不做這層封裝,那麼每一個建立代理類的操作,都需要自己使用 Proxy.newProxyInstance 進行處理,那麼這樣的操作方式就顯得比較麻煩了。
  • 另外如果你對代理不是太熟悉,可以著重把 JDK Proxy 的內容做幾個案例補充下這塊的內容。

五、測試

1. 事先準備

cn.bugstack.mybatis.test.dao.IUserDao

public interface IUserDao {

    String queryUserName(String uId);

    Integer queryUserAge(String uId);

}
  • 首先提供一個 DAO 介面,並定義2個介面方法。

2. 測試用例

@Test
public void test_MapperProxyFactory() {
    MapperProxyFactory<IUserDao> factory = new MapperProxyFactory<>(IUserDao.class);
    Map<String, String> sqlSession = new HashMap<>();

    sqlSession.put("cn.bugstack.mybatis.test.dao.IUserDao.queryUserName", "模擬執行 Mapper.xml 中 SQL 語句的操作:查詢使用者姓名");
    sqlSession.put("cn.bugstack.mybatis.test.dao.IUserDao.queryUserAge", "模擬執行 Mapper.xml 中 SQL 語句的操作:查詢使用者年齡");
    IUserDao userDao = factory.newInstance(sqlSession);

    String res = userDao.queryUserName("10001");
    logger.info("測試結果:{}", res);
}
  • 在單測中建立 MapperProxyFactory 工廠,並手動給 sqlSession Map 賦值,這裡的賦值相當於模擬資料庫中的操作。
  • 接下來再把賦值資訊傳遞給代理物件例項化操作,這樣就可以在我們呼叫具體的 DAO 方法時從 sqlSession 中取值了。

測試結果

17:03:41.817 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:你的被代理了!模擬執行 Mapper.xml 中 SQL 語句的操作:查詢使用者姓名

Process finished with exit code 0
  • 從測試結果可以看到的,我們的介面已經被代理類實現了,同時我們可以在代理類中進行自己的操作封裝。那麼在我們後續實現的資料庫操作中,就可以對這部分內容進行擴充套件了。

六、總結

  • 本章節我們初步對 Mybatis 框架中的資料庫 DAO 操作介面和對映器通過代理類的方式進行連結,這一步也是 ORM 框架裡非常核心的部分。有了這塊的內容,就可以在代理類中進行自己邏輯的擴充套件了。
  • 在框架實現方面引入簡單工廠模式包裝代理類,遮蔽建立細節,這些也是大家在學習過程中需要注意的設計模式的點。
  • 目前內容還比較簡單的,可以手動操作練習,隨著我們內容的增加,會有越來越多的包和類引入,完善 ORM 框架功能。

七、系列推薦

相關文章