天天看點

《Mybatis 手撸專欄》第9章:細化XML語句建構器,完善靜态SQL解析

《Mybatis 手撸專欄》第9章:細化XML語句建構器,完善靜态SQL解析

作者:小傅哥

部落格:https://bugstack.cn

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

一、前言

你隻是在解釋過程,而他是在闡述高度!

如果不是長時間的沉澱、積累和儲備,我一定也沒有辦法用更多的次元和更多的視角來對一個問題進行多方面闡述。就像你我;越過峭壁山川,才知枕席還師的通達平坦。領略過雷聲千嶂落,雨色萬峰來,才聞到八表流雲澄夜色,九霄華月動春城的甯靜。

是以引申到程式設計開發,往簡單了說就是寫寫代碼,改改bug。但如果就局限在隻是寫寫代碼,其實很難領略到那些衆多設計思想和複雜問題中,庖丁解牛般的酣暢淋漓。而這些酣暢的體驗,都需要你對技術的拓展學習和深度探索,從衆多的優秀源碼架構中吸收經驗。反複揣摩、反複嘗試,終有那麼一個時間點,你會有種悟了的感覺。而這些一個個感覺的積累,就能幫助你以後在面試、述職、答辯、分享、彙報等場景中,說出更有深度的技術思想和類比設計對照,站在更高的角度俯視業務場景的走向和給出長遠的架構方案。

二、目标

實作到本章節前,關于 Mybatis ORM 架構的大部分核心結構已經逐漸展現出來了,包括;解析、綁定、映射、事務、執行、資料源等。但随着更多功能的逐漸完善,我們需要對子產品内的實作進行細化處理,而不單單隻是完成功能邏輯。這就有點像把 CRUD 使用設計原則進行拆分解耦,滿足代碼的易維護和可擴充性。而這裡我們首先着手要處理的就是關于 XML 解析的問題,把之前粗糙的實作進行細化,滿足我們對解析時一些參數的整合和處理。

《Mybatis 手撸專欄》第9章:細化XML語句建構器,完善靜态SQL解析
  • 這一部分的解析,就是在我們本章節之前的 XMLConfigBuilder#mapperElement 方法中的操作。看上去雖然能實作功能,但總會讓人感覺它不夠規整。就像我們平常開發的 CRUD 羅列到一塊的邏輯一樣,什麼流程都能處理,但什麼流程都會越來越混亂。
  • 就像我們在 ORM 架構 DefaultSqlSession 中調用具體執行資料庫操作的方法,需要進行 PreparedStatementHandler#parameterize 參數時,其實并沒有準确的定位到參數的類型,jdbcType和javaType的轉換關系,是以後續的屬性填充就會顯得比較混亂且不易于擴充。當然,如果你硬寫也是寫的出來的,不過這種就不是一個好的設計!
  • 是以接下來小傅哥會帶着讀者,把這部分解析的處理,使用設計原則将流程和職責進行解耦,并結合我們的目前訴求,優先處理靜态 SQL 内容。待架構結構逐漸完善,再進行一些動态SQL和更多參數類型的處理,滿足讀者以後在閱讀 Mybatis 源碼,以及需要開發自己的 X-ORM 架構的時候,有一些經驗積累。

三、設計

參照設計原則,對于 XML 資訊的讀取,各個功能子產品的流程上應該符合單一職責,而每一個具體的實作又得具備迪米特法則,這樣實作出來的功能才能具有良好的擴充性。通常這類代碼也會看着很幹淨 那麼基于這樣的訴求,我們則需要給解析過程中,所屬解析的不同内容,按照各自的職責類進行拆解和串聯調用。整體設計如圖 9-2

