《手寫Mybatis》第4章:Mapper XML的解析和註冊使用

小傅哥發表於2022-04-11

作者:小傅哥

系列:https://bugstack.cn/md/spring/develop-mybatis/2022-03-20-%E7%AC%AC1%E7%AB%A0%EF%BC%9A%E5%BC%80%E7%AF%87%E4%BB%8B%E7%BB%8D%EF%BC%8C%E6%89%8B%E5%86%99Mybatis%E8%83%BD%E7%BB%99%E4%BD%A0%E5%B8%A6%E6%9D%A5%E4%BB%80%E4%B9%88%EF%BC%9F.html

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

一、前言

你是怎麼面對功能迭代的?

其實很多程式設計師在剛開始做程式設計或者新加入一家公司時,都沒有多少機會可以做一個新專案,大部分時候都是在老專案上不斷的迭代更新。在這個過程你可能要學習N個前人留下的各式各樣的風格迥異的程式碼片段,在這些縱橫交錯的流程中,找到一席之地,把自己的ifelse加進去。

雖然這樣胡亂的加ifelse,剛上手就“擺爛”的心態,讓人很難受。但要想在已經被壓縮的工期下,還能交付出高質量的程式碼其實也很難完成,所以一部分研發被逼到能用就行,能跑就可以。

但說回來,其實不能逐步清理一片屎山,讓程式碼在你的手上逐步清晰、整潔、乾淨,很多時候也是作為碼農自身經驗的不足,不懂得系統重構、不瞭解設計原則、不熟悉業務背景、不清楚產品走向等等原因造成的。所以最好的辦法是提升自身的能力,沒接到一次需求都有一些技術上的改變,既然它是屎山,那就當做打怪升級了,修一點、改一塊、補一片,總會在你手上越來越易於維護和擴充套件的。

二、目標

在我們漸進式的逐步實現 Mybatis 框架過程中,首先我們要有一個目標導向的思路,也就是說 Mybatis 的核心邏輯怎麼實現。

其實我們可以把這樣一個 ORM 框架的目標,簡單的描述成是為了給一個介面提供代理類,類中包括了對 Mapper 也就是 xml 檔案中的 SQL 資訊(型別入參出參條件)進行解析和處理,這個處理過程就是對資料庫的操作以及返回對應的結果給到介面。如圖 4-1

圖 4-1 ORM 框架核心流程

那麼按照 ORM 核心流程的執行過程,我們本章節就需要在上一章節的基礎上,繼續擴充套件對 Mapper 檔案的解析以及提取出對應的 SQL 檔案。並在當前這個階段,可以滿足我們呼叫 DAO 介面方法的時候,可以返回 Mapper 中對應的待執行 SQL 語句。為了不至於把整個工程撐大,小傅哥會帶著大家逐步完成這些內容,所以本章節暫時不會對資料庫進行操作,待後續逐步實現

三、設計

結合上一章節我們使用了 MapperRegistry 對包路徑進行掃描註冊對映器,並在 DefaultSqlSession 中進行使用。那麼在我們可以把這些名稱空間、SQL描述、對映資訊統一維護到每一個 DAO 對應的 Mapper XML 的檔案以後,其實 XML 就是我們的源頭了。通過對 XML 檔案的解析和處理就可以完成 Mapper 對映器的註冊和 SQL 管理。這樣也就更加我們操作和使用了。如圖 4-2

圖 4-2 XML 檔案解析註冊處理

  • 首先需要定義 SqlSessionFactoryBuilder 工廠建造者模式類,通過入口 IO 的方式對 XML 檔案進行解析。當前我們主要以解析 SQL 部分為主,並註冊對映器,串聯出整個核心流程的脈絡。
  • 檔案解析以後會存放到 Configuration 配置類中,接下來你會看到這個配置類會被串聯到整個 Mybatis 流程中,所有內容存放和讀取都離不開這個類。如我們在 DefaultSqlSession 中獲取 Mapper 和執行 selectOne 也同樣是需要在 Configuration 配置類中進行讀取操作。

四、實現

1. 工程結構

mybatis-step-03
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           │   ├── xml
    │           │   │   └── XMLConfigBuilder.java
    │           │   └── BaseBuilder.java
    │           ├── io
    │           │   └── Resources.java
    │           ├── mapping
    │           │   ├── MappedStatement.java
    │           │   └── SqlCommandType.java
    │           └── session
    │               ├── defaults
    │               │   ├── DefaultSqlSession.java
    │               │   └── DefaultSqlSessionFactory.java
    │               ├── Configuration.java
    │               ├── SqlSession.java
    │               ├── SqlSessionFactory.java
    │               └── SqlSessionFactoryBuilder.java
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml

XML 解析和註冊類實現關係,如圖 4-2

圖 4-2 XML 解析和註冊類實現關係

  • SqlSessionFactoryBuilder 作為整個 Mybatis 的入口,提供建造者工廠,包裝 XML 解析處理,並返回對應 SqlSessionFactory 處理類。
  • 通過解析把 XML 資訊註冊到 Configuration 配置類中,再通過傳遞 Configuration 配置類到各個邏輯處理類裡,包括 DefaultSqlSession 中,這樣就可以在獲取對映器和執行SQL的時候,從配置類中拿到對應的內容了。

2. 構建SqlSessionFactory建造者工廠

原始碼詳見cn.bugstack.mybatis.session.SqlSessionFactoryBuilder