《Mybatis 手撸專欄》第9章:細化XML語句建構器,完善靜态SQL解析
  • 與之前的解析代碼相對照,不在是把所有的解析都在一個循環中處理,而是在整個解析過程中,引入 XMLMapperBuilder、XMLStatementBuilder 分别處理

    映射建構器

    語句建構器

    ,按照不同的職責分别進行解析。
  • 與此同時也在語句建構器中,引入腳本語言驅動器,預設實作的是 XML語言驅動器 XMLLanguageDriver,這個類來具體操作靜态和動态 SQL 語句節點的解析。這部分的解析處理實作方式很多,即使自己使用正則或者 String 截取也是可以的。是以為了保持與 Mybatis 的統一,我們直接參照源碼 Ognl 的方式進行處理。對應的類是 DynamicContext
  • 這裡所有的解析鋪墊,通過解耦的方式實作,都是為了後續在 executor 執行器中,更加友善的處理 setParameters 參數的設定。後面參數的設定,也會涉及到前面我們實作的元對象反射工具類的使用。

四、實作

1. 工程結構

mybatis-step-08
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           ├── builder
    │           │   ├── xml
    │           │   │   ├── XMLConfigBuilder.java
    │           │   │   ├── XMLMapperBuilder.java
    │           │   │   └── XMLStatementBuilder.java
    │           │   ├── BaseBuilder.java
    │           │   ├── ParameterExpression.java
    │           │   ├── SqlSourceBuilder.java
    │           │   └── StaticSqlSource.java
    │           ├── datasource
    │           ├── executor
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   ├── statement
    │           │   │   ├── BaseStatementHandler.java
    │           │   │   ├── PreparedStatementHandler.java
    │           │   │   ├── SimpleStatementHandler.java
    │           │   │   └── StatementHandler.java
    │           │   ├── BaseExecutor.java
    │           │   ├── Executor.java
    │           │   └── SimpleExecutor.java
    │           ├── io
    │           ├── mapping
    │           │   ├── BoundSql.java
    │           │   ├── Environment.java
    │           │   ├── MappedStatement.java
    │           │   ├── ParameterMapping.java
    │           │   ├── SqlCommandType.java
    │           │   └── SqlSource.java
    │           ├── parsing
    │           │   ├── GenericTokenParser.java
    │           │   └── TokenHandler.java
    │           ├── reflection
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultHandler.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    │               ├── JdbcType.java
    │               ├── TypeAliasRegistry.java
    │               ├── TypeHandler.java
    │               └── TypeHandlerRegistry.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
           

工程源碼:https://github.com/fuzhengwei/small-mybatis

XML 語句解析建構器,核心邏輯類關系,如圖 9-3 所示

《Mybatis 手撸專欄》第9章:細化XML語句建構器,完善靜态SQL解析
  • 解耦原 XMLConfigBuilder 中對 XML 的解析,擴充映射建構器、語句建構器,處理 SQL 的提取和參數的包裝,整個核心流圖以 XMLConfigBuilder#mapperElement 為入口進行串聯調用。
  • 在 XMLStatementBuilder#parseStatementNode 方法中解析

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">...</select>

    配置語句,提取參數類型、結果類型,而這裡的語句處理流程稍微較長,因為需要用到腳本語言驅動器,進行解析處理,建立出 SqlSource 語句資訊。SqlSource 包含了 BoundSql,同時這裡擴充了 ParameterMapping 作為參數包裝傳遞類,而不是僅僅作為 Map 結構包裝。因為通過這樣的方式,可以封裝解析後的 javaType/jdbcType 資訊

2. 解耦映射解析

提供單獨的 XML 映射建構器 XMLMapperBuilder 類,把關于 Mapper 内的 SQL 進行解析處理。提供了這個類以後,就可以把這個類的操作放到 XML 配置建構器,XMLConfigBuilder#mapperElement 中進行使用了。具體我們看下如下代碼。

源碼詳見:

cn.bugstack.mybatis.builder.xml.XMLMapperBuilder

public class XMLMapperBuilder extends BaseBuilder {