public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(Reader reader) {
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
        return build(xmlConfigBuilder.parse());
    }

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }

}
  • SqlSessionFactoryBuilder 是作為整個 Mybatis 的入口類,通過指定解析XML的IO,引導整個流程的啟動。
  • 從這個類開始新增加了 XMLConfigBuilder、Configuration 兩個處理類,分別用於解析 XML 和串聯整個流程的物件儲存操作。接下來我們會分別介紹這些新引入的物件。

3. XML 解析處理

原始碼詳見cn.bugstack.mybatis.builder.xml.XMLConfigBuilder

public class XMLConfigBuilder extends BaseBuilder {

    private Element root;

    public XMLConfigBuilder(Reader reader) {
        // 1. 呼叫父類初始化Configuration
        super(new Configuration());
        // 2. dom4j 處理 xml
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new InputSource(reader));
            root = document.getRootElement();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    public Configuration parse() {
        try {
            // 解析對映器
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }

    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            		// 解析處理,具體參照原始碼
            		
                // 新增解析 SQL
                configuration.addMappedStatement(mappedStatement);
            }

            // 註冊Mapper對映器
            configuration.addMapper(Resources.classForName(namespace));
        }
    }
    
}
  • XMLConfigBuilder 核心操作在於初始化 Configuration,因為 Configuration 的使用離解析 XML 和存放是最近的操作,所以放在這裡比較適合。
  • 之後就是具體的 parse() 解析操作,並把解析後的資訊,通過 Configuration 配置類進行存放,包括:新增解析 SQL、註冊Mapper對映器。
  • 解析配置整體包括:型別別名、外掛、物件工廠、物件包裝工廠、設定、環境、型別轉換、對映器,但目前我們還不需要那麼多,所以只做一些必要的 SQL 解析處理。

4. 通過配置類包裝序號產生器和SQL語句

原始碼詳見(配置項)cn.bugstack.mybatis.session.Configuration

public class Configuration {

    /**
     * 對映序號產生器
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    /**
     * 對映的語句,存在Map裡
     */
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }
}

在配置類中新增對映器序號產生器和對映語句的存放;

  • 對映器序號產生器是我們上一章節實現的內容,用於註冊 Mapper 對映器鎖提供的操作類。
  • 另外一個 MappedStatement 是本章節新新增的 SQL 資訊記錄物件,包括記錄:SQL型別、SQL語句、入參型別、出參型別等。詳細可參照原始碼

5. DefaultSqlSession結合配置項獲取資訊

原始碼詳見cn.bugstack.mybatis.session.defaults.DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        MappedStatement mappedStatement = configuration.getMappedStatement(statement);
        return (T) ("你被代理了!" + "\n方法:" + statement + "\n入參:" + parameter + "\n待執行SQL:" + mappedStatement.getSql());
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.getMapper(type, this);
    }

}
  • DefaultSqlSession 相對於上一章節,小傅哥這裡把 MapperRegistry mapperRegistry 替換為 Configuration configuration,這樣才能傳遞更豐富的資訊內容,而不只是註冊器操作。
  • 之後在 DefaultSqlSession#selectOne、DefaultSqlSession#getMapper 兩個方法中都使用 configuration 來獲取對應的資訊。
  • 目前 selectOne 方法中只是把獲取的資訊進行列印,後續將引入 SQL 執行器進行結果查詢並返回。

五、測試

1. 事先準備

提供 DAO 介面和對應的 Mapper xml 配置

public interface IUserDao {

    String queryUserInfoById(String uId);

}
<mapper namespace="cn.bugstack.mybatis.test.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
        SELECT id, userId, userHead, createTime
        FROM user
        where id = #{id}
    </select>

</mapper>

2. 單元測試

@Test
public void test_SqlSessionFactory() throws IOException {
    // 1. 從SqlSessionFactory中獲取SqlSession
    Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 2. 獲取對映器物件
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 3. 測試驗證
    String res = userDao.queryUserInfoById("10001");
    logger.info("測試結果:{}", res);
}
  • 目前的使用方式就和 Mybatis 非常像了,通過載入 xml 配置檔案,交給 SqlSessionFactoryBuilder 進行構建解析,並獲取 SqlSessionFactory 工廠。這樣就可以順利的開啟 Session 以及完成後續的操作。

測試結果

07:07:40.519 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:你被代理了!
方法:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById
入參:[Ljava.lang.Object;@23223dd8
待執行SQL:
        SELECT id, userId, userHead, createTime
        FROM user
        where id = ?
    

Process finished with exit code 0
  • 從測試結果我們可以看到,目前的代理操作已經可以把我們從 XML 中解析的 SQL 資訊進行列印了,後續我們將結合這部分的處理繼續完成資料庫的操作。

六、總結

  • 瞭解 ORM 處理的核心流程,知曉目前我們所處在的步驟和要完成的內容,只有非常清楚的知道這個代理、封裝、解析和返回結果的過程才能更好的完成整個框架的實現。
  • SqlSessionFactoryBuilder 的引入包裝了整個執行過程,包括:XML 檔案的解析、Configuration 配置類的處理,讓 DefaultSqlSession 可以更加靈活的拿到對應的資訊,獲取 Mapper 和 SQL 語句。
  • 另外從整個工程搭建的過程中,可以看到有很多工廠模式、建造者模式、代理模式的使用,也有很多設計原則的運用,這些技巧都可以讓整個工程變得易於維護和易於迭代。這也是研發人員在學習原始碼的過程中,非常值得重點關注的地方。

相關文章