    /**
     * 解析
     */
    public void parse() throws Exception {
        // 如果目前資源沒有加載過再加載,防止重複加載
        if (!configuration.isResourceLoaded(resource)) {
            configurationElement(element);
            // 标記一下,已經加載過了
            configuration.addLoadedResource(resource);
            // 綁定映射器到namespace
            configuration.addMapper(Resources.classForName(currentNamespace));
        }
    }

    // 配置mapper元素
    // <mapper namespace="org.mybatis.example.BlogMapper">
    //   <select id="selectBlog" parameterType="int" resultType="Blog">
    //    select * from Blog where id = #{id}
    //   </select>
    // </mapper>
    private void configurationElement(Element element) {
        // 1.配置namespace
        currentNamespace = element.attributeValue("namespace");
        if (currentNamespace.equals("")) {
            throw new RuntimeException("Mapper's namespace cannot be empty");
        }

        // 2.配置select|insert|update|delete
        buildStatementFromContext(element.elements("select"));
    }

    // 配置select|insert|update|delete
    private void buildStatementFromContext(List<Element> list) {
        for (Element element : list) {
            final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, element, currentNamespace);
            statementParser.parseStatementNode();
        }
    }

}
           

在 XMLMapperBuilder#parse 的解析中,主要展現在資源解析判斷、Mapper解析和綁定映射器到;

  • configuration.isResourceLoaded 資源判斷避免重複解析,做了個記錄。
  • configuration.addMapper 綁定映射器主要是把 namespace

    cn.bugstack.mybatis.test.dao.IUserDao

    綁定到 Mapper 上。也就是注冊到映射器注冊機裡。
  • configurationElement 方法調用的 buildStatementFromContext,重在處理 XML 語句建構器,下文中單獨講解。

配置建構器,調用映射建構器,源碼詳見:

cn.bugstack.mybatis.builder.xml.XMLMapperBuilder

public class XMLConfigBuilder extends BaseBuilder {

    /*
     * <mappers>
     *	 <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
     *	 <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
     *	 <mapper resource="org/mybatis/builder/PostMapper.xml"/>
     * </mappers>
     */
    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            String resource = e.attributeValue("resource");
            InputStream inputStream = Resources.getResourceAsStream(resource);

            // 在for循環裡每個mapper都重新new一個XMLMapperBuilder,來解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);
            mapperParser.parse();
        }
    }

}
           
  • 在 XMLConfigBuilder#mapperElement 中,把原來流程化的處理進行解耦,調用 XMLMapperBuilder#parse 方法進行解析處理。

3. 語句建構器

XMLStatementBuilder 語句建構器主要解析 XML 中

select|insert|update|delete

中的語句,目前我們先以 select 解析為案例,後續再擴充其他的解析流程。

源碼詳見:

cn.bugstack.mybatis.builder.xml.XMLStatementBuilder

public class XMLStatementBuilder extends BaseBuilder {

    //解析語句(select|insert|update|delete)
    //<select
    //  id="selectPerson"
    //  parameterType="int"
    //  parameterMap="deprecated"
    //  resultType="hashmap"
    //  resultMap="personResultMap"
    //  flushCache="false"
    //  useCache="true"
    //  timeout="10000"
    //  fetchSize="256"
    //  statementType="PREPARED"
    //  resultSetType="FORWARD_ONLY">
    //  SELECT * FROM PERSON WHERE ID = #{id}
    //</select>
    public void parseStatementNode() {
        String id = element.attributeValue("id");
        // 參數類型
        String parameterType = element.attributeValue("parameterType");
        Class<?> parameterTypeClass = resolveAlias(parameterType);
        // 結果類型
        String resultType = element.attributeValue("resultType");
        Class<?> resultTypeClass = resolveAlias(resultType);
        // 擷取指令類型(select|insert|update|delete)
        String nodeName = element.getName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

        // 擷取預設語言驅動器
        Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
        LanguageDriver langDriver = configuration.getLanguageRegistry().getDriver(langClass);

        SqlSource sqlSource = langDriver.createSqlSource(configuration, element, parameterTypeClass);

        MappedStatement mappedStatement = new MappedStatement.Builder(configuration, currentNamespace + "." + id, sqlCommandType, sqlSource, resultTypeClass).build();

        // 添加解析 SQL
        configuration.addMappedStatement(mappedStatement);
    }

}
           
  • 整個這部分内容的解析,就是從 XMLConfigBuilder 拆解出來關于 Mapper 語句解析的部分,通過這樣這樣的解耦設計,會讓整個流程更加清晰。
  • XMLStatementBuilder#parseStatementNode 方法是解析 SQL 語句節點的過程,包括了語句的ID、參數類型、結果類型、指令(

    select|insert|update|delete

    ),以及使用語言驅動器處理和封裝SQL資訊,當解析完成後寫入到 Configuration 配置檔案中的

    Map<String, MappedStatement>

    映射語句存放中。

4. 腳本語言驅動

在 XMLStatementBuilder#parseStatementNode 語句建構器的解析中,可以看到這麼一塊,擷取預設語言驅動器并解析SQL的操作。其實這部分就是 XML 腳步語言驅動器所實作的功能,在 XMLScriptBuilder 中處理靜态SQL和動态SQL,不過目前我們隻是實作了其中的一部分,待後續這部分架構都完善後在進行擴充,避免一次引入過多的代碼。

4.1 定義接口

源碼詳見:

cn.bugstack.mybatis.scripting.LanguageDriver

public interface LanguageDriver {

    SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);

}
           
  • 定義腳本語言驅動接口,提供建立 SQL 資訊的方法,入參包括了配置、元素、參數。其實它的實作類一共有3個;

    XMLLanguageDriver

    RawLanguageDriver

    VelocityLanguageDriver

    ,這裡我們隻是實作了預設的第一個即可。

4.2 XML語言驅動器實作

源碼詳見:

cn.bugstack.mybatis.scripting.xmltags.XMLLanguageDriver

public class XMLLanguageDriver implements LanguageDriver {

    @Override
    public SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

}
           
  • 關于 XML 語言驅動器的實作比較簡單,隻是封裝了對 XMLScriptBuilder 的調用處理。

4.3 XML腳本建構器解析

源碼詳見:

cn.bugstack.mybatis.scripting.xmltags.XMLScriptBuilder

public class XMLScriptBuilder extends BaseBuilder {

    public SqlSource parseScriptNode() {
        List<SqlNode> contents = parseDynamicTags(element);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        return new RawSqlSource(configuration, rootSqlNode, parameterType);
    }

    List<SqlNode> parseDynamicTags(Element element) {
        List<SqlNode> contents = new ArrayList<>();
        // element.getText 拿到 SQL
        String data = element.getText();
        contents.add(new StaticTextSqlNode(data));
        return contents;
    }

}
           
  • XMLScriptBuilder#parseScriptNode 解析SQL節點的處理其實沒有太多複雜的内容,主要是對 RawSqlSource 的包裝處理。其他小細節可以閱讀源碼進行學習

4.4 SQL源碼建構器

源碼詳見:

cn.bugstack.mybatis.builder.SqlSourceBuilder

public class SqlSourceBuilder extends BaseBuilder {

    private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";

    public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
    }

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        // 傳回靜态 SQL
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
       
        @Override
        public String handleToken(String content) {
            parameterMappings.add(buildParameterMapping(content));
            return "?";
        }

        // 建構參數映射
        private ParameterMapping buildParameterMapping(String content) {
            // 先解析參數映射,就是轉化成一個 HashMap | #{favouriteSection,jdbcType=VARCHAR}
            Map<String, String> propertiesMap = new ParameterExpression(content);
            String property = propertiesMap.get("property");
            Class<?> propertyType = parameterType;
            ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
            return builder.build();
        }

    }
    
}
           
  • 關于以上文中提到的,關于 BoundSql.parameterMappings 的參數就是來自于 ParameterMappingTokenHandler#buildParameterMapping 方法進行建構處理的。
  • 具體的 javaType、jdbcType 會展現到 ParameterExpression 參數表達式中完成解析操作。這個解析過程直接是 Mybatis 自己的源碼,整個過程功能較單一,直接對照學習即可

5. DefaultSqlSession 調用調整

因為以上整個設計和實作,調整了解析過程,以及細化了 SQL 的建立。那麼在 MappedStatement 映射語句中,則使用 SqlSource 替換了 BoundSql,是以在 DefaultSqlSession 中也會有相應的調整。

源碼詳見:

cn.bugstack.mybatis.session.defaults.DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;
    private Executor executor;

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement(statement);
        List<T> list = executor.query(ms, parameter, Executor.NO_RESULT_HANDLER, ms.getSqlSource().getBoundSql(parameter));
        return list.get(0);
    }

}
           
  • 這裡的使用調整也不大,主要展現在擷取SQL的操作;

    ms.getSqlSource().getBoundSql(parameter)

    這樣擷取後,後面的流程就沒有多少變化了。在我們整個解析架構逐漸完善後,就會開始對各個字段的屬性資訊添加進行設定操作。

五、測試

1. 事先準備

1.1 建立庫表

建立一個資料庫名稱為 mybatis 并在庫中建立表 user 以及添加測試資料,如下:

CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
        userId VARCHAR(9) COMMENT '使用者ID',
        userHead VARCHAR(16) COMMENT '使用者頭像',
        createTime TIMESTAMP NULL COMMENT '建立時間',
        updateTime TIMESTAMP NULL COMMENT '更新時間',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');    
           

1.2 配置資料源

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
           
  • 通過

    mybatis-config-datasource.xml

    配置資料源資訊,包括:driver、url、username、password
  • 在這裡 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 進行測試驗證。

1.3 配置Mapper

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>
           
  • 這部分暫時不需要調整,目前還隻是一個入參的類型的參數,後續我們全部完善這部分内容以後,則再提供更多的其他參數進行驗證。

2. 單元測試

@Test
public void test_SqlSessionFactory() throws IOException {
    // 1. 從SqlSessionFactory中擷取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
   
    // 2. 擷取映射器對象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    
    // 3. 測試驗證
    User user = userDao.queryUserInfoById(1L);
    logger.info("測試結果:{}", JSON.toJSONString(user));
}
           
  • 這裡的測試不需要調整,因為我們本章節的開發内容,主要以解耦 XML 的解析,隻要能保持和之前章節一樣,正常輸出結果就可以。

測試結果

《Mybatis 手撸專欄》第9章:細化XML語句建構器,完善靜态SQL解析
07:26:15.049 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 1138410383.
07:26:15.192 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
Disconnected from the target VM, address: '127.0.0.1:54797', transport: 'socket'

Process finished with exit code 0
           
  • 從測試結果和調試的截圖可以看到,我們的 XML 解析處理拆解後,已經可以順利的支撐我們的使用。

六、總結

  • 本章節我們就像是去把原本 CRUD 的代碼,通過設計原則進行拆分和解耦,運用不用的類來承擔不同的職責,完整整個功能的實作。這包括;映射建構器、語句建構器、源碼建構器的綜合使用,以及對應的引用;腳本語言驅動和腳本建構器解析,處理我們的 XML 中的 SQL 語句。
  • 通過這樣的重構代碼,也能讓我們對平常的業務開發中的大片面向過程的流程代碼有所感悟,當你可以細分拆解職責功能到不同的類中去以後,你的代碼會更加的清晰并易于維護。
  • 後續我們将繼續按照現在的擴充結構底座,完成其他子產品的功能邏輯開發,因為了這些基礎内容的建造,再繼續補充功能也會更加容易。當然這些代碼還是需要你熟悉以後才能駕馭,在學習的過程中可以嘗試斷點調試,看看每一個步驟都在完成哪些工作。

公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!